diff --git a/.github/.eslintrc.js b/.github/.eslintrc.js index d1f75405f7a2..41fc57fb9829 100644 --- a/.github/.eslintrc.js +++ b/.github/.eslintrc.js @@ -7,5 +7,6 @@ module.exports = { 'no-await-in-loop': 'off', 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], 'no-continue': 'off', + 'no-restricted-imports': 'off', }, }; diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 9b4b5dac69f5..612a2457d630 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -17017,6 +17017,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index cfbe438022a0..7bdbafc0b722 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12258,6 +12258,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index e0cb48b0a9c4..74cd1509fbfa 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11541,6 +11541,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/checkReactCompiler/action.yml b/.github/actions/javascript/checkReactCompiler/action.yml new file mode 100644 index 000000000000..a8a1c35744c3 --- /dev/null +++ b/.github/actions/javascript/checkReactCompiler/action.yml @@ -0,0 +1,12 @@ +name: 'Check React compiler' +description: 'Compares two lists of compiled files and fails a job if previously successfully compiled files are no longer compiled successfully' +inputs: + OLD_LIST: + description: List of compiled files from the previous commit + required: true + NEW_LIST: + description: List of compiled files from the current commit + required: true +runs: + using: 'node20' + main: 'index.js' diff --git a/.github/actions/javascript/checkReactCompiler/checkReactCompiler.ts b/.github/actions/javascript/checkReactCompiler/checkReactCompiler.ts new file mode 100644 index 000000000000..fbc4b249cb46 --- /dev/null +++ b/.github/actions/javascript/checkReactCompiler/checkReactCompiler.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import * as core from '@actions/core'; + +type ReactCompilerOutput = { + success: string[]; + failure: string[]; +}; + +const run = function (): Promise { + const oldList = JSON.parse(core.getInput('OLD_LIST', {required: true})) as ReactCompilerOutput; + const newList = JSON.parse(core.getInput('NEW_LIST', {required: true})) as ReactCompilerOutput; + + const errors: string[] = []; + + oldList.success.forEach((file) => { + if (newList.success.includes(file) || !newList.failure.includes(file)) { + return; + } + + errors.push(file); + }); + + if (errors.length > 0) { + errors.forEach((error) => console.error(error)); + throw new Error( + 'Some files could be compiled with react-compiler before successfully, but now they can not be compiled. Check https://github.com/Expensify/App/blob/main/contributingGuides/REACT_COMPILER.md documentation to see how you can fix this.', + ); + } + + return Promise.resolve(); +}; + +if (require.main === module) { + run(); +} + +export default run; diff --git a/.github/actions/javascript/checkReactCompiler/index.js b/.github/actions/javascript/checkReactCompiler/index.js new file mode 100644 index 000000000000..1d8ca6adbd16 --- /dev/null +++ b/.github/actions/javascript/checkReactCompiler/index.js @@ -0,0 +1,2883 @@ +/** + * NOTE: This is a compiled file. DO NOT directly edit this file. + */ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 351: +/***/ (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; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issue = exports.issueCommand = void 0; +const os = __importStar(__nccwpck_require__(37)); +const utils_1 = __nccwpck_require__(278); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 186: +/***/ (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; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +const command_1 = __nccwpck_require__(351); +const file_command_1 = __nccwpck_require__(717); +const utils_1 = __nccwpck_require__(278); +const os = __importStar(__nccwpck_require__(37)); +const path = __importStar(__nccwpck_require__(17)); +const oidc_utils_1 = __nccwpck_require__(41); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode = exports.ExitCode || (exports.ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = utils_1.toCommandValue(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('ENV', file_command_1.prepareKeyValueMessage(name, val)); + } + command_1.issueCommand('set-env', { name }, convertedVal); +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + command_1.issueCommand('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + file_command_1.issueFileCommand('PATH', inputPath); + } + else { + command_1.issueCommand('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. + * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. + * Returns an empty string if the value is not defined. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + if (options && options.trimWhitespace === false) { + return val; + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Gets the values of an multiline input. Each value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string[] + * + */ +function getMultilineInput(name, options) { + const inputs = getInput(name, options) + .split('\n') + .filter(x => x !== ''); + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); +} +exports.getMultilineInput = getMultilineInput; +/** + * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. + * Support boolean input list: `true | True | TRUE | false | False | FALSE` . + * The return value is also in boolean type. + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns boolean + */ +function getBooleanInput(name, options) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + const val = getInput(name, options); + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + + `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); +} +exports.getBooleanInput = getBooleanInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('OUTPUT', file_command_1.prepareKeyValueMessage(name, value)); + } + process.stdout.write(os.EOL); + command_1.issueCommand('set-output', { name }, utils_1.toCommandValue(value)); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + command_1.issue('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + command_1.issueCommand('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function error(message, properties = {}) { + command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds a warning issue + * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function warning(message, properties = {}) { + command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + command_1.issue('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + command_1.issue('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('STATE', file_command_1.prepareKeyValueMessage(name, value)); + } + command_1.issueCommand('save-state', { name }, utils_1.toCommandValue(value)); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +function getIDToken(aud) { + return __awaiter(this, void 0, void 0, function* () { + return yield oidc_utils_1.OidcClient.getIDToken(aud); + }); +} +exports.getIDToken = getIDToken; +/** + * Summary exports + */ +var summary_1 = __nccwpck_require__(327); +Object.defineProperty(exports, "summary", ({ enumerable: true, get: function () { return summary_1.summary; } })); +/** + * @deprecated use core.summary + */ +var summary_2 = __nccwpck_require__(327); +Object.defineProperty(exports, "markdownSummary", ({ enumerable: true, get: function () { return summary_2.markdownSummary; } })); +/** + * Path exports + */ +var path_utils_1 = __nccwpck_require__(981); +Object.defineProperty(exports, "toPosixPath", ({ enumerable: true, get: function () { return path_utils_1.toPosixPath; } })); +Object.defineProperty(exports, "toWin32Path", ({ enumerable: true, get: function () { return path_utils_1.toWin32Path; } })); +Object.defineProperty(exports, "toPlatformPath", ({ enumerable: true, get: function () { return path_utils_1.toPlatformPath; } })); +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 717: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// For internal use, subject to change. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__nccwpck_require__(147)); +const os = __importStar(__nccwpck_require__(37)); +const uuid_1 = __nccwpck_require__(840); +const utils_1 = __nccwpck_require__(278); +function issueFileCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${uuid_1.v4()}`; + const convertedValue = utils_1.toCommandValue(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 41: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OidcClient = void 0; +const http_client_1 = __nccwpck_require__(255); +const auth_1 = __nccwpck_require__(526); +const core_1 = __nccwpck_require__(186); +class OidcClient { + static createHttpClient(allowRetry = true, maxRetry = 10) { + const requestOptions = { + allowRetries: allowRetry, + maxRetries: maxRetry + }; + return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions); + } + static getRequestToken() { + const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + if (!token) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'); + } + return token; + } + static getIDTokenUrl() { + const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + if (!runtimeUrl) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable'); + } + return runtimeUrl; + } + static getCall(id_token_url) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const httpclient = OidcClient.createHttpClient(); + const res = yield httpclient + .getJson(id_token_url) + .catch(error => { + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n + Error Message: ${error.result.message}`); + }); + const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; + if (!id_token) { + throw new Error('Response json body do not have ID Token field'); + } + return id_token; + }); + } + static getIDToken(audience) { + return __awaiter(this, void 0, void 0, function* () { + try { + // New ID Token is requested from action service + let id_token_url = OidcClient.getIDTokenUrl(); + if (audience) { + const encodedAudience = encodeURIComponent(audience); + id_token_url = `${id_token_url}&audience=${encodedAudience}`; + } + core_1.debug(`ID token url is ${id_token_url}`); + const id_token = yield OidcClient.getCall(id_token_url); + core_1.setSecret(id_token); + return id_token; + } + catch (error) { + throw new Error(`Error message: ${error.message}`); + } + }); + } +} +exports.OidcClient = OidcClient; +//# sourceMappingURL=oidc-utils.js.map + +/***/ }), + +/***/ 981: +/***/ (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; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0; +const path = __importStar(__nccwpck_require__(17)); +/** + * toPosixPath converts the given path to the posix form. On Windows, \\ will be + * replaced with /. + * + * @param pth. Path to transform. + * @return string Posix path. + */ +function toPosixPath(pth) { + return pth.replace(/[\\]/g, '/'); +} +exports.toPosixPath = toPosixPath; +/** + * toWin32Path converts the given path to the win32 form. On Linux, / will be + * replaced with \\. + * + * @param pth. Path to transform. + * @return string Win32 path. + */ +function toWin32Path(pth) { + return pth.replace(/[/]/g, '\\'); +} +exports.toWin32Path = toWin32Path; +/** + * toPlatformPath converts the given path to a platform-specific path. It does + * this by replacing instances of / and \ with the platform-specific path + * separator. + * + * @param pth The path to platformize. + * @return string The platform-specific path. + */ +function toPlatformPath(pth) { + return pth.replace(/[/\\]/g, path.sep); +} +exports.toPlatformPath = toPlatformPath; +//# sourceMappingURL=path-utils.js.map + +/***/ }), + +/***/ 327: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; +const os_1 = __nccwpck_require__(37); +const fs_1 = __nccwpck_require__(147); +const { access, appendFile, writeFile } = fs_1.promises; +exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; +exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'; +class Summary { + constructor() { + this._buffer = ''; + } + /** + * Finds the summary file path from the environment, rejects if env var is not found or file does not exist + * Also checks r/w permissions. + * + * @returns step summary file path + */ + filePath() { + return __awaiter(this, void 0, void 0, function* () { + if (this._filePath) { + return this._filePath; + } + const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR]; + if (!pathFromEnv) { + throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); + } + try { + yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK); + } + catch (_a) { + throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); + } + this._filePath = pathFromEnv; + return this._filePath; + }); + } + /** + * Wraps content in an HTML tag, adding any HTML attributes + * + * @param {string} tag HTML tag to wrap + * @param {string | null} content content within the tag + * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add + * + * @returns {string} content wrapped in HTML element + */ + wrap(tag, content, attrs = {}) { + const htmlAttrs = Object.entries(attrs) + .map(([key, value]) => ` ${key}="${value}"`) + .join(''); + if (!content) { + return `<${tag}${htmlAttrs}>`; + } + return `<${tag}${htmlAttrs}>${content}`; + } + /** + * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. + * + * @param {SummaryWriteOptions} [options] (optional) options for write operation + * + * @returns {Promise} summary instance + */ + write(options) { + return __awaiter(this, void 0, void 0, function* () { + const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); + const filePath = yield this.filePath(); + const writeFunc = overwrite ? writeFile : appendFile; + yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); + return this.emptyBuffer(); + }); + } + /** + * Clears the summary buffer and wipes the summary file + * + * @returns {Summary} summary instance + */ + clear() { + return __awaiter(this, void 0, void 0, function* () { + return this.emptyBuffer().write({ overwrite: true }); + }); + } + /** + * Returns the current summary buffer as a string + * + * @returns {string} string of summary buffer + */ + stringify() { + return this._buffer; + } + /** + * If the summary buffer is empty + * + * @returns {boolen} true if the buffer is empty + */ + isEmptyBuffer() { + return this._buffer.length === 0; + } + /** + * Resets the summary buffer without writing to summary file + * + * @returns {Summary} summary instance + */ + emptyBuffer() { + this._buffer = ''; + return this; + } + /** + * Adds raw text to the summary buffer + * + * @param {string} text content to add + * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) + * + * @returns {Summary} summary instance + */ + addRaw(text, addEOL = false) { + this._buffer += text; + return addEOL ? this.addEOL() : this; + } + /** + * Adds the operating system-specific end-of-line marker to the buffer + * + * @returns {Summary} summary instance + */ + addEOL() { + return this.addRaw(os_1.EOL); + } + /** + * Adds an HTML codeblock to the summary buffer + * + * @param {string} code content to render within fenced code block + * @param {string} lang (optional) language to syntax highlight code + * + * @returns {Summary} summary instance + */ + addCodeBlock(code, lang) { + const attrs = Object.assign({}, (lang && { lang })); + const element = this.wrap('pre', this.wrap('code', code), attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML list to the summary buffer + * + * @param {string[]} items list of items to render + * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) + * + * @returns {Summary} summary instance + */ + addList(items, ordered = false) { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map(item => this.wrap('li', item)).join(''); + const element = this.wrap(tag, listItems); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML table to the summary buffer + * + * @param {SummaryTableCell[]} rows table rows + * + * @returns {Summary} summary instance + */ + addTable(rows) { + const tableBody = rows + .map(row => { + const cells = row + .map(cell => { + if (typeof cell === 'string') { + return this.wrap('td', cell); + } + const { header, data, colspan, rowspan } = cell; + const tag = header ? 'th' : 'td'; + const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); + return this.wrap(tag, data, attrs); + }) + .join(''); + return this.wrap('tr', cells); + }) + .join(''); + const element = this.wrap('table', tableBody); + return this.addRaw(element).addEOL(); + } + /** + * Adds a collapsable HTML details element to the summary buffer + * + * @param {string} label text for the closed state + * @param {string} content collapsable content + * + * @returns {Summary} summary instance + */ + addDetails(label, content) { + const element = this.wrap('details', this.wrap('summary', label) + content); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML image tag to the summary buffer + * + * @param {string} src path to the image you to embed + * @param {string} alt text description of the image + * @param {SummaryImageOptions} options (optional) addition image attributes + * + * @returns {Summary} summary instance + */ + addImage(src, alt, options) { + const { width, height } = options || {}; + const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); + const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML section heading element + * + * @param {string} text heading text + * @param {number | string} [level=1] (optional) the heading level, default: 1 + * + * @returns {Summary} summary instance + */ + addHeading(text, level) { + const tag = `h${level}`; + const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) + ? tag + : 'h1'; + const element = this.wrap(allowedTag, text); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML thematic break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addSeparator() { + const element = this.wrap('hr', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML line break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addBreak() { + const element = this.wrap('br', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML blockquote to the summary buffer + * + * @param {string} text quote text + * @param {string} cite (optional) citation url + * + * @returns {Summary} summary instance + */ + addQuote(text, cite) { + const attrs = Object.assign({}, (cite && { cite })); + const element = this.wrap('blockquote', text, attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML anchor tag to the summary buffer + * + * @param {string} text link text/content + * @param {string} href hyperlink + * + * @returns {Summary} summary instance + */ + addLink(text, href) { + const element = this.wrap('a', text, { href }); + return this.addRaw(element).addEOL(); + } +} +const _summary = new Summary(); +/** + * @deprecated use `core.summary` + */ +exports.markdownSummary = _summary; +exports.summary = _summary; +//# sourceMappingURL=summary.js.map + +/***/ }), + +/***/ 278: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toCommandProperties = exports.toCommandValue = void 0; +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + file: annotationProperties.file, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 526: +/***/ (function(__unused_webpack_module, exports) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.PersonalAccessTokenCredentialHandler = exports.BearerCredentialHandler = exports.BasicCredentialHandler = void 0; +class BasicCredentialHandler { + constructor(username, password) { + this.username = username; + this.password = password; + } + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BasicCredentialHandler = BasicCredentialHandler; +class BearerCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Bearer ${this.token}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BearerCredentialHandler = BearerCredentialHandler; +class PersonalAccessTokenCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; +//# sourceMappingURL=auth.js.map + +/***/ }), + +/***/ 255: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.HttpClient = exports.isHttps = exports.HttpClientResponse = exports.HttpClientError = exports.getProxyUrl = exports.MediaTypes = exports.Headers = exports.HttpCodes = void 0; +const http = __importStar(__nccwpck_require__(685)); +const https = __importStar(__nccwpck_require__(687)); +const pm = __importStar(__nccwpck_require__(835)); +const tunnel = __importStar(__nccwpck_require__(294)); +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {})); +var Headers; +(function (Headers) { + Headers["Accept"] = "accept"; + Headers["ContentType"] = "content-type"; +})(Headers = exports.Headers || (exports.Headers = {})); +var MediaTypes; +(function (MediaTypes) { + MediaTypes["ApplicationJson"] = "application/json"; +})(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {})); +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +function getProxyUrl(serverUrl) { + const proxyUrl = pm.getProxyUrl(new URL(serverUrl)); + return proxyUrl ? proxyUrl.href : ''; +} +exports.getProxyUrl = getProxyUrl; +const HttpRedirectCodes = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +]; +const HttpResponseRetryCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientError extends Error { + constructor(message, statusCode) { + super(message); + this.name = 'HttpClientError'; + this.statusCode = statusCode; + Object.setPrototypeOf(this, HttpClientError.prototype); + } +} +exports.HttpClientError = HttpClientError; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let output = Buffer.alloc(0); + this.message.on('data', (chunk) => { + output = Buffer.concat([output, chunk]); + }); + this.message.on('end', () => { + resolve(output.toString()); + }); + })); + }); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + const parsedUrl = new URL(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + }); + } + get(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + }); + } + del(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + }); + } + post(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + }); + } + patch(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + }); + } + put(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + }); + } + head(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + }); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request(verb, requestUrl, stream, additionalHeaders); + }); + } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + getJson(requestUrl, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + const res = yield this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + postJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + putJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + patchJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + request(verb, requestUrl, data, headers) { + return __awaiter(this, void 0, void 0, function* () { + if (this._disposed) { + throw new Error('Client has already been disposed.'); + } + const parsedUrl = new URL(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) + ? this._maxRetries + 1 + : 1; + let numTries = 0; + let response; + do { + response = yield this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (const handler of this.handlers) { + if (handler.canHandleAuthentication(response)) { + authenticationHandler = handler; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (response.message.statusCode && + HttpRedirectCodes.includes(response.message.statusCode) && + this._allowRedirects && + redirectsRemaining > 0) { + const redirectUrl = response.message.headers['location']; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + const parsedRedirectUrl = new URL(redirectUrl); + if (parsedUrl.protocol === 'https:' && + parsedUrl.protocol !== parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade) { + throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + yield response.readBody(); + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (const header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header]; + } + } + } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info, data); + redirectsRemaining--; + } + if (!response.message.statusCode || + !HttpResponseRetryCodes.includes(response.message.statusCode)) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + yield response.readBody(); + yield this._performExponentialBackoff(numTries); + } + } while (numTries < maxTries); + return response; + }); + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + function callbackForResult(err, res) { + if (err) { + reject(err); + } + else if (!res) { + // If `err` is not passed, then `res` must be passed. + reject(new Error('Unknown error')); + } + else { + resolve(res); + } + } + this.requestRawWithCallback(info, data, callbackForResult); + }); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + if (typeof data === 'string') { + if (!info.options.headers) { + info.options.headers = {}; + } + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + function handleResult(err, res) { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + } + const req = info.httpModule.request(info.options, (msg) => { + const res = new HttpClientResponse(msg); + handleResult(undefined, res); + }); + let socket; + req.on('socket', sock => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error(`Request timeout: ${info.options.path}`)); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err); + }); + if (data && typeof data === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof data !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl) { + const parsedUrl = new URL(serverUrl); + return this._getAgent(parsedUrl); + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort; + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers) { + for (const handler of this.handlers) { + handler.prepareRequest(info.options); + } + } + return info; + } + _mergeHeaders(headers) { + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers || {})); + } + return lowercaseKeys(headers || {}); + } + _getExistingOrDefaultHeader(additionalHeaders, header, _default) { + let clientHeader; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + _getAgent(parsedUrl) { + let agent; + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (this._keepAlive && !useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + // This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. + if (proxyUrl && proxyUrl.hostname) { + const agentOptions = { + maxSockets, + keepAlive: this._keepAlive, + proxy: Object.assign(Object.assign({}, ((proxyUrl.username || proxyUrl.password) && { + proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` + })), { host: proxyUrl.hostname, port: proxyUrl.port }) + }; + let tunnelAgent; + const overHttps = proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if reusing agent across request and tunneling agent isn't assigned create a new agent + if (this._keepAlive && !agent) { + const options = { keepAlive: this._keepAlive, maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + // if not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }); + } + return agent; + } + _performExponentialBackoff(retryNumber) { + return __awaiter(this, void 0, void 0, function* () { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + }); + } + _processResponse(res, options) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + const statusCode = res.message.statusCode || 0; + const response = { + statusCode, + result: null, + headers: {} + }; + // not found leads to null obj returned + if (statusCode === HttpCodes.NotFound) { + resolve(response); + } + // get the result from the body + function dateTimeDeserializer(key, value) { + if (typeof value === 'string') { + const a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + return value; + } + let obj; + let contents; + try { + contents = yield res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, dateTimeDeserializer); + } + else { + obj = JSON.parse(contents); + } + response.result = obj; + } + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg; + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } + else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } + else { + msg = `Failed request: (${statusCode})`; + } + const err = new HttpClientError(msg, statusCode); + err.result = response.result; + reject(err); + } + else { + resolve(response); + } + })); + }); + } +} +exports.HttpClient = HttpClient; +const lowercaseKeys = (obj) => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 835: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.checkBypass = exports.getProxyUrl = void 0; +function getProxyUrl(reqUrl) { + const usingSsl = reqUrl.protocol === 'https:'; + if (checkBypass(reqUrl)) { + return undefined; + } + const proxyVar = (() => { + if (usingSsl) { + return process.env['https_proxy'] || process.env['HTTPS_PROXY']; + } + else { + return process.env['http_proxy'] || process.env['HTTP_PROXY']; + } + })(); + if (proxyVar) { + return new URL(proxyVar); + } + else { + return undefined; + } +} +exports.getProxyUrl = getProxyUrl; +function checkBypass(reqUrl) { + if (!reqUrl.hostname) { + return false; + } + const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; + if (!noProxy) { + return false; + } + // Determine the request port + let reqPort; + if (reqUrl.port) { + reqPort = Number(reqUrl.port); + } + else if (reqUrl.protocol === 'http:') { + reqPort = 80; + } + else if (reqUrl.protocol === 'https:') { + reqPort = 443; + } + // Format the request hostname and hostname with port + const upperReqHosts = [reqUrl.hostname.toUpperCase()]; + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); + } + // Compare request host against noproxy + for (const upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperReqHosts.some(x => x === upperNoProxyItem)) { + return true; + } + } + return false; +} +exports.checkBypass = checkBypass; +//# sourceMappingURL=proxy.js.map + +/***/ }), + +/***/ 294: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = __nccwpck_require__(219); + + +/***/ }), + +/***/ 219: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +var net = __nccwpck_require__(808); +var tls = __nccwpck_require__(404); +var http = __nccwpck_require__(685); +var https = __nccwpck_require__(687); +var events = __nccwpck_require__(361); +var assert = __nccwpck_require__(491); +var util = __nccwpck_require__(837); + + +exports.httpOverHttp = httpOverHttp; +exports.httpsOverHttp = httpsOverHttp; +exports.httpOverHttps = httpOverHttps; +exports.httpsOverHttps = httpsOverHttps; + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + return agent; +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + return agent; +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + + +function TunnelingAgent(options) { + var self = this; + self.options = options || {}; + self.proxyOptions = self.options.proxy || {}; + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; + self.requests = []; + self.sockets = []; + + self.on('free', function onFree(socket, host, port, localAddress) { + var options = toOptions(host, port, localAddress); + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i]; + if (pending.host === options.host && pending.port === options.port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1); + pending.request.onSocket(socket); + return; + } + } + socket.destroy(); + self.removeSocket(socket); + }); +} +util.inherits(TunnelingAgent, events.EventEmitter); + +TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { + var self = this; + var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push(options); + return; + } + + // If we are under maxSockets create a new one. + self.createSocket(options, function(socket) { + socket.on('free', onFree); + socket.on('close', onCloseOrRemove); + socket.on('agentRemove', onCloseOrRemove); + req.onSocket(socket); + + function onFree() { + self.emit('free', socket, options); + } + + function onCloseOrRemove(err) { + self.removeSocket(socket); + socket.removeListener('free', onFree); + socket.removeListener('close', onCloseOrRemove); + socket.removeListener('agentRemove', onCloseOrRemove); + } + }); +}; + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this; + var placeholder = {}; + self.sockets.push(placeholder); + + var connectOptions = mergeOptions({}, self.proxyOptions, { + method: 'CONNECT', + path: options.host + ':' + options.port, + agent: false, + headers: { + host: options.host + ':' + options.port + } + }); + if (options.localAddress) { + connectOptions.localAddress = options.localAddress; + } + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + + debug('making CONNECT request'); + var connectReq = self.request(connectOptions); + connectReq.useChunkedEncodingByDefault = false; // for v0.6 + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true; + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head); + }); + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners(); + socket.removeAllListeners(); + + if (res.statusCode !== 200) { + debug('tunneling socket could not be established, statusCode=%d', + res.statusCode); + socket.destroy(); + var error = new Error('tunneling socket could not be established, ' + + 'statusCode=' + res.statusCode); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } + + function onError(cause) { + connectReq.removeAllListeners(); + + debug('tunneling socket could not be established, cause=%s\n', + cause.message, cause.stack); + var error = new Error('tunneling socket could not be established, ' + + 'cause=' + cause.message); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + } +}; + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) { + return; + } + this.sockets.splice(pos, 1); + + var pending = this.requests.shift(); + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); + } +}; + +function createSecureSocket(options, cb) { + var self = this; + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + var hostHeader = options.request.getHeader('host'); + var tlsOptions = mergeOptions({}, self.options, { + socket: socket, + servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host + }); + + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, tlsOptions); + self.sockets[self.sockets.indexOf(socket)] = secureSocket; + cb(secureSocket); + }); +} + + +function toOptions(host, port, localAddress) { + if (typeof host === 'string') { // since v0.10 + return { + host: host, + port: port, + localAddress: localAddress + }; + } + return host; // for v0.11 or later +} + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof overrides === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + +var debug; +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0]; + } else { + args.unshift('TUNNEL:'); + } + console.error.apply(console, args); + } +} else { + debug = function() {}; +} +exports.debug = debug; // for test + + +/***/ }), + +/***/ 840: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "v1", ({ + enumerable: true, + get: function () { + return _v.default; + } +})); +Object.defineProperty(exports, "v3", ({ + enumerable: true, + get: function () { + return _v2.default; + } +})); +Object.defineProperty(exports, "v4", ({ + enumerable: true, + get: function () { + return _v3.default; + } +})); +Object.defineProperty(exports, "v5", ({ + enumerable: true, + get: function () { + return _v4.default; + } +})); +Object.defineProperty(exports, "NIL", ({ + enumerable: true, + get: function () { + return _nil.default; + } +})); +Object.defineProperty(exports, "version", ({ + enumerable: true, + get: function () { + return _version.default; + } +})); +Object.defineProperty(exports, "validate", ({ + enumerable: true, + get: function () { + return _validate.default; + } +})); +Object.defineProperty(exports, "stringify", ({ + enumerable: true, + get: function () { + return _stringify.default; + } +})); +Object.defineProperty(exports, "parse", ({ + enumerable: true, + get: function () { + return _parse.default; + } +})); + +var _v = _interopRequireDefault(__nccwpck_require__(628)); + +var _v2 = _interopRequireDefault(__nccwpck_require__(409)); + +var _v3 = _interopRequireDefault(__nccwpck_require__(122)); + +var _v4 = _interopRequireDefault(__nccwpck_require__(120)); + +var _nil = _interopRequireDefault(__nccwpck_require__(332)); + +var _version = _interopRequireDefault(__nccwpck_require__(595)); + +var _validate = _interopRequireDefault(__nccwpck_require__(900)); + +var _stringify = _interopRequireDefault(__nccwpck_require__(950)); + +var _parse = _interopRequireDefault(__nccwpck_require__(746)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/***/ }), + +/***/ 569: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _crypto = _interopRequireDefault(__nccwpck_require__(113)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function md5(bytes) { + if (Array.isArray(bytes)) { + bytes = Buffer.from(bytes); + } else if (typeof bytes === 'string') { + bytes = Buffer.from(bytes, 'utf8'); + } + + return _crypto.default.createHash('md5').update(bytes).digest(); +} + +var _default = md5; +exports["default"] = _default; + +/***/ }), + +/***/ 332: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _default = '00000000-0000-0000-0000-000000000000'; +exports["default"] = _default; + +/***/ }), + +/***/ 746: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _validate = _interopRequireDefault(__nccwpck_require__(900)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function parse(uuid) { + if (!(0, _validate.default)(uuid)) { + throw TypeError('Invalid UUID'); + } + + let v; + const arr = new Uint8Array(16); // Parse ########-....-....-....-............ + + arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24; + arr[1] = v >>> 16 & 0xff; + arr[2] = v >>> 8 & 0xff; + arr[3] = v & 0xff; // Parse ........-####-....-....-............ + + arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8; + arr[5] = v & 0xff; // Parse ........-....-####-....-............ + + arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8; + arr[7] = v & 0xff; // Parse ........-....-....-####-............ + + arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8; + arr[9] = v & 0xff; // Parse ........-....-....-....-############ + // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes) + + arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff; + arr[11] = v / 0x100000000 & 0xff; + arr[12] = v >>> 24 & 0xff; + arr[13] = v >>> 16 & 0xff; + arr[14] = v >>> 8 & 0xff; + arr[15] = v & 0xff; + return arr; +} + +var _default = parse; +exports["default"] = _default; + +/***/ }), + +/***/ 814: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; +exports["default"] = _default; + +/***/ }), + +/***/ 807: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = rng; + +var _crypto = _interopRequireDefault(__nccwpck_require__(113)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const rnds8Pool = new Uint8Array(256); // # of random values to pre-allocate + +let poolPtr = rnds8Pool.length; + +function rng() { + if (poolPtr > rnds8Pool.length - 16) { + _crypto.default.randomFillSync(rnds8Pool); + + poolPtr = 0; + } + + return rnds8Pool.slice(poolPtr, poolPtr += 16); +} + +/***/ }), + +/***/ 274: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _crypto = _interopRequireDefault(__nccwpck_require__(113)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function sha1(bytes) { + if (Array.isArray(bytes)) { + bytes = Buffer.from(bytes); + } else if (typeof bytes === 'string') { + bytes = Buffer.from(bytes, 'utf8'); + } + + return _crypto.default.createHash('sha1').update(bytes).digest(); +} + +var _default = sha1; +exports["default"] = _default; + +/***/ }), + +/***/ 950: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _validate = _interopRequireDefault(__nccwpck_require__(900)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Convert array of 16 byte values to UUID string format of the form: + * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + */ +const byteToHex = []; + +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 0x100).toString(16).substr(1)); +} + +function stringify(arr, offset = 0) { + // Note: Be careful editing this code! It's been tuned for performance + // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 + const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one + // of the following: + // - One or more input array values don't map to a hex octet (leading to + // "undefined" in the uuid) + // - Invalid input values for the RFC `version` or `variant` fields + + if (!(0, _validate.default)(uuid)) { + throw TypeError('Stringified UUID is invalid'); + } + + return uuid; +} + +var _default = stringify; +exports["default"] = _default; + +/***/ }), + +/***/ 628: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _rng = _interopRequireDefault(__nccwpck_require__(807)); + +var _stringify = _interopRequireDefault(__nccwpck_require__(950)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html +let _nodeId; + +let _clockseq; // Previous uuid creation time + + +let _lastMSecs = 0; +let _lastNSecs = 0; // See https://github.com/uuidjs/uuid for API details + +function v1(options, buf, offset) { + let i = buf && offset || 0; + const b = buf || new Array(16); + options = options || {}; + let node = options.node || _nodeId; + let clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // node and clockseq need to be initialized to random values if they're not + // specified. We do this lazily to minimize issues related to insufficient + // system entropy. See #189 + + if (node == null || clockseq == null) { + const seedBytes = options.random || (options.rng || _rng.default)(); + + if (node == null) { + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + node = _nodeId = [seedBytes[0] | 0x01, seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]]; + } + + if (clockseq == null) { + // Per 4.2.2, randomize (14 bit) clockseq + clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff; + } + } // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + + + let msecs = options.msecs !== undefined ? options.msecs : Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + + let nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs) + + const dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression + + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + + + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } // Per 4.2.1.2 Throw error if too many uuids are requested + + + if (nsecs >= 10000) { + throw new Error("uuid.v1(): Can't create more than 10M uuids/sec"); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + + msecs += 12219292800000; // `time_low` + + const tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; // `time_mid` + + const tmh = msecs / 0x100000000 * 10000 & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; // `time_high_and_version` + + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + + b[i++] = tmh >>> 16 & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + + b[i++] = clockseq >>> 8 | 0x80; // `clock_seq_low` + + b[i++] = clockseq & 0xff; // `node` + + for (let n = 0; n < 6; ++n) { + b[i + n] = node[n]; + } + + return buf || (0, _stringify.default)(b); +} + +var _default = v1; +exports["default"] = _default; + +/***/ }), + +/***/ 409: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _v = _interopRequireDefault(__nccwpck_require__(998)); + +var _md = _interopRequireDefault(__nccwpck_require__(569)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const v3 = (0, _v.default)('v3', 0x30, _md.default); +var _default = v3; +exports["default"] = _default; + +/***/ }), + +/***/ 998: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = _default; +exports.URL = exports.DNS = void 0; + +var _stringify = _interopRequireDefault(__nccwpck_require__(950)); + +var _parse = _interopRequireDefault(__nccwpck_require__(746)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function stringToBytes(str) { + str = unescape(encodeURIComponent(str)); // UTF8 escape + + const bytes = []; + + for (let i = 0; i < str.length; ++i) { + bytes.push(str.charCodeAt(i)); + } + + return bytes; +} + +const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; +exports.DNS = DNS; +const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; +exports.URL = URL; + +function _default(name, version, hashfunc) { + function generateUUID(value, namespace, buf, offset) { + if (typeof value === 'string') { + value = stringToBytes(value); + } + + if (typeof namespace === 'string') { + namespace = (0, _parse.default)(namespace); + } + + if (namespace.length !== 16) { + throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)'); + } // Compute hash of namespace and value, Per 4.3 + // Future: Use spread syntax when supported on all platforms, e.g. `bytes = + // hashfunc([...namespace, ... value])` + + + let bytes = new Uint8Array(16 + value.length); + bytes.set(namespace); + bytes.set(value, namespace.length); + bytes = hashfunc(bytes); + bytes[6] = bytes[6] & 0x0f | version; + bytes[8] = bytes[8] & 0x3f | 0x80; + + if (buf) { + offset = offset || 0; + + for (let i = 0; i < 16; ++i) { + buf[offset + i] = bytes[i]; + } + + return buf; + } + + return (0, _stringify.default)(bytes); + } // Function#name is not settable on some platforms (#270) + + + try { + generateUUID.name = name; // eslint-disable-next-line no-empty + } catch (err) {} // For CommonJS default export support + + + generateUUID.DNS = DNS; + generateUUID.URL = URL; + return generateUUID; +} + +/***/ }), + +/***/ 122: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _rng = _interopRequireDefault(__nccwpck_require__(807)); + +var _stringify = _interopRequireDefault(__nccwpck_require__(950)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function v4(options, buf, offset) { + options = options || {}; + + const rnds = options.random || (options.rng || _rng.default)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + + + rnds[6] = rnds[6] & 0x0f | 0x40; + rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided + + if (buf) { + offset = offset || 0; + + for (let i = 0; i < 16; ++i) { + buf[offset + i] = rnds[i]; + } + + return buf; + } + + return (0, _stringify.default)(rnds); +} + +var _default = v4; +exports["default"] = _default; + +/***/ }), + +/***/ 120: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _v = _interopRequireDefault(__nccwpck_require__(998)); + +var _sha = _interopRequireDefault(__nccwpck_require__(274)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const v5 = (0, _v.default)('v5', 0x50, _sha.default); +var _default = v5; +exports["default"] = _default; + +/***/ }), + +/***/ 900: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _regex = _interopRequireDefault(__nccwpck_require__(814)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function validate(uuid) { + return typeof uuid === 'string' && _regex.default.test(uuid); +} + +var _default = validate; +exports["default"] = _default; + +/***/ }), + +/***/ 595: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _validate = _interopRequireDefault(__nccwpck_require__(900)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function version(uuid) { + if (!(0, _validate.default)(uuid)) { + throw TypeError('Invalid UUID'); + } + + return parseInt(uuid.substr(14, 1), 16); +} + +var _default = version; +exports["default"] = _default; + +/***/ }), + +/***/ 83: +/***/ (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; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +/* eslint-disable @typescript-eslint/naming-convention */ +const core = __importStar(__nccwpck_require__(186)); +const run = function () { + const oldList = JSON.parse(core.getInput('OLD_LIST', { required: true })); + const newList = JSON.parse(core.getInput('NEW_LIST', { required: true })); + const errors = []; + oldList.success.forEach((file) => { + if (newList.success.includes(file) || !newList.failure.includes(file)) { + return; + } + errors.push(file); + }); + if (errors.length > 0) { + errors.forEach((error) => console.error(error)); + throw new Error('Some files could be compiled with react-compiler before successfully, but now they can not be compiled. Check https://github.com/Expensify/App/blob/main/contributingGuides/REACT_COMPILER.md documentation to see how you can fix this.'); + } + return Promise.resolve(); +}; +if (require.main === require.cache[eval('__filename')]) { + run(); +} +exports["default"] = run; + + +/***/ }), + +/***/ 491: +/***/ ((module) => { + +"use strict"; +module.exports = require("assert"); + +/***/ }), + +/***/ 113: +/***/ ((module) => { + +"use strict"; +module.exports = require("crypto"); + +/***/ }), + +/***/ 361: +/***/ ((module) => { + +"use strict"; +module.exports = require("events"); + +/***/ }), + +/***/ 147: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs"); + +/***/ }), + +/***/ 685: +/***/ ((module) => { + +"use strict"; +module.exports = require("http"); + +/***/ }), + +/***/ 687: +/***/ ((module) => { + +"use strict"; +module.exports = require("https"); + +/***/ }), + +/***/ 808: +/***/ ((module) => { + +"use strict"; +module.exports = require("net"); + +/***/ }), + +/***/ 37: +/***/ ((module) => { + +"use strict"; +module.exports = require("os"); + +/***/ }), + +/***/ 17: +/***/ ((module) => { + +"use strict"; +module.exports = require("path"); + +/***/ }), + +/***/ 404: +/***/ ((module) => { + +"use strict"; +module.exports = require("tls"); + +/***/ }), + +/***/ 837: +/***/ ((module) => { + +"use strict"; +module.exports = require("util"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; +/******/ +/************************************************************************/ +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module is referenced by other modules so it can't be inlined +/******/ var __webpack_exports__ = __nccwpck_require__(83); +/******/ module.exports = __webpack_exports__; +/******/ +/******/ })() +; diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index d9fa61b08448..6e7237e7cd93 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -14353,6 +14353,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index c1cca94b6cbd..82bf90ef6d2b 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11502,6 +11502,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts index c15036c93232..b9d01702e66e 100644 --- a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts +++ b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts @@ -19,26 +19,32 @@ async function run() { // eslint-disable-next-line @typescript-eslint/naming-convention workflow_id: 'platformDeploy.yml', status: 'completed', - event: isProductionDeploy ? 'release' : 'push', }) ).data.workflow_runs // Note: we filter out cancelled runs instead of looking only for success runs // because if a build fails on even one platform, then it will have the status 'failure' .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); - // Find the most recent deploy workflow for which at least one of the build jobs finished successfully. + // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); while ( - lastSuccessfulDeploy && - !( - await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + lastSuccessfulDeploy?.head_branch && + (( + await GithubUtils.octokit.repos.getReleaseByTag({ owner: github.context.repo.owner, repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', + tag: lastSuccessfulDeploy.head_branch, }) - ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success') + ).data.prerelease === isProductionDeploy || + !( + await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: lastSuccessfulDeploy.id, + filter: 'latest', + }) + ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) ) { console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); lastSuccessfulDeploy = completedDeploys.shift(); diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 3173dd2358eb..05ae086fcc24 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11514,21 +11514,25 @@ async function run() { // eslint-disable-next-line @typescript-eslint/naming-convention workflow_id: 'platformDeploy.yml', status: 'completed', - event: isProductionDeploy ? 'release' : 'push', })).data.workflow_runs // Note: we filter out cancelled runs instead of looking only for success runs // because if a build fails on even one platform, then it will have the status 'failure' .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); - // Find the most recent deploy workflow for which at least one of the build jobs finished successfully. + // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); - while (lastSuccessfulDeploy && - !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + while (lastSuccessfulDeploy?.head_branch && + ((await GithubUtils_1.default.octokit.repos.getReleaseByTag({ owner: github.context.repo.owner, repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', - })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) { + tag: lastSuccessfulDeploy.head_branch, + })).data.prerelease === isProductionDeploy || + !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: lastSuccessfulDeploy.id, + filter: 'latest', + })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))) { console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); lastSuccessfulDeploy = completedDeploys.shift(); } @@ -11636,6 +11640,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index aff2a13da163..7b7ff20ef426 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -2769,6 +2769,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index f1c2054cca1d..8580842b380c 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11604,6 +11604,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 19acda9b7474..9e823e8da5ae 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11502,6 +11502,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 06d569d6fb5a..9f97e4a72d20 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -6555,6 +6555,1174 @@ function isPlainObject(o) { exports.isPlainObject = isPlainObject; +/***/ }), + +/***/ 5902: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var hashClear = __nccwpck_require__(1789), + hashDelete = __nccwpck_require__(712), + hashGet = __nccwpck_require__(5395), + hashHas = __nccwpck_require__(5232), + hashSet = __nccwpck_require__(7320); + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +module.exports = Hash; + + +/***/ }), + +/***/ 6608: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var listCacheClear = __nccwpck_require__(9792), + listCacheDelete = __nccwpck_require__(7716), + listCacheGet = __nccwpck_require__(5789), + listCacheHas = __nccwpck_require__(9386), + listCacheSet = __nccwpck_require__(7399); + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +module.exports = ListCache; + + +/***/ }), + +/***/ 881: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getNative = __nccwpck_require__(4479), + root = __nccwpck_require__(9882); + +/* Built-in method references that are verified to be native. */ +var Map = getNative(root, 'Map'); + +module.exports = Map; + + +/***/ }), + +/***/ 938: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var mapCacheClear = __nccwpck_require__(1610), + mapCacheDelete = __nccwpck_require__(6657), + mapCacheGet = __nccwpck_require__(1372), + mapCacheHas = __nccwpck_require__(609), + mapCacheSet = __nccwpck_require__(5582); + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +module.exports = MapCache; + + +/***/ }), + +/***/ 9213: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Built-in value references. */ +var Symbol = root.Symbol; + +module.exports = Symbol; + + +/***/ }), + +/***/ 6752: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var eq = __nccwpck_require__(1901); + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +module.exports = assocIndexOf; + + +/***/ }), + +/***/ 7497: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Symbol = __nccwpck_require__(9213), + getRawTag = __nccwpck_require__(923), + objectToString = __nccwpck_require__(4200); + +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); +} + +module.exports = baseGetTag; + + +/***/ }), + +/***/ 411: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isFunction = __nccwpck_require__(7799), + isMasked = __nccwpck_require__(9058), + isObject = __nccwpck_require__(3334), + toSource = __nccwpck_require__(6928); + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used for built-in method references. */ +var funcProto = Function.prototype, + objectProto = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +module.exports = baseIsNative; + + +/***/ }), + +/***/ 8380: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +module.exports = coreJsData; + + +/***/ }), + +/***/ 2085: +/***/ ((module) => { + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +module.exports = freeGlobal; + + +/***/ }), + +/***/ 9980: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isKeyable = __nccwpck_require__(3308); + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +module.exports = getMapData; + + +/***/ }), + +/***/ 4479: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseIsNative = __nccwpck_require__(411), + getValue = __nccwpck_require__(3542); + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +module.exports = getNative; + + +/***/ }), + +/***/ 923: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Symbol = __nccwpck_require__(9213); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; +} + +module.exports = getRawTag; + + +/***/ }), + +/***/ 3542: +/***/ ((module) => { + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +module.exports = getValue; + + +/***/ }), + +/***/ 1789: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; +} + +module.exports = hashClear; + + +/***/ }), + +/***/ 712: +/***/ ((module) => { + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; +} + +module.exports = hashDelete; + + +/***/ }), + +/***/ 5395: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; +} + +module.exports = hashGet; + + +/***/ }), + +/***/ 5232: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); +} + +module.exports = hashHas; + + +/***/ }), + +/***/ 7320: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} + +module.exports = hashSet; + + +/***/ }), + +/***/ 3308: +/***/ ((module) => { + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +module.exports = isKeyable; + + +/***/ }), + +/***/ 9058: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var coreJsData = __nccwpck_require__(8380); + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +module.exports = isMasked; + + +/***/ }), + +/***/ 9792: +/***/ ((module) => { + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; + this.size = 0; +} + +module.exports = listCacheClear; + + +/***/ }), + +/***/ 7716: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** Used for built-in method references. */ +var arrayProto = Array.prototype; + +/** Built-in value references. */ +var splice = arrayProto.splice; + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; +} + +module.exports = listCacheDelete; + + +/***/ }), + +/***/ 5789: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +module.exports = listCacheGet; + + +/***/ }), + +/***/ 9386: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +module.exports = listCacheHas; + + +/***/ }), + +/***/ 7399: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +module.exports = listCacheSet; + + +/***/ }), + +/***/ 1610: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Hash = __nccwpck_require__(5902), + ListCache = __nccwpck_require__(6608), + Map = __nccwpck_require__(881); + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; +} + +module.exports = mapCacheClear; + + +/***/ }), + +/***/ 6657: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; +} + +module.exports = mapCacheDelete; + + +/***/ }), + +/***/ 1372: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +module.exports = mapCacheGet; + + +/***/ }), + +/***/ 609: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +module.exports = mapCacheHas; + + +/***/ }), + +/***/ 5582: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; +} + +module.exports = mapCacheSet; + + +/***/ }), + +/***/ 3041: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getNative = __nccwpck_require__(4479); + +/* Built-in method references that are verified to be native. */ +var nativeCreate = getNative(Object, 'create'); + +module.exports = nativeCreate; + + +/***/ }), + +/***/ 4200: +/***/ ((module) => { + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString.call(value); +} + +module.exports = objectToString; + + +/***/ }), + +/***/ 9882: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var freeGlobal = __nccwpck_require__(2085); + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +module.exports = root; + + +/***/ }), + +/***/ 6928: +/***/ ((module) => { + +/** Used for built-in method references. */ +var funcProto = Function.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +module.exports = toSource; + + +/***/ }), + +/***/ 1901: +/***/ ((module) => { + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +module.exports = eq; + + +/***/ }), + +/***/ 7799: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseGetTag = __nccwpck_require__(7497), + isObject = __nccwpck_require__(3334); + +/** `Object#toString` result references. */ +var asyncTag = '[object AsyncFunction]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + proxyTag = '[object Proxy]'; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; +} + +module.exports = isFunction; + + +/***/ }), + +/***/ 3334: +/***/ ((module) => { + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +module.exports = isObject; + + +/***/ }), + +/***/ 9885: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var MapCache = __nccwpck_require__(938); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ +function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; +} + +// Expose `MapCache`. +memoize.Cache = MapCache; + +module.exports = memoize; + + /***/ }), /***/ 467: @@ -11500,6 +12668,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); /* eslint-disable @typescript-eslint/naming-convention, import/no-import-module-exports */ const core = __importStar(__nccwpck_require__(2186)); const github_1 = __nccwpck_require__(5438); +const memoize_1 = __importDefault(__nccwpck_require__(9885)); const ActionUtils = __importStar(__nccwpck_require__(6981)); const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); @@ -11535,6 +12704,7 @@ async function commentPR(PR, message) { } } const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; +const getCommit = (0, memoize_1.default)(GithubUtils_1.default.octokit.git.getCommit); async function run() { const prList = ActionUtils.getJSONInput('PR_LIST', { required: true }).map((num) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true }); @@ -11573,25 +12743,11 @@ async function run() { } return; } - // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag const { data: recentTags } = await GithubUtils_1.default.octokit.repos.listTags({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, per_page: 100, }); - const currentTag = recentTags.find((tag) => tag.name === version); - if (!currentTag) { - const err = `Could not find tag matching ${version}`; - console.error(err); - core.setFailed(err); - return; - } - const { data: commit } = await GithubUtils_1.default.octokit.git.getCommit({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - commit_sha: currentTag.commit.sha, - }); - const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); for (const prNumber of prList) { /* * Determine who the deployer for the PR is. The "deployer" for staging deploys is: @@ -11604,7 +12760,28 @@ async function run() { repo: CONST_1.default.APP_REPO, pull_number: prNumber, }); - const deployer = isCP ? commit.committer.name : pr.merged_by?.login; + // Check for the CP Staging label on the issue to see if it was cherry-picked + const isCP = pr.labels.some(({ name: labelName }) => labelName === CONST_1.default.LABELS.CP_STAGING); + // Determine the deployer. For most PRs it will be whoever merged the PR. + // For CPs it will be whoever created the tag for the PR (i.e: whoever triggered the CP) + let deployer = pr.merged_by?.login; + if (isCP) { + for (const tag of recentTags) { + const { data: commit } = await getCommit({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + commit_sha: tag.commit.sha, + }); + const prNumForCPMergeCommit = commit.message.match(/Merge pull request #(\d+)[\S\s]*\(cherry picked from commit .*\)/); + if (prNumForCPMergeCommit?.at(1) === String(prNumber)) { + const cpActor = commit.message.match(/.*\(CP triggered by (.*)\)/)?.at(1); + if (cpActor) { + deployer = cpActor; + } + break; + } + } + } const title = pr.title; const deployMessage = deployer ? getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title) : ''; await commentPR(prNumber, deployMessage); @@ -11709,6 +12886,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 53018cbb035e..71a5c7d5c6ee 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -2,6 +2,7 @@ import * as core from '@actions/core'; import {context} from '@actions/github'; import type {RequestError} from '@octokit/types'; +import memoize from 'lodash/memoize'; import * as ActionUtils from '@github/libs/ActionUtils'; import CONST from '@github/libs/CONST'; import GithubUtils from '@github/libs/GithubUtils'; @@ -42,6 +43,8 @@ async function commentPR(PR: number, message: string) { const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; +const getCommit = memoize(GithubUtils.octokit.git.getCommit); + async function run() { const prList = (ActionUtils.getJSONInput('PR_LIST', {required: true}) as string[]).map((num) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}) as boolean; @@ -88,25 +91,11 @@ async function run() { return; } - // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag const {data: recentTags} = await GithubUtils.octokit.repos.listTags({ owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, per_page: 100, }); - const currentTag = recentTags.find((tag) => tag.name === version); - if (!currentTag) { - const err = `Could not find tag matching ${version}`; - console.error(err); - core.setFailed(err); - return; - } - const {data: commit} = await GithubUtils.octokit.git.getCommit({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - commit_sha: currentTag.commit.sha, - }); - const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); for (const prNumber of prList) { /* @@ -120,7 +109,30 @@ async function run() { repo: CONST.APP_REPO, pull_number: prNumber, }); - const deployer = isCP ? commit.committer.name : pr.merged_by?.login; + + // Check for the CP Staging label on the issue to see if it was cherry-picked + const isCP = pr.labels.some(({name: labelName}) => labelName === CONST.LABELS.CP_STAGING); + + // Determine the deployer. For most PRs it will be whoever merged the PR. + // For CPs it will be whoever created the tag for the PR (i.e: whoever triggered the CP) + let deployer = pr.merged_by?.login; + if (isCP) { + for (const tag of recentTags) { + const {data: commit} = await getCommit({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + commit_sha: tag.commit.sha, + }); + const prNumForCPMergeCommit = commit.message.match(/Merge pull request #(\d+)[\S\s]*\(cherry picked from commit .*\)/); + if (prNumForCPMergeCommit?.at(1) === String(prNumber)) { + const cpActor = commit.message.match(/.*\(CP triggered by (.*)\)/)?.at(1); + if (cpActor) { + deployer = cpActor; + } + break; + } + } + } const title = pr.title; const deployMessage = deployer ? getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title) : ''; diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 6c47718584ce..4f62879a4419 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11601,6 +11601,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 7f7c4ecc38ac..c14b825e1198 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -18089,6 +18089,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 75d40871926c..83131f363ef8 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11512,6 +11512,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 0cec1bc183f0..2a0977db8016 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11604,6 +11604,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 0d441b9f52d3..49a4341b84af 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11544,6 +11544,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/libs/CONST.ts b/.github/libs/CONST.ts index a46f4afc421c..499ff15e5aeb 100644 --- a/.github/libs/CONST.ts +++ b/.github/libs/CONST.ts @@ -14,6 +14,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh index ae8d87b38341..ea675aef5634 100755 --- a/.github/scripts/buildActions.sh +++ b/.github/scripts/buildActions.sh @@ -12,6 +12,7 @@ declare -r GITHUB_ACTIONS=( "$ACTIONS_DIR/awaitStagingDeploys/awaitStagingDeploys.ts" "$ACTIONS_DIR/bumpVersion/bumpVersion.ts" "$ACTIONS_DIR/checkDeployBlockers/checkDeployBlockers.ts" + "$ACTIONS_DIR/checkReactCompiler/checkReactCompiler.ts" "$ACTIONS_DIR/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts" "$ACTIONS_DIR/getDeployPullRequestList/getDeployPullRequestList.ts" "$ACTIONS_DIR/getPreviousVersion/getPreviousVersion.ts" diff --git a/.github/scripts/verifyPodfile.sh b/.github/scripts/verifyPodfile.sh index 2c9a7dee672a..ff67b11c8657 100755 --- a/.github/scripts/verifyPodfile.sh +++ b/.github/scripts/verifyPodfile.sh @@ -63,6 +63,7 @@ if ! SPEC_DIRS=$(yq '.["EXTERNAL SOURCES"].[].":path" | select( . == "*node_modu cleanupAndExit 1 fi +# Retrieve a list of podspec paths from react-native config if ! read_lines_into_array PODSPEC_PATHS < <(npx react-native config | jq --raw-output '.dependencies[].platforms.ios.podspecPath | select ( . != null)'); then error "Error: could not parse podspec paths from react-native config command" cleanupAndExit 1 diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index dd2c92e95568..1772d5d309cc 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -82,7 +82,6 @@ jobs: id: cherryPick run: | echo "Attempting to cherry-pick ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}" - git config user.name ${{ github.actor }} if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then echo "🎉 No conflicts! CP was a success, PR can be automerged 🎉" echo "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT" @@ -93,7 +92,7 @@ jobs: GIT_MERGE_AUTOEDIT=no git cherry-pick --continue echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT" fi - git config user.name OSBotify + git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})" - name: Push changes run: | @@ -122,6 +121,11 @@ jobs: env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} + - name: Label PR with CP Staging + run: gh pr edit ${{ inputs.PULL_REQUEST_NUMBER }} --add-label 'CP Staging' + env: + GITHUB_TOKEN: ${{ github.token }} + - name: "Announces a CP failure in the #announce Slack room" uses: 8398a7/action-slack@v3 if: ${{ failure() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3f5a8881f244..a20a53678712 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,11 +23,11 @@ jobs: OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} - - name: Tag version - run: git tag "$(npm run print-version --silent)" + - name: Get current app version + run: echo "STAGING_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: 🚀 Push tags to trigger staging deploy 🚀 - run: git push --tags + - name: 🚀 Create prerelease to trigger staging deploy 🚀 + run: gh release create ${{ env.STAGING_VERSION }} --title ${{ env.STAGING_VERSION }} --generate-notes --prerelease - name: Warn deployers if staging deploy failed if: ${{ failure() }} @@ -68,8 +68,8 @@ jobs: - name: Get current app version run: echo "PRODUCTION_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --title ${{ env.PRODUCTION_VERSION }} --generate-notes + - name: 🚀 Edit the release to be no longer a prerelease to deploy production 🚀 + run: gh release edit ${{ env.PRODUCTION_VERSION }} --prerelease=false --latest env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml index cb5dc6d28b32..47df9b4285b9 100644 --- a/.github/workflows/deployBlocker.yml +++ b/.github/workflows/deployBlocker.yml @@ -39,7 +39,7 @@ jobs: channel: '#expensify-open-source', attachments: [{ color: "#DB4545", - text: '💥 We have found a New Expensify Deploy Blocker, if you have any idea which PR could be causing this, please comment in the issue: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>' + text: '💥 New Deploy Blocker: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>. If you have any idea which PR could be causing this, please comment in the issue.' }] } env: diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 3cb6cd8e5d81..397122930652 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -1,15 +1,12 @@ name: Build and deploy android, desktop, iOS, and web clients -# This workflow is run when any tag is published +# This workflow is run when a release or prerelease is created on: - push: - tags: - - '*' release: - types: [created] + types: [prereleased, released] env: - SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} + SHOULD_DEPLOY_PRODUCTION: ${{ github.event.action == 'released' }} concurrency: group: ${{ github.workflow }}-${{ github.event_name }} @@ -36,7 +33,7 @@ jobs: deployChecklist: name: Create or update deploy checklist uses: ./.github/workflows/createDeployChecklist.yml - if: ${{ github.event_name != 'release' }} + if: ${{ github.event.action != 'released' }} needs: validateActor secrets: inherit @@ -226,7 +223,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd ios && bundle exec pod install + command: scripts/pod-install.sh - name: Decrypt AppStore profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 5cfe5e213d2f..e5ccdfa53076 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -4,7 +4,7 @@ name: Process new code merged to main on: push: branches: [main] - paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**, workflow_tests/**] + paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**] jobs: typecheck: diff --git a/.github/workflows/reactCompiler.yml b/.github/workflows/reactCompiler.yml new file mode 100644 index 000000000000..dc2e1b17d804 --- /dev/null +++ b/.github/workflows/reactCompiler.yml @@ -0,0 +1,45 @@ +name: 🔮 React Compiler + +on: + pull_request: + paths: + - ".github/workflows/reactCompiler.yml" + - "src/**" + - "package.json" + +jobs: + check: + name: 🧬 Conformity + runs-on: ubuntu-latest + + steps: + - name: Checkout to target branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + - name: Setup Node + uses: ./.github/actions/composite/setupNode + - name: Get list of compiled files (main) + id: old-list + run: | + RAW_OUTPUT=$(npx react-compiler-healthcheck --json 2>/dev/null) + echo "Raw output: $RAW_OUTPUT" + OLD_LIST=$(echo "$RAW_OUTPUT" | jq -c .) + echo "OLD_LIST=$OLD_LIST" >> "$GITHUB_OUTPUT" + - name: Checkout to current branch + uses: actions/checkout@v4 + - name: Setup Node + uses: ./.github/actions/composite/setupNode + - name: Get list of compiled files (PR) + id: new-list + run: | + RAW_OUTPUT=$(npx react-compiler-healthcheck --json 2>/dev/null) + echo "Raw output: $RAW_OUTPUT" + NEW_LIST=$(echo "$RAW_OUTPUT" | jq -c .) + echo "NEW_LIST=$NEW_LIST" >> "$GITHUB_OUTPUT" + - name: Check for react compiler changes + id: checkReactCompiler + uses: ./.github/actions/javascript/checkReactCompiler + with: + NEW_LIST: ${{ steps.new-list.outputs.NEW_LIST }} + OLD_LIST: ${{ steps.old-list.outputs.OLD_LIST }} diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a0489a52711b..d4a25a63952b 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] - paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, workflow_tests/**, '**.md', '**.sh'] + paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, '**.md', '**.sh'] jobs: perf-tests: diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 30a30918f4f6..42d946cece95 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -3,7 +3,7 @@ name: Send Reassure Performance Tests to Graphite on: push: branches: [main] - paths-ignore: [docs/**, contributingGuides/**, jest/**, workflow_tests/**] + paths-ignore: [docs/**, contributingGuides/**, jest/**] jobs: perf-tests: @@ -36,7 +36,7 @@ jobs: - name: Get and save graphite string id: saveGraphiteString uses: ./.github/actions/javascript/getGraphiteString - with: + with: PR_NUMBER: ${{ steps.getMergedPullRequest.outputs.number }} - name: Send graphite data diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 024f5b712a3f..da4225f0e4be 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -184,7 +184,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd ios && bundle exec pod install --verbose + command: scripts/pod-install.sh - name: Decrypt AdHoc profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AdHoc.mobileprovision NewApp_AdHoc.mobileprovision.gpg diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml deleted file mode 100644 index f65319f14be4..000000000000 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test GitHub Actions workflows - -on: - workflow_dispatch: - workflow_call: - pull_request: - types: [opened, reopened, synchronize] - branches-ignore: [staging, production] - paths: ['.github/**'] - -jobs: - testGHWorkflows: - if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} - runs-on: ubuntu-latest - env: - CI: true - name: test GitHub Workflows - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Node - uses: Expensify/App/.github/actions/composite/setupNode@main - - - name: Setup Homebrew - uses: Homebrew/actions/setup-homebrew@master - - - name: Login to GitHub Container Regstry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: OSBotify - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Act - run: brew install act - - - name: Set ACT_BINARY - run: echo "ACT_BINARY=$(which act)" >> "$GITHUB_ENV" - - - name: Run tests - run: npm run workflow-test diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 3bfc0ed28d1a..32c9e35315b3 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -34,7 +34,7 @@ jobs: # - git diff is used to see the files that were added on this branch # - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main # - wc counts the words in the result of the intersection - count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) + count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) if [ "$count_new_js" -gt "0" ]; then echo "ERROR: Found new JavaScript files in the project; use TypeScript instead." exit 1 diff --git a/.gitignore b/.gitignore index aa6aad4cc429..e33ec43a01fe 100644 --- a/.gitignore +++ b/.gitignore @@ -110,10 +110,6 @@ tsconfig.tsbuildinfo # Mock-github /repo/ -# Workflow test logs -/workflow_tests/logs/ -/workflow_tests/repo/ - # Yalc .yalc yalc.lock diff --git a/.nvmrc b/.nvmrc index 48b14e6b2b56..b8e593f5210c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.14.0 +20.15.1 diff --git a/android/app/build.gradle b/android/app/build.gradle index 04c5562b089b..dae7612c9893 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -108,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009001604 - versionName "9.0.16-4" + versionCode 1009001810 + versionName "9.0.18-10" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/cards-and-domains.svg b/assets/images/cards-and-domains.svg deleted file mode 100644 index a6a3918f6423..000000000000 --- a/assets/images/cards-and-domains.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/simple-illustrations/advanced-approvals-icon-square.svg b/assets/images/simple-illustrations/advanced-approvals-icon-square.svg new file mode 100644 index 000000000000..00f3de51bd42 --- /dev/null +++ b/assets/images/simple-illustrations/advanced-approvals-icon-square.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/emptystate__big-vault.svg b/assets/images/simple-illustrations/emptystate__big-vault.svg new file mode 100644 index 000000000000..02606e39fafd --- /dev/null +++ b/assets/images/simple-illustrations/emptystate__big-vault.svg @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/user-check.svg b/assets/images/user-check.svg new file mode 100644 index 000000000000..2da67de751f4 --- /dev/null +++ b/assets/images/user-check.svg @@ -0,0 +1,9 @@ + + + + diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 0ddaafda2d82..61baec9d9f1c 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -57,7 +57,7 @@ The 168 hours (aka 7 days) will be measured by calculating the time between when A job could be fixing a bug or working on a new feature. There are two ways you can find a job that you can contribute to: #### Finding a job that Expensify posted -This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. +This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/nx/search/jobs/?nbs=1&q=expensify%20react%20native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. >**Note:** Our problem solving approach at Expensify is to focus on high value problems and avoid small optimizations with results that are difficult to measure. We also prefer to identify and solve problems at their root. Given that, please ensure all proposed jobs fix a specific problem in a measurable way with evidence so they are easy to evaluate. Here's an example of a good problem/solution: > diff --git a/contributingGuides/REACT_COMPILER.md b/contributingGuides/REACT_COMPILER.md new file mode 100644 index 000000000000..fd790bce0f8e --- /dev/null +++ b/contributingGuides/REACT_COMPILER.md @@ -0,0 +1,48 @@ +# React Compiler + +## What is the React Compiler? + +[React Compiler](https://react.dev/learn/react-compiler) is a tool designed to enhance the performance of React applications by automatically memoizing components that lack optimizations. + +At Expensify, we are early adopters of this tool and aim to fully leverage its capabilities. + +## React Compiler CI check + +We have implemented a CI check that runs the React Compiler on all pull requests (PRs). This check compares compilable files from the PR branch with those in the target branch. If it detects that a file was previously compiled successfully but now fails to compile, the check will fail. + +## What if CI check fails in my PR? + +If the CI check fails for your PR, you need to fix the problem. If you're unsure how to resolve it, you can ask for help in the `#expensify-open-source` Slack channel (and tag `@Kiryl Ziusko`). + +## How can I check what exactly prevents file from successful optimization or whether my fix for passing `react-compiler` actually works? + +You can run `npm run react-compiler-healthcheck` and examine the output. This command will list the files that failed to compile and provide details on what caused the failures. The output can be extensive, so you may want to write it to a file for easier review: + +```bash +npm run react-compiler-healthcheck &> output.txt +``` + +## How to fix a particular problem? + +Below are the most common failures and approaches to fix them: + +### New `ref` produces `Mutating a value returned from a function whose return value should not be mutated` + +If you encounter this error, you need to add the `Ref` postfix to the variable name. For example: + +```diff +-const rerender = useRef(); ++const rerenderRef = useRef() +``` + +### New `SharedValue` produces `Mutating a value returned from a function whose return value should not be mutated` + +If you added a modification to `SharedValue`, you'll likely encounter this error. You can ignore this error for now because the current `react-native-reanimated` API is not compatible with `react-compiler` rules. Once [this PR](https://github.com/software-mansion/react-native-reanimated/pull/6312) is merged, we'll rewrite the code to be compatible with `react-compiler`. Until then, you can ignore this error. + +### `manual memoization could not be preserved` + +This error usually occurs when a dependency used inside a hook is omitted. This omission creates a memoization that is too complex to optimize automatically. Try including the missing dependencies. + +## What if my type of error is not listed here? + +This list is actively maintained. If you discover a new error that is not listed and find a way to fix it, please update this documentation and create a PR. diff --git a/docs/_includes/section.html b/docs/_includes/section.html index 6bb24adbb496..b6def157e954 100644 --- a/docs/_includes/section.html +++ b/docs/_includes/section.html @@ -15,7 +15,7 @@

- {% assign sortedArticles = section.articles %} + {% assign sortedArticles = section.articles | sort: 'order', 'last' | default: 999 %} {% for article in sortedArticles %} {% assign article_href = section.href | append: '/' | append: article.href %} {% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %} diff --git a/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md b/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md index f39ffe1a05ad..18980377c4cb 100644 --- a/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md +++ b/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md @@ -18,10 +18,13 @@ Expensify allows Workspace Admins to create workflows and automations that deter 5. Select an approval mode. - **Submit and Close**: No approval is required. Once a report is submitted, it will be automatically approved and closed. This option may be useful if your expense approvals occur in another system or if the submitter and approver are the same person. - **Submit and Approve**: All reports go to one person that you assign as the approver. Once a report is submitted, it is sent to the approver. This is the default option. - - **Advanced Approval**: This workflow feature is for companies that require more than one person to approve a report before it can be reimbursed. - Advanced Approval is only available on the Control plan. - + - **Advanced Approval**: Allows for more complex workflows, like assigning different [approvers](https://help.expensify.com/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees) for different employees or requiring secondary approvals for expenses that exceed a [set limit](https://help.expensify.com/articles/expensify-classic/reports/Require-review-for-over-limit-expenses). To add to your approval workflow, you can also set up approval rules for specific categories and tags. +### Enforce workflow +If you want to ensure your employees cannot override the workflow you set for them, enable workflow enforcement on your workspace’s Members tab. Admins will still be able to “take control” of reports and override the set workflow. + +Visit our How Complex Approval Workflows Work guide for more details. +
diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md index db050e5be312..787602337bd2 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -1,6 +1,24 @@ --- title: Configure Quickbooks Online -description: Coming soon +description: Coming Soon --- -# Coming soon +# FAQ + +## How do I know if a report is successfully exported to QuickBooks Online? + +When a report exports successfully, a message is posted in the expense’s related chat room: + +![Confirmation message posted in the expense chat room](https://help.expensify.com/assets/images/QBO_help_01.png){:width="100%"} + +## What happens if I manually export a report that has already been exported? + +When an admin manually exports a report, Expensify will notify them if the report has already been exported. Exporting the data again will create a duplicate report in QuickBooks Online. + +## What happens to existing approved and reimbursed reports if I enable Auto Sync? + +- If Auto Sync was disabled when your Workspace was linked to QuickBooks Online, enabling it won’t impact existing reports that haven’t been exported. +- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in QuickBooks Online during the next sync. +- If a report has been exported and marked as paid in QuickBooks Online, it will be automatically marked as reimbursed in Expensify during the next sync. + +Reports that have yet to be exported to QuickBooks Online won’t be automatically exported. diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md index 5256459d6f9a..b132a75e9297 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md @@ -1,6 +1,37 @@ --- title: Quickbooks Online Troubleshooting -description: Coming soon +description: A list of common QuickBooks Online errors and how to resolve them --- -# Coming soon +## Report won’t automatically export to QuickBooks Online + +If an error occurs during an automatic export to QuickBooks Online: + +- You’ll receive an email detailing the error. +- The error will appear in the related Workspace Chat, indicated by a red dot next to the report. +- For auto-sync errors, a message will be posted in the related #admins room. The message contains a link to the workspace’s Accounting settings where an explanation for the error appears next to the connection. + +An error on a report will prevent it from automatically exporting. + +### How to resolve: + +To resolve this, open the expense and make the required changes. Then an admin must manually export the report to QuickBooks Online by clicking on Details > Export: + +![Click the Export button found in the Details tab](https://help.expensify.com/assets/images/QBO_help_02.png){:width="100%"} + +![Select QuickBooks Online in the Export tab](https://help.expensify.com/assets/images/QBO_help_03.png){:width="100%"} + +## Unable to manually export a report + +To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking “Export” will lead to an empty page, as the data is not yet available for export: + +![If the Report is in the Open status, the Not Ready to Export message shows](https://help.expensify.com/assets/images/QBO_help_04.png){:width="100%"} + +### How to resolve: + +To resolve this, open the report and make the required changes: + +1. If the report is in the Open status, please ensure that it is submitted. +2. If the Report is in the Processing status, an admin or approver will need to approve it. + +Once this is done, then an admin must manually export the report to QuickBooks Online. diff --git a/docs/articles/new-expensify/connections/xero/Configure-Xero.md b/docs/articles/new-expensify/connections/xero/Configure-Xero.md index 0c65db1b4fd9..218e81c98707 100644 --- a/docs/articles/new-expensify/connections/xero/Configure-Xero.md +++ b/docs/articles/new-expensify/connections/xero/Configure-Xero.md @@ -3,4 +3,23 @@ title: Configure Xero description: Coming soon --- -# Coming soon +# FAQ + +## How do I know if a report successfully exported to Xero? + +When a report exports successfully, a message is posted in the related Expensify Chat room. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_01.png){:width="100%"} + +## What happens if I manually export a report that has already been exported? + +When an admin manually exports a report, Expensify will warn them if the report has already been exported. If the admin chooses to export it again, it will create a duplicate report in Xero. You will need to delete the duplicate entries from within Xero. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_05.png){:width="100%"} + +## What happens to existing reports that have already been approved and reimbursed if I enable Auto Sync? + +- If Auto Sync was disabled when your Workspace was linked to Xero, enabling it won’t impact existing reports that haven’t been exported. +- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in Xero during the next sync. +- If a report has been exported and marked as paid in Xero, it will be automatically marked as reimbursed in Expensify during the next sync. +- If a report has not yet been exported to Xero, it won’t be automatically exported. diff --git a/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting b/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting new file mode 100644 index 000000000000..0c69493f3935 --- /dev/null +++ b/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting @@ -0,0 +1,37 @@ +--- +title: Xero Troubleshooting +description: A list of common Xero errors and how to resolve them +--- + +## Report won’t automatically export to Xero + +If an error occurs during an automatic export to Xero: + +- You’ll receive an email detailing the error. +- The error will appear in the related Workspace Chat, indicated by a red dot next to the report. +- For auto-sync errors, a message will be posted in the related #admins room. The message contains a link to the workspace’s accounting settings where an explanation for the error appears next to the connection. + +An error on a report will prevent it from automatically exporting. + +## How to resolve + +Open the expense and make the required changes. Then an admin must manually export the report to Xero by clicking the heading at the top of the expense and selecting Export. Then they’ll select Xero. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_02.png){:width="100%"} + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_03.png){:width="100%"} + +## Unable to manually export a report + +To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking Export will lead to a notification that the data is not yet available for export. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_04.png){:width="100%"} + +## How to resolve + +Open the report and make the required changes: + +- If the report is in the Open status, ensure that it is submitted. +- If the report is in the Processing status, an admin or approver will need to approve it. + +Once complete, an admin must manually export the report to Xero by clicking the heading at the top of the expense and selecting Export. Then they’ll select Xero. diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md index df112259edbb..f06c436449eb 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md @@ -10,7 +10,9 @@ To export your expense data to a CSV, 1. Click the **[Search](https://new.expensify.com/search/all?sortBy=date&sortOrder=desc)** tab in the bottom left menu. 2. Select the checkbox to the left of the expenses or reports you wish to export. - 3. Click **# selected** at the top-right and select **Download**. + 3. Click **# selected** at the top-right and select **Download**. + +![Select the expenses to download]({{site.url}}/assets/images/Export-Expenses.png){:width="100%"} The CSV download will save locally to your device with the file naming prefix _“Expensify.”_ This file provides the following data for each expense: - Date diff --git a/docs/articles/new-expensify/expensify-card/Manage-Expensify-Cards.md b/docs/articles/new-expensify/expensify-card/Manage-Expensify-Cards.md new file mode 100644 index 000000000000..d7a1b75e128d --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Manage-Expensify-Cards.md @@ -0,0 +1,22 @@ +--- +title: Manage Expensify Cards +description: How to manage the Expensify Card +--- +
+ +Once your Expensify Cards have been issued, you can monitor them and check your card’s current balance, remaining limit, and earned cash back. + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace that contains the desired Expensify Cards. +4. Click **Expensify Card** in the left menu. Here, you’ll see a list of all of the issued cards. +5. To view a card’s details or adjust the card limit, limit type, name, or deactivate it, click the card row. +6. To adjust the settlement account that resolves the card’s expenses or to change the settlement frequency, click **Settings** in the top right. + +![Click Expensify Card in the left menu to see a list of cards]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png){:width="100%"} + +![Click the card row to view the card details and make settings adjustments]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png){:width="100%"} + +![Click Settings to adjust the settlement account or frequency]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png){:width="100%"} + +
diff --git a/docs/articles/new-expensify/expensify-card/Set-up-the-Expensify-Card.md b/docs/articles/new-expensify/expensify-card/Set-up-the-Expensify-Card.md new file mode 100644 index 000000000000..ad4982cee4b3 --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Set-up-the-Expensify-Card.md @@ -0,0 +1,74 @@ +--- +title: Set up the Expensify Card +description: How to set up the Expensify Card on your workspace +--- +
+ +{% include info.html %} +You must be a Workspace Admin to complete this process. +{% include end-info.html %} + +The Expensify Visa® Commercial Card is available on all USD workspaces. It provides tools to manage employee spending, including: + +- Unlimited virtual cards +- Controlled spending amounts on virtual cards to manage subscriptions +- Tighter controls for managing spend across employees and merchants +- Fixed or monthly spend limits for each card +- Unique naming for each virtual card for simplified expense categorization + +To enable and issue Expensify Cards for your workspace, complete the steps below. + +# Step 1: Enable Expensify Cards + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to enable Expensify Cards for. +4. Click **More features** in the left menu. +5. Under the **Spend** section, enable the Expensify Card toggle. + +![Click the toggle next to Expensify Card]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png){:width="100%"} + +# Step 2: Select a bank account + +Before you can issue your Expensify Cards, you must connect your cards with the bank account that will be used to pay balances on the cards. + +1. Click **Expensify Card** in the left menu. +2. Click **Issue new card**. +3. Select an existing bank account from the list, or follow the steps to add a new one. + +![Click the issue card button]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png){:width="100%"} + +# Step 3: Issue cards + +You can issue an unlimited amount of physical and/or virtual cards to employees. + +1. Click **Issue card** in the top right. +2. Select the employee that you want to issue the card to. +3. Select whether you want to issue a physical or virtual card. +4. Pick a smart, monthly, or fixed limit: + - Smart limits allow spending up to a certain amount before requiring approval. + - Monthly limits allow spending up to a certain amount per month. + - Fixed limits allow spending up to a certain amount before the card is terminated. +5. Enter the limit amount. +6. Add a card name. +7. Click **Issue card** to confirm and issue the card. + +![Click the issue card button in the top right]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png){:width="100%"} + +![Click issue card to confirm and issue the card]({{site.url}}/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png){:width="100%"} + +{% include faq-begin.md %} +**Do you need a specific type of bank account to use the Expensify Card?** + +At this time, the Expensify Card requires a US business bank account opened in the name of a business incorporated in the US. + +**Can I use the Expensify Card across multiple workspaces?** + +You can use the Expensify Card on every workspace you create. However, a settlement account can only be used with the Expensify Card on one workspace. For example, if you’re using the Expensify Card on three workspaces, you need three settlement accounts. + +**Can I issue multiple cards to the same employee?** + +You can issue an unlimited number of both physical and virtual cards to employees. This supports a wide variety of different use cases, ranging from each employee only having one card, to employees having numerous cards for things like individual trips, specific purchases, etc. +{% include faq-end.md %} + +
diff --git a/docs/assets/images/Duty-of-care.png b/docs/assets/images/Duty-of-care.png new file mode 100644 index 000000000000..f09f8254b478 Binary files /dev/null and b/docs/assets/images/Duty-of-care.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png new file mode 100644 index 000000000000..3dcf92d028ab Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png new file mode 100644 index 000000000000..cafb106e897e Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png new file mode 100644 index 000000000000..08b553857110 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_1.png b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png new file mode 100644 index 000000000000..cba73c2ce150 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_2.png b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png new file mode 100644 index 000000000000..e09b8ac5b2b0 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_3.png b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png new file mode 100644 index 000000000000..999e6785ae5f Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png differ diff --git a/docs/assets/images/Export-Expenses.png b/docs/assets/images/Export-Expenses.png new file mode 100644 index 000000000000..37cabda7922e Binary files /dev/null and b/docs/assets/images/Export-Expenses.png differ diff --git a/docs/assets/images/Travel-Analytics.png b/docs/assets/images/Travel-Analytics.png new file mode 100644 index 000000000000..27f696e28bcd Binary files /dev/null and b/docs/assets/images/Travel-Analytics.png differ diff --git a/docs/assets/images/Xero_help_01.png b/docs/assets/images/Xero_help_01.png new file mode 100644 index 000000000000..ce05ea83c925 Binary files /dev/null and b/docs/assets/images/Xero_help_01.png differ diff --git a/docs/assets/images/Xero_help_02.png b/docs/assets/images/Xero_help_02.png new file mode 100644 index 000000000000..c2d556c7aed0 Binary files /dev/null and b/docs/assets/images/Xero_help_02.png differ diff --git a/docs/assets/images/Xero_help_03.png b/docs/assets/images/Xero_help_03.png new file mode 100644 index 000000000000..30616ffd3d64 Binary files /dev/null and b/docs/assets/images/Xero_help_03.png differ diff --git a/docs/assets/images/Xero_help_04.png b/docs/assets/images/Xero_help_04.png new file mode 100644 index 000000000000..d0e950d3968a Binary files /dev/null and b/docs/assets/images/Xero_help_04.png differ diff --git a/docs/assets/images/Xero_help_05.png b/docs/assets/images/Xero_help_05.png new file mode 100644 index 000000000000..be65e9c62960 Binary files /dev/null and b/docs/assets/images/Xero_help_05.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 99a9202fe0ca..baa943a33cce 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.16 + 9.0.18 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.16.4 + 9.0.18.10 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index aa4f7d8dfdc4..1ba7a291dd14 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.16 + 9.0.18 CFBundleSignature ???? CFBundleVersion - 9.0.16.4 + 9.0.18.10 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 8e8f81131d1f..cbe83c1dd34a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.16 + 9.0.18 CFBundleVersion - 9.0.16.4 + 9.0.18.10 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4776096973bc..f5c825e63868 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1871,7 +1871,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.107): + - RNLiveMarkdown (0.1.111): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1889,9 +1889,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.107) + - RNLiveMarkdown/common (= 0.1.111) - Yoga - - RNLiveMarkdown/common (0.1.107): + - RNLiveMarkdown/common (0.1.111): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2614,7 +2614,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: f0c641a0bcf5fdea3ec1bb52a64b30ff88d25c1f + RNLiveMarkdown: cf2707e6050a3548bde4f66bd752d721f91e8ab6 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3 diff --git a/package-lock.json b/package-lock.json index 81a081da835b..05cccebfd823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.16-4", + "version": "9.0.18-10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.16-4", + "version": "9.0.18-10", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.107", + "@expensify/react-native-live-markdown": "^0.1.111", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.61", + "expensify-common": "2.0.64", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -95,14 +95,14 @@ "react-native-google-places-autocomplete": "2.5.6", "react-native-haptic-feedback": "^2.2.0", "react-native-image-picker": "^7.0.3", - "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", + "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#93399c6410de32966eb57085936ef6951398c2c3", "react-native-key-command": "^1.0.8", "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.57", + "react-native-onyx": "2.0.64", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -174,6 +174,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/base-64": "^1.0.2", "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", @@ -201,7 +202,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", + "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", @@ -220,7 +221,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625", + "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -240,7 +241,7 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", - "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625", + "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", @@ -265,7 +266,7 @@ "yaml": "^2.2.1" }, "engines": { - "node": "20.14.0", + "node": "20.15.1", "npm": "10.7.0" } }, @@ -3950,9 +3951,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.107", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.107.tgz", - "integrity": "sha512-0Yhqo1azCu3cTmzv/KkILZX2yPiyFUZNRx+AdMdT18pMxpqTAuBtFV4HM44rlimmpT3vgwQ1F/0C0AfRAk5dZA==", + "version": "0.1.111", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.111.tgz", + "integrity": "sha512-oBRKAGA6Cv+e/D+Z5YduKL7jnD0RJC26SSyUDNMfj11Y3snG0ayi4+XKjVtfbEor9Qb/54WxM8QgEAolxcZ7Xg==", "workspaces": [ "parser", "example", @@ -17344,6 +17345,12 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/base-64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.2.tgz", + "integrity": "sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "dev": true, @@ -20179,9 +20186,9 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-696af53-20240625", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-696af53-20240625.tgz", - "integrity": "sha512-OUDKms8qmcm5bX0D+sJWC1YcKcd7AZ2aJ7eY6gkR+Xr7PDfkXLbqAld4Qs9B0ntjVbUMEtW/PjlQrxDtY4raHg==", + "version": "0.0.0-experimental-334f00b-20240725", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-334f00b-20240725.tgz", + "integrity": "sha512-ktVKfOtJdHqrLib7IriUe00hnrs585He/n8uzs2yJT9pnH2eyrmMG21aRGBJKxt/P5mdizGLxgyFk0HSMrekhA==", "dev": true, "dependencies": { "@babel/generator": "7.2.0", @@ -25259,9 +25266,9 @@ } }, "node_modules/eslint-plugin-react-compiler": { - "version": "0.0.0-experimental-0998c1e-20240625", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-0998c1e-20240625.tgz", - "integrity": "sha512-npq2RomExoQI3jETs4OrifaygyJYgOcX/q74Q9OC7GmffLh5zSJaQpzjs2fi61NMNkJyIvTBD0C6sKTGGcetOw==", + "version": "0.0.0-experimental-9ed098e-20240725", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-9ed098e-20240725.tgz", + "integrity": "sha512-Xv2iD8kU6R4Wdjdh1WhdP8UnSqSV+/XcadxwBCmMr836fQUoXGuw/uVGc01v9opZs9SwKzo+8My6ayVCgAinPA==", "dev": true, "dependencies": { "@babel/core": "^7.24.4", @@ -25935,9 +25942,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.61", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.61.tgz", - "integrity": "sha512-X900glu2M/m2ggF9xlYlrrihNiwYN6cscYi7WmWp1yGzhGe5VFT+w033doJD1I8JLygtkZoV/xVMY4Porexrxw==", + "version": "2.0.64", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.64.tgz", + "integrity": "sha512-+P9+SMPlY799b2l4A3LQ1dle+KvJXcZ01vAFxIDHni4L2Gc1QyddPKLejbwjOrkGqgl3muoR9cwuX/o+QYlYxA==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -36833,16 +36840,18 @@ } }, "node_modules/react-devtools-core": { - "version": "4.27.8", - "license": "MIT", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", + "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -37257,8 +37266,8 @@ }, "node_modules/react-native-image-size": { "version": "1.1.3", - "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", - "license": "MIT" + "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#93399c6410de32966eb57085936ef6951398c2c3", + "integrity": "sha512-hR38DhM3ewEv5VPhyCAbrhgWWlA1Hyys69BdUFkUes2wgiZc2ARVaXoLKuvzYT3g9fNYLwijylaSEs3juDkPKg==" }, "node_modules/react-native-key-command": { "version": "1.0.8", @@ -37336,16 +37345,16 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.57.tgz", - "integrity": "sha512-+/XndOz9kjCvUAYltq6wJbTsPcof+FZz6eFx0cpu/cDEHaYpjNoPWRKhWgWewg5wTYwu7SWl9aYSShRGVUsZWg==", + "version": "2.0.64", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.64.tgz", + "integrity": "sha512-RFYiEQBFw9610iTGLXIZ1nQMWuf8VyVEMqiRMLpao75+VnbD6lzh0z7Uuj1eoKMDkjeXJhsPP3rh2MkLnqruug==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=20.14.0", + "node": ">=20.15.1", "npm": ">=10.7.0" }, "peerDependencies": { diff --git a/package.json b/package.json index c35a97a8b91d..66195a9a41b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.16-4", + "version": "9.0.18-10", "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.", @@ -14,7 +14,7 @@ "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 --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", + "pod-install": "scripts/pod-install.sh", "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", @@ -59,8 +59,6 @@ "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts", "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", - "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", - "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.ts", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", "e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/", "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", @@ -71,7 +69,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.107", + "@expensify/react-native-live-markdown": "^0.1.111", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -113,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.61", + "expensify-common": "2.0.64", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -153,14 +151,14 @@ "react-native-google-places-autocomplete": "2.5.6", "react-native-haptic-feedback": "^2.2.0", "react-native-image-picker": "^7.0.3", - "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", + "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#93399c6410de32966eb57085936ef6951398c2c3", "react-native-key-command": "^1.0.8", "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.57", + "react-native-onyx": "2.0.64", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -232,6 +230,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/base-64": "^1.0.2", "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", @@ -259,7 +258,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", + "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", @@ -278,7 +277,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625", + "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -298,7 +297,7 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", - "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625", + "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", @@ -350,7 +349,7 @@ ] }, "engines": { - "node": "20.14.0", + "node": "20.15.1", "npm": "10.7.0" } } diff --git a/patches/expo-image-manipulator+11.8.0.patch b/patches/expo-image-manipulator+11.8.0.patch new file mode 100644 index 000000000000..672b7c99bdac --- /dev/null +++ b/patches/expo-image-manipulator+11.8.0.patch @@ -0,0 +1,71 @@ +diff --git a/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js b/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js +index 5b77ad6..a3ecdb0 100644 +--- a/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js ++++ b/node_modules/expo-image-manipulator/build/ExpoImageManipulator.web.js +@@ -1,5 +1,13 @@ + import { crop, extent, flip, resize, rotate } from './actions/index.web'; + import { getContext } from './utils/getContext.web'; ++ ++const SAFARI_MOBILE_CANVAS_LIMIT = 4096; ++ ++const isMobileIOS = () => { ++ const userAgent = navigator.userAgent; ++ return /iP(ad|od|hone)/i.test(userAgent) && /(WebKit|CriOS|FxiOS|OPiOS|mercury)/i.test(userAgent); ++}; ++ + function getResults(canvas, options) { + let uri; + if (options) { +@@ -21,16 +29,49 @@ function getResults(canvas, options) { + base64: uri.replace(/^data:image\/\w+;base64,/, ''), + }; + } ++ ++function getAdjustedCanvasSize(originalWidth, originalHeight) { ++ if(!isMobileIOS()) return { width: originalWidth, height: originalHeight }; ++ ++ const aspectRatio = originalWidth / originalHeight; ++ let newWidth; ++ let newHeight; ++ ++ if (originalWidth <= SAFARI_MOBILE_CANVAS_LIMIT && originalHeight <= SAFARI_MOBILE_CANVAS_LIMIT) { ++ return { width: originalWidth, height: originalHeight }; ++ } ++ ++ if (aspectRatio > 1) { ++ newWidth = SAFARI_MOBILE_CANVAS_LIMIT; ++ newHeight = Math.round(newWidth / aspectRatio); ++ } else { ++ newHeight = SAFARI_MOBILE_CANVAS_LIMIT; ++ newWidth = Math.round(newHeight * aspectRatio); ++ } ++ ++ if (newWidth > SAFARI_MOBILE_CANVAS_LIMIT) { ++ newWidth = SAFARI_MOBILE_CANVAS_LIMIT; ++ newHeight = Math.round(newWidth / aspectRatio); ++ } else if (newHeight > SAFARI_MOBILE_CANVAS_LIMIT) { ++ newHeight = SAFARI_MOBILE_CANVAS_LIMIT; ++ newWidth = Math.round(newHeight * aspectRatio); ++ } ++ ++ return { width: newWidth, height: newHeight }; ++} ++ + function loadImageAsync(uri) { + return new Promise((resolve, reject) => { + const imageSource = new Image(); + imageSource.crossOrigin = 'anonymous'; + const canvas = document.createElement('canvas'); + imageSource.onload = () => { +- canvas.width = imageSource.naturalWidth; +- canvas.height = imageSource.naturalHeight; ++ const adjudstedCanvasSize = getAdjustedCanvasSize(imageSource.naturalWidth, imageSource.naturalHeight); ++ ++ canvas.width = adjudstedCanvasSize.width; ++ canvas.height = adjudstedCanvasSize.height; + const context = getContext(canvas); +- context.drawImage(imageSource, 0, 0, imageSource.naturalWidth, imageSource.naturalHeight); ++ context.drawImage(imageSource, 0, 0, adjudstedCanvasSize.width, adjudstedCanvasSize.height); + resolve(canvas); + }; + imageSource.onerror = () => reject(canvas); diff --git a/patches/react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch b/patches/react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch similarity index 100% rename from patches/react-native+0.73.4+023+iOS-fix-adjustFontSizeToFit-new-architecture.patch rename to patches/react-native+0.73.4+022+iOS-fix-adjustFontSizeToFit-new-architecture.patch diff --git a/patches/react-native+0.73.4+022+textInputClear.patch b/patches/react-native+0.73.4+022+textInputClear.patch deleted file mode 100644 index 1cadce6a0783..000000000000 --- a/patches/react-native+0.73.4+022+textInputClear.patch +++ /dev/null @@ -1,66 +0,0 @@ -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -index 7ce04da..123968f 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -@@ -452,6 +452,12 @@ - (void)blur - [_backedTextInputView resignFirstResponder]; - } - -+- (void)clear -+{ -+ [self setTextAndSelection:_mostRecentEventCount value:@"" start:0 end:0]; -+ _mostRecentEventCount++; -+} -+ - - (void)setTextAndSelection:(NSInteger)eventCount - value:(NSString *__nullable)value - start:(NSInteger)start -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -index fe3376a..6a9a45f 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h -@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN - @protocol RCTTextInputViewProtocol - - (void)focus; - - (void)blur; -+- (void)clear; - - (void)setTextAndSelection:(NSInteger)eventCount - value:(NSString *__nullable)value - start:(NSInteger)start -@@ -49,6 +50,19 @@ RCTTextInputHandleCommand(id componentView, const NSSt - return; - } - -+ if ([commandName isEqualToString:@"clear"]) { -+#if RCT_DEBUG -+ if ([args count] != 0) { -+ RCTLogError( -+ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0); -+ return; -+ } -+#endif -+ -+ [componentView clear]; -+ return; -+ } -+ - if ([commandName isEqualToString:@"setTextAndSelection"]) { - #if RCT_DEBUG - if ([args count] != 4) { -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 8496a7d..e6bcfc4 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -331,6 +331,12 @@ public class ReactTextInputManager extends BaseViewManager) => void) + | undefined; + ++ /** ++ * Callback that is called when the text input was cleared using the native clear command. ++ */ ++ onClear?: ++ | ((e: NativeSyntheticEvent) => void) ++ | undefined; ++ + /** + * Callback that is called when the text input's text changes. + */ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 481938f..346acaa 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -1329,6 +1329,11 @@ function InternalTextInput(props: Props): React.Node { + }); + }; + ++ const _onClear = (event: ChangeEvent) => { ++ setMostRecentEventCount(event.nativeEvent.eventCount); ++ props.onClear && props.onClear(event); ++ }; ++ + const _onFocus = (event: FocusEvent) => { + TextInputState.focusInput(inputRef.current); + if (props.onFocus) { +@@ -1462,6 +1467,7 @@ function InternalTextInput(props: Props): React.Node { + nativeID={id ?? props.nativeID} + onBlur={_onBlur} + onKeyPressSync={props.unstable_onKeyPressSync} ++ onClear={_onClear} + onChange={_onChange} + onChangeSync={useOnChangeSync === true ? _onChangeSync : null} + onContentSizeChange={props.onContentSizeChange} +@@ -1516,6 +1522,7 @@ function InternalTextInput(props: Props): React.Node { + nativeID={id ?? props.nativeID} + numberOfLines={props.rows ?? props.numberOfLines} + onBlur={_onBlur} ++ onClear={_onClear} + onChange={_onChange} + onFocus={_onFocus} + /* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index a19b555..4785987 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -62,6 +62,7 @@ @implementation RCTBaseTextInputViewManager { + + RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock) ++RCT_EXPORT_VIEW_PROPERTY(onClear, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index 7ce04da..70754bf 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -452,6 +452,19 @@ - (void)blur + [_backedTextInputView resignFirstResponder]; + } + ++- (void)clear ++{ ++ auto metrics = [self _textInputMetrics]; ++ [self setTextAndSelection:_mostRecentEventCount value:@"" start:0 end:0]; ++ ++ _mostRecentEventCount++; ++ metrics.eventCount = _mostRecentEventCount; ++ ++ // Notify JS that the event counter has changed ++ const auto &textInputEventEmitter = static_cast(*_eventEmitter); ++ textInputEventEmitter.onClear(metrics); ++} ++ + - (void)setTextAndSelection:(NSInteger)eventCount + value:(NSString *__nullable)value + start:(NSInteger)start +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h +index fe3376a..6889eed 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h +@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN + @protocol RCTTextInputViewProtocol + - (void)focus; + - (void)blur; ++- (void)clear; + - (void)setTextAndSelection:(NSInteger)eventCount + value:(NSString *__nullable)value + start:(NSInteger)start +@@ -49,6 +50,19 @@ RCTTextInputHandleCommand(id componentView, const NSSt + return; + } + ++ if ([commandName isEqualToString:@"clear"]) { ++#if RCT_DEBUG ++ if ([args count] != 0) { ++ RCTLogError( ++ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0); ++ return; ++ } ++#endif ++ ++ [componentView clear]; ++ return; ++ } ++ + if ([commandName isEqualToString:@"setTextAndSelection"]) { + #if RCT_DEBUG + if ([args count] != 4) { +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java +new file mode 100644 +index 0000000..0c142a0 +--- /dev/null ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextClearEvent.java +@@ -0,0 +1,53 @@ ++/* ++ * Copyright (c) Meta Platforms, Inc. and affiliates. ++ * ++ * This source code is licensed under the MIT license found in the ++ * LICENSE file in the root directory of this source tree. ++ */ ++ ++package com.facebook.react.views.textinput; ++ ++import androidx.annotation.Nullable; ++ ++import com.facebook.react.bridge.Arguments; ++import com.facebook.react.bridge.WritableMap; ++import com.facebook.react.uimanager.common.ViewUtil; ++import com.facebook.react.uimanager.events.Event; ++ ++/** ++ * Event emitted by EditText native view when text changes. VisibleForTesting from {@link ++ * TextInputEventsTestCase}. ++ */ ++public class ReactTextClearEvent extends Event { ++ ++ public static final String EVENT_NAME = "topClear"; ++ ++ private String mText; ++ private int mEventCount; ++ ++ @Deprecated ++ public ReactTextClearEvent(int viewId, String text, int eventCount) { ++ this(ViewUtil.NO_SURFACE_ID, viewId, text, eventCount); ++ } ++ ++ public ReactTextClearEvent(int surfaceId, int viewId, String text, int eventCount) { ++ super(surfaceId, viewId); ++ mText = text; ++ mEventCount = eventCount; ++ } ++ ++ @Override ++ public String getEventName() { ++ return EVENT_NAME; ++ } ++ ++ @Nullable ++ @Override ++ protected WritableMap getEventData() { ++ WritableMap eventData = Arguments.createMap(); ++ eventData.putString("text", mText); ++ eventData.putInt("eventCount", mEventCount); ++ eventData.putInt("target", getViewTag()); ++ return eventData; ++ } ++} +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +index 8496a7d..53e5c49 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +@@ -8,6 +8,7 @@ + package com.facebook.react.views.textinput; + + import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; ++import static com.facebook.react.uimanager.UIManagerHelper.getSurfaceId; + + import android.content.Context; + import android.content.res.ColorStateList; +@@ -273,6 +274,9 @@ public class ReactTextInputManager extends BaseViewManager /dev/null && pwd)")" +cd "$ROOT_DIR" || exit 1 + +# Cleanup and exit +# param - status code +function cleanupAndExit { + cd "$START_DIR" || exit 1 + exit "$1" +} + +source scripts/shellUtils.sh + +# Check if bundle is installed +if ! bundle --version > /dev/null 2>&1; then + error 'bundle is not installed. Please install bundle and try again' + cleanupAndExit 1 +fi + +# Check if jq is installed +if ! jq --version > /dev/null 2>&1; then + error 'jq is not installed. Please install jq and try again' + cleanupAndExit 1 +fi + +# Check if yq is installed +if ! yq --version > /dev/null 2>&1; then + error 'yq is not installed. Please install yq and try again' + cleanupAndExit 1 +fi + +CACHED_PODSPEC_DIR='ios/Pods/Local Podspecs' +if [ -d "$CACHED_PODSPEC_DIR" ]; then + info "Verifying pods from Podfile.lock match local podspecs..." + + # Convert podfile.lock to json since yq is missing some features of jq (namely, if/else) + PODFILE_LOCK_AS_JSON="$(yq -o=json ios/Podfile.lock)" + + # Retrieve a list of pods and their versions from Podfile.lock + declare PODS_FROM_LOCKFILE + if ! read_lines_into_array PODS_FROM_LOCKFILE < <(jq -r '.PODS | map (if (.|type) == "object" then keys[0] else . end) | .[]' < <(echo "$PODFILE_LOCK_AS_JSON")); then + error "Error: Could not parse pod versions from Podfile.lock" + cleanupAndExit 1 + fi + + for CACHED_PODSPEC_PATH in "$CACHED_PODSPEC_DIR"/*; do + if [ -f "$CACHED_PODSPEC_PATH" ]; then + # The next two lines use bash parameter expansion to get just the pod name from the path + # i.e: `ios/Pods/Local Podspecs/hermes-engine.podspec.json` to just `hermes-engine` + # It extracts the part of the string between the last `/` and the first `.` + CACHED_POD_NAME="${CACHED_PODSPEC_PATH##*/}" + CACHED_POD_NAME="${CACHED_POD_NAME%%.*}" + + info "🫛 Verifying local pod $CACHED_POD_NAME" + CACHED_POD_VERSION="$(jq -r '.version' < <(cat "$CACHED_PODSPEC_PATH"))" + for POD_FROM_LOCKFILE in "${PODS_FROM_LOCKFILE[@]}"; do + # Extract the pod name and version that was parsed from the lockfile. POD_FROM_LOCKFILE looks like `PodName (version)` + IFS=' ' read -r POD_NAME_FROM_LOCKFILE POD_VERSION_FROM_LOCKFILE <<< "$POD_FROM_LOCKFILE" + if [[ "$CACHED_POD_NAME" == "$POD_NAME_FROM_LOCKFILE" ]]; then + if [[ "$POD_VERSION_FROM_LOCKFILE" != "($CACHED_POD_VERSION)" ]]; then + clear_last_line + info "⚠️ found mismatched pod: $CACHED_POD_NAME, removing local podspec $CACHED_PODSPEC_PATH" + rm "$CACHED_PODSPEC_PATH" + echo -e "\n" + fi + break + fi + done + clear_last_line + fi + done +fi + +cd ios || cleanupAndExit 1 +bundle exec pod install + +# Go back to where we started +cleanupAndExit 0 diff --git a/scripts/shellUtils.sh b/scripts/shellUtils.sh index fa44f2ee7d3a..c1ceace09d0a 100644 --- a/scripts/shellUtils.sh +++ b/scripts/shellUtils.sh @@ -41,6 +41,11 @@ function title { printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET" } +# Function to clear the last printed line +clear_last_line() { + echo -ne "\033[1A\033[K" +} + function assert_equal { if [[ "$1" != "$2" ]]; then error "Assertion failed: $1 is not equal to $2" diff --git a/src/CONST.ts b/src/CONST.ts index e21e3be55efe..893fea5b4386 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -13,6 +13,7 @@ import type {Unit} from './types/onyx/Policy'; type RateAndUnit = { unit: Unit; rate: number; + currency: string; }; type CurrencyDefaultMileageRate = Record; @@ -74,6 +75,12 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + HEIC_SIGNATURES: [ + '6674797068656963', // 'ftypheic' - Indicates standard HEIC file + '6674797068656978', // 'ftypheix' - Indicates a variation of HEIC + '6674797068657631', // 'ftyphevc' - Typically for HEVC encoded media (common in HEIF) + '667479706d696631', // 'ftypmif1' - Multi-Image Format part of HEIF, broader usage + ], RECENT_WAYPOINTS_NUMBER: 20, DEFAULT_DB_NAME: 'OnyxDB', DEFAULT_TABLE_NAME: 'keyvaluepairs', @@ -102,6 +109,7 @@ const CONST = { BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, SCREEN_TRANSITION_END_TIMEOUT: 1000, ARROW_HIDE_DELAY: 3000, + MAX_IMAGE_CANVAS_AREA: 16777216, API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion @@ -143,6 +151,8 @@ const CONST = { LOGO_MAX_SCALE: 1.5, + MAX_IMAGE_DIMENSION: 2400, + BREADCRUMB_TYPE: { ROOT: 'root', STRONG: 'strong', @@ -364,7 +374,6 @@ const CONST = { BETAS: { ALL: 'all', DEFAULT_ROOMS: 'defaultRooms', - VIOLATIONS: 'violations', DUPE_DETECTION: 'dupeDetection', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_ADVANCED_APPROVAL: 'workflowsAdvancedApproval', @@ -974,6 +983,7 @@ const CONST = { SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, + SEARCH_FILTER_OPTIONS: 'search_filter_options', }, PRIORITY_MODE: { GSD: 'gsd', @@ -1119,6 +1129,11 @@ const CONST = { // It's copied here so that the same regex pattern can be used in form validations to be consistent with the server. VALIDATE_FOR_HTML_TAG_REGEX: /<([^>\s]+)(?:[^>]*?)>/g, + // The regex below is used to remove dots only from the local part of the user email (local-part@domain) + // so when we are using search, we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) + // More info https://github.com/Expensify/App/issues/8007 + EMAIL_SEARCH_REGEX: /\.(?=[^\s@]*@)/g, + VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX: /<([\s]+.+[\s]*)>/g, WHITELISTED_TAGS: [/<>/, /< >/, /<->/, /<-->/, /
/, //], @@ -2511,6 +2526,7 @@ const CONST = { CATEGORY: 'category', RECEIPT: 'receipt', DISTANCE: 'distance', + DISTANCE_RATE: 'distanceRate', TAG: 'tag', TAX_RATE: 'taxRate', TAX_AMOUNT: 'taxAmount', @@ -5397,6 +5413,14 @@ const CONST = { description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.description` as const, icon: 'IntacctSquare', }, + approvals: { + id: 'approvals' as const, + alias: 'approvals' as const, + name: 'Advanced Approvals' as const, + title: `workspace.upgrade.approvals.title` as const, + description: `workspace.upgrade.approvals.description` as const, + icon: 'AdvancedApprovalsSquare', + }, glCodes: { id: 'glCodes' as const, alias: 'gl-codes', diff --git a/src/Expensify.tsx b/src/Expensify.tsx index ca9dec6d2279..8ec3fa194cf9 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -300,6 +300,7 @@ function Expensify({ authenticated={isAuthenticated} lastVisitedPath={lastVisitedPath as Route} initialUrl={initialUrl} + shouldShowRequire2FAModal={shouldShowRequire2FAModal} /> )} diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 62ba2f83a69c..7102d6396381 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -205,6 +205,9 @@ const ONYXKEYS = { /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** The NVP containing all information related to educational tooltip in workspace chat */ + NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -536,6 +539,8 @@ const ONYXKEYS = { MONEY_REQUEST_DATE_FORM_DRAFT: 'moneyRequestCreatedFormDraft', MONEY_REQUEST_HOLD_FORM: 'moneyHoldReasonForm', MONEY_REQUEST_HOLD_FORM_DRAFT: 'moneyHoldReasonFormDraft', + MONEY_REQUEST_COMPANY_INFO_FORM: 'moneyRequestCompanyInfoForm', + MONEY_REQUEST_COMPANY_INFO_FORM_DRAFT: 'moneyRequestCompanyInfoFormDraft', NEW_CONTACT_METHOD_FORM: 'newContactMethodForm', NEW_CONTACT_METHOD_FORM_DRAFT: 'newContactMethodFormDraft', WAYPOINT_FORM: 'waypointForm', @@ -608,6 +613,8 @@ const ONYXKEYS = { SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft', SEARCH_ADVANCED_FILTERS_FORM: 'searchAdvancedFiltersForm', SEARCH_ADVANCED_FILTERS_FORM_DRAFT: 'searchAdvancedFiltersFormDraft', + TEXT_PICKER_MODAL_FORM: 'textPickerModalForm', + TEXT_PICKER_MODAL_FORM_DRAFT: 'textPickerModalFormDraft', }, } as const; @@ -643,6 +650,7 @@ type OnyxFormValuesMapping = { [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.MONEY_REQUEST_COMPANY_INFO_FORM]: FormTypes.MoneyRequestCompanyInfoForm; [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.NewContactMethodForm; [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.WaypointForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm; @@ -681,6 +689,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FORM_ID_FORM]: FormTypes.NetSuiteCustomFormIDForm; [ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm; [ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM]: FormTypes.SearchAdvancedFiltersForm; + [ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM]: FormTypes.TextPickerModalForm; }; type OnyxFormDraftValuesMapping = { @@ -867,6 +876,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; + [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflow; }; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f08227b71519..27f565929c56 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -47,6 +47,8 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_STATUS: 'search/filters/status', + SEARCH_ADVANCED_FILTERS_CURRENCY: 'search/filters/currency', + SEARCH_ADVANCED_FILTERS_MERCHANT: 'search/filters/merchant', SEARCH_ADVANCED_FILTERS_DESCRIPTION: 'search/filters/description', @@ -54,6 +56,8 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_REPORT_ID: 'search/filters/reportID', SEARCH_ADVANCED_FILTERS_CATEGORY: 'search/filters/category', + SEARCH_ADVANCED_FILTERS_KEYWORD: 'search/filters/keyword', + SEARCH_ADVANCED_FILTERS_CARD: 'search/filters/card', SEARCH_REPORT: { route: 'search/view/:reportID', @@ -254,7 +258,12 @@ const ROUTES = { }, REPORT_AVATAR: { route: 'r/:reportID/avatar', - getRoute: (reportID: string) => `r/${reportID}/avatar` as const, + getRoute: (reportID: string, policyID?: string) => { + if (policyID) { + return `r/${reportID}/avatar?policyID=${policyID}` as const; + } + return `r/${reportID}/avatar` as const; + }, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', @@ -363,6 +372,11 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType as string}/from/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_COMPANY_INFO: { + route: 'create/:iouType/company-info/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType as string}/company-info/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, participantsAutoAssigned?: boolean) => @@ -706,7 +720,7 @@ const ROUTES = { }, WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account', - getRoute: (policyID: string, connection: ValueOf) => + getRoute: (policyID: string, connection?: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const, }, WORKSPACE_CATEGORIES: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 40a1ebd817fa..d125c9658bf2 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -35,10 +35,13 @@ const SCREENS = { ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP', ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP', ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP', + ADVANCED_FILTERS_CURRENCY_RHP: 'Search_Advanced_Filters_Currency_RHP', ADVANCED_FILTERS_DESCRIPTION_RHP: 'Search_Advanced_Filters_Description_RHP', ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP', ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP', ADVANCED_FILTERS_CATEGORY_RHP: 'Search_Advanced_Filters_Category_RHP', + ADVANCED_FILTERS_KEYWORD_RHP: 'Search_Advanced_Filters_Keyword_RHP', + ADVANCED_FILTERS_CARD_RHP: 'Search_Advanced_Filters_Card_RHP', TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, @@ -190,6 +193,7 @@ const SCREENS = { STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', STEP_SPLIT_PAYER: 'Money_Request_Step_Split_Payer', STEP_SEND_FROM: 'Money_Request_Step_Send_From', + STEP_COMPANY_INFO: 'Money_Request_Step_Company_Info', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index bd437cbb062d..5621c031f959 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -1,5 +1,5 @@ import type {RefObject} from 'react'; -import React, {useEffect} from 'react'; +import React, {useEffect, useState} from 'react'; import type {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -13,6 +13,7 @@ import type {Report, Session} from '@src/types/onyx'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import * as Expensicons from './Icon/Expensicons'; import type {PaymentMethod} from './KYCWall/types'; +import type BaseModalProps from './Modal/types'; import PopoverMenu from './PopoverMenu'; type AddPaymentMethodMenuOnyxProps = { @@ -61,6 +62,7 @@ function AddPaymentMethodMenu({ shouldShowPersonalBankAccountOption = false, }: AddPaymentMethodMenuProps) { const {translate} = useLocalize(); + const [restoreFocusType, setRestoreFocusType] = useState(); // Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report // which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee. @@ -88,11 +90,17 @@ function AddPaymentMethodMenu({ return ( { + setRestoreFocusType(undefined); + onClose(); + }} anchorPosition={anchorPosition} anchorAlignment={anchorAlignment} anchorRef={anchorRef} - onItemSelected={onClose} + onItemSelected={() => { + setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE); + onClose(); + }} menuItems={[ ...(canUsePersonalBankAccount ? [ @@ -124,6 +132,8 @@ function AddPaymentMethodMenu({ // ], ]} withoutOverlay + shouldEnableNewFocusManagement + restoreFocusType={restoreFocusType} /> ); } diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx new file mode 100644 index 000000000000..899e83c9440b --- /dev/null +++ b/src/components/ApprovalWorkflowSection.tsx @@ -0,0 +1,106 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import MenuItem from './MenuItem'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; +import Text from './Text'; + +type ApprovalWorkflowSectionProps = { + /** Single workflow displayed in this component */ + approvalWorkflow: ApprovalWorkflow; + + /** ID of the policy */ + policyId?: string; +}; + +function ApprovalWorkflowSection({approvalWorkflow, policyId}: ApprovalWorkflowSectionProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate, toLocaleOrdinal} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const openApprovalsEdit = useCallback( + () => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EDIT.getRoute(policyId ?? '', approvalWorkflow.approvers[0].email)), + [approvalWorkflow.approvers, policyId], + ); + const approverTitle = useCallback( + (index: number) => + approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`, + [approvalWorkflow.approvers.length, toLocaleOrdinal, translate], + ); + + return ( + + + {approvalWorkflow.isDefault && ( + + + + {translate('workflowsPage.addApprovalTip')} + + + )} + m.displayName).join(', ')} + icon={Expensicons.Users} + iconHeight={20} + iconWidth={20} + iconFill={theme.icon} + onPress={openApprovalsEdit} + shouldRemoveBackground + /> + + {approvalWorkflow.approvers.map((approver, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + + ))} + + + + ); +} + +export default ApprovalWorkflowSection; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 7d4fbd97f4f7..edcdabed9101 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,11 +1,12 @@ import {Str} from 'expensify-common'; +import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Alert, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker'; import {launchImageLibrary} from 'react-native-image-picker'; -import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; +import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker'; import ImageSize from 'react-native-image-size'; import type {FileObject, ImagePickerResponse as FileResponse} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -41,11 +42,12 @@ type Item = { * See https://github.com/react-native-image-picker/react-native-image-picker/#options * for ImagePicker configuration options */ -const imagePickerOptions = { +const imagePickerOptions: Partial = { includeBase64: false, saveToPhotos: false, selectionLimit: 1, includeExtra: false, + assetRepresentationMode: 'current', }; /** @@ -158,12 +160,44 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s return reject(new Error(`Error during attachment selection: ${response.errorMessage}`)); } - return resolve(response.assets); + const targetAsset = response.assets?.[0]; + const targetAssetUri = targetAsset?.uri; + + if (!targetAssetUri) { + return resolve(); + } + + if (targetAsset?.type?.startsWith('image')) { + FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignatures: CONST.HEIC_SIGNATURES}) + .then((isHEIC) => { + // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature + if (isHEIC && targetAssetUri) { + manipulateAsync(targetAssetUri, [], {format: SaveFormat.JPEG}) + .then((manipResult) => { + const uri = manipResult.uri; + const convertedAsset = { + uri, + name: uri.substring(uri.lastIndexOf('/') + 1).split('?')[0], + type: 'image/jpeg', + width: manipResult.width, + height: manipResult.height, + }; + + return resolve([convertedAsset]); + }) + .catch((err) => reject(err)); + } else { + return resolve(response.assets); + } + }) + .catch((err) => reject(err)); + } else { + return resolve(response.assets); + } }); }), [showGeneralAlert, type], ); - /** * Launch the DocumentPicker. Results are in the same format as ImagePicker * diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx index c761faccad39..c2081fa33bd1 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx @@ -8,21 +8,21 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type TransparentOverlayProps = { - resetSuggestions: () => void; + onPress: () => void; }; type OnPressHandler = PressableProps['onPress']; -function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { +function TransparentOverlay({onPress: onPressProp}: TransparentOverlayProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const onResetSuggestions = useCallback>( + const onPress = useCallback>( (event) => { event?.preventDefault(); - resetSuggestions(); + onPressProp(); }, - [resetSuggestions], + [onPressProp], ); const handlePointerDown = useCallback((e: PointerEvent) => { @@ -35,7 +35,7 @@ function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { style={styles.fullScreen} > ({left = 0, width = 0, bottom return ( - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx index d26dd0422368..4d322fe15c4e 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx @@ -39,7 +39,7 @@ function AutoCompleteSuggestionsPortal({ bodyElement && ReactDOM.createPortal( <> - + {componentToRender} , bodyElement, diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 38bf3912ae4b..2ccdd47c3205 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -58,12 +58,16 @@ function AvatarWithDisplayName({ const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const title = ReportUtils.getReportName(report); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); + const [invoiceReceiverPolicy] = useOnyx( + `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, + ); + const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); - const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); + const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 1fa40ad1b6ff..1eeadde84c47 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -5,10 +5,12 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import mergeRefs from '@libs/mergeRefs'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -37,6 +39,7 @@ function ButtonWithDropdownMenu({ onOptionsMenuHide, enterKeyEventListenerPriority = 0, wrapperStyle, + useKeyboardShortcuts = false, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -46,6 +49,7 @@ function ButtonWithDropdownMenu({ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); const dropdownAnchor = useRef(null); + const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, dropdownAnchor); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; @@ -70,6 +74,19 @@ function ButtonWithDropdownMenu({ }); } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, + (e) => { + onPress(e, selectedItem.value); + }, + { + captureOnInputs: true, + shouldBubble: false, + isActive: useKeyboardShortcuts, + }, + ); + return ( {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -77,12 +94,7 @@ function ButtonWithDropdownMenu({
)} - {hasAssignedCard ? ( -
- {}} - /> -
- ) : null} -
- {}} - shouldEnableScroll={false} - style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} - listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} - /> -
diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index f79ed8440139..f3df5ebfb0b5 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -35,7 +35,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CloseAccountForm} from '@src/types/form'; -import type {Account, Credentials} from '@src/types/onyx'; +import type {Account} from '@src/types/onyx'; import htmlDivElementRef from '@src/types/utils/htmlDivElementRef'; import viewRef from '@src/types/utils/viewRef'; import type LoginFormProps from './types'; @@ -47,19 +47,15 @@ type BaseLoginFormOnyxProps = { /** Message to display when user successfully closed their account */ closeAccount: OnyxEntry; - - /** The credentials of the logged in person */ - credentials: OnyxEntry; }; type BaseLoginFormProps = WithToggleVisibilityViewProps & BaseLoginFormOnyxProps & LoginFormProps; -function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false, isVisible}: BaseLoginFormProps, ref: ForwardedRef) { +function BaseLoginForm({account, login, onLoginChanged, closeAccount, blurOnSubmit = false, isVisible}: BaseLoginFormProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); const input = useRef(null); - const [login, setLogin] = useState(() => Str.removeSMSDomain(credentials?.login ?? '')); const [formError, setFormError] = useState(); const prevIsVisible = usePrevious(isVisible); const firstBlurred = useRef(false); @@ -101,7 +97,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false */ const onTextInput = useCallback( (text: string) => { - setLogin(text); + onLoginChanged(text); if (firstBlurred.current) { validate(text); } @@ -115,7 +111,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false CloseAccount.setDefaultData(); } }, - [account, closeAccount, input, setLogin, validate], + [account, closeAccount, input, onLoginChanged, validate], ); function getSignInWithStyles() { @@ -338,7 +334,6 @@ BaseLoginForm.displayName = 'BaseLoginForm'; export default withToggleVisibilityView( withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, - credentials: {key: ONYXKEYS.CREDENTIALS}, closeAccount: {key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM}, })(forwardRef(BaseLoginForm)), ); diff --git a/src/pages/signin/LoginForm/types.ts b/src/pages/signin/LoginForm/types.ts index 775009072a2d..9539b22e36ca 100644 --- a/src/pages/signin/LoginForm/types.ts +++ b/src/pages/signin/LoginForm/types.ts @@ -1,4 +1,10 @@ type LoginFormProps = { + /** The login input value */ + login: string; + + /** A callback to notify that the login input value is changed */ + onLoginChanged: (login: string) => void; + /** Function used to scroll to the top of the page */ scrollPageToTop?: () => void; diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index 024d5a7c5610..567612bb1aa6 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -161,6 +161,8 @@ function SignInPage({credentials, account, activeClients = [], preferredLocale, * if we need to clear their sign in details so they can enter a login */ const [hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin] = useState(false); + const [login, setLogin] = useState(() => Str.removeSMSDomain(credentials?.login ?? '')); + const isClientTheLeader = !!activeClients && ActiveClientManager.isClientTheLeader(); // We need to show "Another login page is opened" message if the page isn't active and visible // eslint-disable-next-line rulesdir/no-negated-variables @@ -283,6 +285,8 @@ function SignInPage({credentials, account, activeClients = [], preferredLocale, diff --git a/src/pages/signin/SignInPageLayout/SignInHeroImage.tsx b/src/pages/signin/SignInPageLayout/SignInHeroImage.tsx index 1aef31bb6448..db3472b21d76 100644 --- a/src/pages/signin/SignInPageLayout/SignInHeroImage.tsx +++ b/src/pages/signin/SignInPageLayout/SignInHeroImage.tsx @@ -1,9 +1,7 @@ import React, {useMemo} from 'react'; -import {View} from 'react-native'; import Lottie from '@components/Lottie'; import LottieAnimations from '@components/LottieAnimations'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSplashScreen from '@hooks/useSplashScreen'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; @@ -24,14 +22,6 @@ function SignInHeroImage() { }; }, [shouldUseNarrowLayout, isMediumScreenWidth]); - const {isSplashHidden} = useSplashScreen(); - // Prevents rendering of the Lottie animation until the splash screen is hidden - // by returning an empty view of the same size as the animation. - // See issue: https://github.com/Expensify/App/issues/34696 - if (!isSplashHidden) { - return ; - } - return ( Navigation.navigate(ROUTES.NEW_TASK_TITLE)} shouldShowRightIcon + rightLabel={translate('common.required')} /> diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 11e430780c53..7404dff38937 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -239,7 +239,7 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro sections={areOptionsInitialized ? sections : []} ListItem={UserListItem} onSelectRow={selectReport} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onChangeText={setSearchValue} textInputValue={searchValue} headerMessage={headerMessage} diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 4367f2ccebcb..da8537227f08 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -138,7 +138,7 @@ function TaskShareDestinationSelectorModal() { ListItem={UserListItem} sections={areOptionsInitialized ? sections : []} onSelectRow={selectReportHandler} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect onChangeText={setSearchValue} textInputValue={searchValue} headerMessage={options.header} diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index cbab4cac34b5..8ab1dadccb30 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -589,7 +589,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson headerMessage={getHeaderMessage()} headerContent={!shouldUseNarrowLayout && getHeaderContent()} onSelectRow={openMemberDetails} - shouldDebounceRowSelect={!isPolicyAdmin} + shouldSingleExecuteRowSelect={!isPolicyAdmin} onCheckboxPress={(item) => toggleUser(item.accountID)} onSelectAll={() => toggleAllUsers(data)} onDismissError={dismissError} diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index e54914fc6817..de9a3e15dd56 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -57,29 +57,6 @@ type SectionObject = { items: Item[]; }; -// TODO: remove when Onyx data is available -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const mockedCardsList = { - test1: { - cardholder: {accountID: 1, lastName: 'Smith', firstName: 'Bob', displayName: 'Bob Smith', avatar: ''}, - name: 'Test 1', - limit: 1000, - lastFourPAN: '1234', - }, - test2: { - cardholder: {accountID: 2, lastName: 'Miller', firstName: 'Alex', displayName: 'Alex Miller', avatar: ''}, - name: 'Test 2', - limit: 2000, - lastFourPAN: '1234', - }, - test3: { - cardholder: {accountID: 3, lastName: 'Brown', firstName: 'Kevin', displayName: 'Kevin Brown', avatar: ''}, - name: 'Test 3', - limit: 3000, - lastFourPAN: '1234', - }, -}; - function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPageProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -91,13 +68,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro !!policy?.connections?.quickbooksOnline?.config?.syncTax || !!policy?.connections?.xero?.config?.importTaxRates || !!policy?.connections?.netsuite?.options?.config?.syncOptions?.syncTax; - const policyID = policy?.id ?? ''; + const policyID = policy?.id; // @ts-expect-error a new props will be added during feed api implementation const workspaceAccountID = (policy?.workspaceAccountID as string) ?? ''; const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}${CONST.EXPENSIFY_CARD.BANK}`); - // Uncomment this line for testing disabled toggle feature - for c+ - // const [cardsList = mockedCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); - const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); const [isIntegrateWarningModalOpen, setIsIntegrateWarningModalOpen] = useState(false); const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); @@ -111,7 +85,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive: policy?.areDistanceRatesEnabled ?? false, pendingAction: policy?.pendingFields?.areDistanceRatesEnabled, action: (isEnabled: boolean) => { - DistanceRate.enablePolicyDistanceRates(policy?.id ?? '-1', isEnabled); + if (!policyID) { + return; + } + DistanceRate.enablePolicyDistanceRates(policyID, isEnabled); }, }, { @@ -121,7 +98,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive: policy?.areWorkflowsEnabled ?? false, pendingAction: policy?.pendingFields?.areWorkflowsEnabled, action: (isEnabled: boolean) => { - Policy.enablePolicyWorkflows(policy?.id ?? '-1', isEnabled); + if (!policyID) { + return; + } + Policy.enablePolicyWorkflows(policyID, isEnabled); }, }, ]; @@ -136,7 +116,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro pendingAction: policy?.pendingFields?.areExpensifyCardsEnabled, disabled: !isEmptyObject(cardsList), action: (isEnabled: boolean) => { - Policy.enableExpensifyCard(policy?.id ?? '-1', isEnabled); + if (!policyID) { + return; + } + Policy.enableExpensifyCard(policyID, isEnabled); }, disabledAction: () => { setIsDisableExpensifyCardWarningModalOpen(true); @@ -144,6 +127,22 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }); } + const earnItems: Item[] = [ + { + icon: Illustrations.InvoiceBlue, + titleTranslationKey: 'workspace.moreFeatures.invoices.title', + subtitleTranslationKey: 'workspace.moreFeatures.invoices.subtitle', + isActive: policy?.areInvoicesEnabled ?? false, + pendingAction: policy?.pendingFields?.areInvoicesEnabled, + action: (isEnabled: boolean) => { + if (!policyID) { + return; + } + Policy.enablePolicyInvoicing(policyID, isEnabled); + }, + }, + ]; + const organizeItems: Item[] = [ { icon: Illustrations.FolderOpen, @@ -153,11 +152,14 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areCategoriesEnabled, action: (isEnabled: boolean) => { + if (!policyID) { + return; + } if (hasAccountingConnection) { setIsOrganizeWarningModalOpen(true); return; } - Category.enablePolicyCategories(policy?.id ?? '-1', isEnabled); + Category.enablePolicyCategories(policyID, isEnabled); }, }, { @@ -168,11 +170,14 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areTagsEnabled, action: (isEnabled: boolean) => { + if (!policyID) { + return; + } if (hasAccountingConnection) { setIsOrganizeWarningModalOpen(true); return; } - Tag.enablePolicyTags(policy?.id ?? '-1', isEnabled); + Tag.enablePolicyTags(policyID, isEnabled); }, }, { @@ -183,11 +188,14 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.tax, action: (isEnabled: boolean) => { + if (!policyID) { + return; + } if (hasAccountingConnection) { setIsOrganizeWarningModalOpen(true); return; } - Policy.enablePolicyTaxes(policy?.id ?? '-1', isEnabled); + Policy.enablePolicyTaxes(policyID, isEnabled); }, }, ]; @@ -201,6 +209,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areReportFieldsEnabled, action: (isEnabled: boolean) => { + if (!policyID) { + return; + } if (hasAccountingConnection) { setIsOrganizeWarningModalOpen(true); return; @@ -229,15 +240,23 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive: isAccountingEnabled, pendingAction: policy?.pendingFields?.areConnectionsEnabled, action: (isEnabled: boolean) => { + if (!policyID) { + return; + } if (hasAccountingConnection) { setIsIntegrateWarningModalOpen(true); return; } - Policy.enablePolicyConnections(policy?.id ?? '-1', isEnabled); + Policy.enablePolicyConnections(policyID, isEnabled); }, disabled: hasAccountingConnection, errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED), - onCloseError: () => Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED), + onCloseError: () => { + if (!policyID) { + return; + } + Policy.clearPolicyErrorField(policyID, CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED); + }, }, ]; @@ -247,6 +266,11 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro subtitleTranslationKey: 'workspace.moreFeatures.spendSection.subtitle', items: spendItems, }, + { + titleTranslationKey: 'workspace.moreFeatures.earnSection.title', + subtitleTranslationKey: 'workspace.moreFeatures.earnSection.subtitle', + items: earnItems, + }, { titleTranslationKey: 'workspace.moreFeatures.organizeSection.title', subtitleTranslationKey: 'workspace.moreFeatures.organizeSection.subtitle', @@ -339,6 +363,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro { + if (!policyID) { + return; + } setIsOrganizeWarningModalOpen(false); Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); }} @@ -351,6 +378,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro { + if (!policyID) { + return; + } setIsIntegrateWarningModalOpen(false); Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); }} @@ -364,6 +394,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro title={translate('workspace.reportFields.disableReportFields')} isVisible={isReportFieldsWarningModalOpen} onConfirm={() => { + if (!policyID) { + return; + } setIsReportFieldsWarningModalOpen(false); Policy.enablePolicyReportFields(policyID, false); }} diff --git a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx index 81f341999200..5856c71d0a43 100644 --- a/src/pages/workspace/WorkspaceResetBankAccountModal.tsx +++ b/src/pages/workspace/WorkspaceResetBankAccountModal.tsx @@ -13,9 +13,6 @@ import type * as OnyxTypes from '@src/types/onyx'; type WorkspaceResetBankAccountModalOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; - - /** The user's data */ - user: OnyxEntry; }; type WorkspaceResetBankAccountModalProps = WorkspaceResetBankAccountModalOnyxProps & { @@ -23,7 +20,7 @@ type WorkspaceResetBankAccountModalProps = WorkspaceResetBankAccountModalOnyxPro reimbursementAccount: OnyxEntry; }; -function WorkspaceResetBankAccountModal({reimbursementAccount, session, user}: WorkspaceResetBankAccountModalProps) { +function WorkspaceResetBankAccountModal({reimbursementAccount, session}: WorkspaceResetBankAccountModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const achData = reimbursementAccount?.achData; @@ -49,7 +46,7 @@ function WorkspaceResetBankAccountModal({reimbursementAccount, session, user}: W } danger onCancel={BankAccounts.cancelResetFreePlanBankAccount} - onConfirm={() => BankAccounts.resetFreePlanBankAccount(bankAccountID, session, achData?.policyID ?? '-1', user)} + onConfirm={() => BankAccounts.resetFreePlanBankAccount(bankAccountID, session, achData?.policyID ?? '-1')} shouldShowCancelButton isVisible /> @@ -62,7 +59,4 @@ export default withOnyx; errorFields?: ErrorFields; }; + function accountingIntegrationData( connectionName: PolicyConnectionName, policyID: string, @@ -213,6 +216,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); const [datetimeToRelative, setDateTimeToRelative] = useState(''); const threeDotsMenuContainerRef = useRef(null); + const {canUseWorkspaceFeeds} = usePermissions(); const lastSyncProgressDate = parseISO(connectionSyncProgress?.timestamp ?? ''); const isSyncInProgress = @@ -245,7 +249,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { { icon: Expensicons.Trashcan, text: translate('workspace.accounting.disconnect'), - onSelected: () => setIsDisconnectModalOpen(true), + onSelected: () => Modal.close(() => setIsDisconnectModalOpen(true)), }, ], [translate, policyID, isOffline, connectedIntegration], @@ -349,6 +353,52 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const shouldHideConfigurationOptions = isConnectionUnverified(policy, connectedIntegration); const integrationData = accountingIntegrationData(connectedIntegration, policyID, translate, undefined, undefined, policy); const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; + + const configurationOptions = [ + { + icon: Expensicons.Pencil, + iconRight: Expensicons.ArrowRight, + shouldShowRightIcon: true, + title: translate('workspace.accounting.import'), + wrapperStyle: [styles.sectionMenuItemTopDescription], + onPress: integrationData?.onImportPagePress, + brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedImportSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + pendingAction: settingsPendingAction(integrationData?.subscribedImportSettings, integrationData?.pendingFields), + }, + { + icon: Expensicons.Send, + iconRight: Expensicons.ArrowRight, + shouldShowRightIcon: true, + title: translate('workspace.accounting.export'), + wrapperStyle: [styles.sectionMenuItemTopDescription], + onPress: integrationData?.onExportPagePress, + brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedExportSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + pendingAction: settingsPendingAction(integrationData?.subscribedExportSettings, integrationData?.pendingFields), + }, + { + icon: Expensicons.ExpensifyCard, + iconRight: Expensicons.ArrowRight, + shouldShowRightIcon: true, + title: translate('workspace.accounting.cardReconciliation'), + wrapperStyle: [styles.sectionMenuItemTopDescription], + onPress: integrationData?.onCardReconciliationPagePress, + }, + { + icon: Expensicons.Gear, + iconRight: Expensicons.ArrowRight, + shouldShowRightIcon: true, + title: translate('workspace.accounting.advanced'), + wrapperStyle: [styles.sectionMenuItemTopDescription], + onPress: integrationData?.onAdvancedPagePress, + brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, integrationData?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + pendingAction: settingsPendingAction(integrationData?.subscribedAdvancedSettings, integrationData?.pendingFields), + }, + ]; + + if (!canUseWorkspaceFeeds || !policy?.areExpensifyCardsEnabled) { + configurationOptions.splice(2, 1); + } + return [ { ...iconProps, @@ -386,55 +436,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { ), }, ...(isEmptyObject(integrationSpecificMenuItems) || shouldShowSynchronizationError || isEmptyObject(policy?.connections) ? [] : [integrationSpecificMenuItems]), - ...(isEmptyObject(policy?.connections) || shouldHideConfigurationOptions - ? [] - : [ - { - icon: Expensicons.Pencil, - iconRight: Expensicons.ArrowRight, - shouldShowRightIcon: true, - title: translate('workspace.accounting.import'), - wrapperStyle: [styles.sectionMenuItemTopDescription], - onPress: integrationData?.onImportPagePress, - brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedImportSettings, integrationData?.errorFields) - ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR - : undefined, - pendingAction: settingsPendingAction(integrationData?.subscribedImportSettings, integrationData?.pendingFields), - }, - { - icon: Expensicons.Send, - iconRight: Expensicons.ArrowRight, - shouldShowRightIcon: true, - title: translate('workspace.accounting.export'), - wrapperStyle: [styles.sectionMenuItemTopDescription], - onPress: integrationData?.onExportPagePress, - brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedExportSettings, integrationData?.errorFields) - ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR - : undefined, - pendingAction: settingsPendingAction(integrationData?.subscribedExportSettings, integrationData?.pendingFields), - }, - { - icon: Expensicons.ExpensifyCard, - iconRight: Expensicons.ArrowRight, - shouldShowRightIcon: true, - title: translate('workspace.accounting.cardReconciliation'), - wrapperStyle: [styles.sectionMenuItemTopDescription], - onPress: integrationData?.onCardReconciliationPagePress, - }, - - { - icon: Expensicons.Gear, - iconRight: Expensicons.ArrowRight, - shouldShowRightIcon: true, - title: translate('workspace.accounting.advanced'), - wrapperStyle: [styles.sectionMenuItemTopDescription], - onPress: integrationData?.onAdvancedPagePress, - brickRoadIndicator: areSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, integrationData?.errorFields) - ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR - : undefined, - pendingAction: settingsPendingAction(integrationData?.subscribedAdvancedSettings, integrationData?.pendingFields), - }, - ]), + ...(isEmptyObject(policy?.connections) || shouldHideConfigurationOptions ? [] : configurationOptions), ]; }, [ policy, @@ -446,6 +448,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { styles.pb0, styles.mt5, styles.popoverMenuIcon, + canUseWorkspaceFeeds, connectionSyncProgress?.stageInProgress, datetimeToRelative, theme.spinner, diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx index ca9a7c61ce12..dea44fe68856 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx @@ -1,7 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Illustrations from '@components/Icon/Illustrations'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -9,6 +8,7 @@ import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -18,7 +18,6 @@ import variables from '@styles/variables'; import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Connections} from '@src/types/onyx/Policy'; @@ -30,7 +29,7 @@ function SageIntacctDefaultVendorPage({route}: SageIntacctDefaultVendorPageProps const {translate} = useLocalize(); const policyID = route.params.policyID ?? '-1'; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const policy = usePolicy(policyID); const {config} = policy?.connections?.intacct ?? {}; const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx index e9965de0d1ae..7bbb25f3d7cb 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx @@ -1,7 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import ConnectionLayout from '@components/ConnectionLayout'; import FormProvider from '@components/Form/FormProvider'; @@ -12,6 +11,7 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearSageIntacctErrorField, editSageIntacctUserDimensions, removeSageIntacctUserDimensions} from '@libs/actions/connections/SageIntacct'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -32,7 +32,7 @@ function SageIntacctEditUserDimensionsPage({route}: SageIntacctEditUserDimension const {translate} = useLocalize(); const editedUserDimensionName: string = route.params.dimensionName; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); + const policy = usePolicy(route.params.policyID); const policyID: string = policy?.id ?? '-1'; const config = policy?.connections?.intacct?.config; const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx index cfa09fa0dc50..736aba970e7b 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx @@ -1,10 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useMemo} from 'react'; -import {useOnyx} from 'react-native-onyx'; import RadioListItem from '@components/SelectionList/RadioListItem'; import SelectionScreen from '@components/SelectionScreen'; import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateSageIntacctMappingValue} from '@libs/actions/connections/SageIntacct'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -13,7 +13,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {settingsPendingAction} from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {SageIntacctMappingName, SageIntacctMappingValue} from '@src/types/onyx/Policy'; @@ -25,7 +24,7 @@ function SageIntacctMappingsTypePage({route}: SageIntacctMappingsTypePageProps) const styles = useThemeStyles(); const mappingName: SageIntacctMappingName = route.params.mapping; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); + const policy = usePolicy(route.params.policyID); const policyID = policy?.id ?? '-1'; const {config} = policy?.connections?.intacct ?? {}; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx index d4f502227144..9d74fa5cee97 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx @@ -1,12 +1,12 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; import React, {useState} from 'react'; -import {useOnyx} from 'react-native-onyx'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearSageIntacctErrorField, updateSageIntacctMappingValue} from '@libs/actions/connections/SageIntacct'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -16,7 +16,6 @@ import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {SageIntacctMappingName, SageIntacctMappingValue} from '@src/types/onyx/Policy'; @@ -49,7 +48,7 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro const {translate} = useLocalize(); const styles = useThemeStyles(); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); + const policy = usePolicy(route.params.policyID); const mappingName: SageIntacctMappingName = route.params.mapping; const policyID: string = policy?.id ?? '-1'; diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx index 3eeebbafc8a3..e8f0d9e8315f 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx @@ -98,7 +98,7 @@ function NetSuiteCustomListSelectorModal({isVisible, currentCustomListValue, onC ListItem={RadioListItem} isRowMultilineSupported initiallyFocusedOptionKey={currentCustomListValue} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect shouldStopPropagation shouldUseDynamicMaxToRenderPerBatch /> diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx index a02c0b76809f..bed84acfb7ce 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx @@ -92,7 +92,7 @@ function QuickbooksAccountSelectPage({policy}: WithPolicyConnectionsProps) { ListItem={RadioListItem} headerContent={listHeaderComponent} onSelectRow={saveSelection} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx index 0459f61b88d6..69acda4e1ba6 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx @@ -93,7 +93,7 @@ function QuickbooksInvoiceAccountSelectPage({policy}: WithPolicyConnectionsProps ListItem={RadioListItem} headerContent={listHeaderComponent} onSelectRow={updateAccount} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx index b1af64cb2f4b..c36f7df6b245 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx @@ -100,7 +100,7 @@ function QuickbooksCompanyCardExpenseAccountSelectCardPage({policy}: WithPolicyC sections={sections} ListItem={RadioListItem} onSelectRow={selectExportCompanyCard} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={sections[0].data.find((option) => option.isSelected)?.keyForList} footerContent={ isLocationEnabled && {translate('workspace.qbo.companyCardsLocationEnabledDescription')} diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx index 3c44888d782d..8bee7d206180 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx @@ -97,7 +97,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx index 64e55edeb862..89fbd6a96b33 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx @@ -58,7 +58,7 @@ function QuickbooksExportDateSelectPage({policy}: WithPolicyConnectionsProps) { sections={[{data}]} ListItem={RadioListItem} onSelectRow={selectExportDate} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx index b95e70fe11dd..e2b285fe3d41 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx @@ -78,7 +78,7 @@ function QuickbooksExportInvoiceAccountSelectPage({policy}: WithPolicyConnection sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportInvoice} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx index 3c9e7c085578..d57da414b57b 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx @@ -75,7 +75,7 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo sections={sections} ListItem={RadioListItem} onSelectRow={selectVendor} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={sections[0]?.data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx index 50b44640642b..bc4523a034f5 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx @@ -116,7 +116,7 @@ function QuickbooksOutOfPocketExpenseAccountSelectPage({policy}: WithPolicyConne sections={data.length ? [{data}] : []} ListItem={RadioListItem} onSelectRow={selectExportAccount} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} listEmptyContent={listEmptyContent} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx index 4843c192991f..b0d8afa6d53b 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx @@ -120,7 +120,7 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec sections={sections} ListItem={RadioListItem} onSelectRow={selectExportEntity} - shouldDebounceRowSelect + shouldSingleExecuteRowSelect initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} footerContent={