diff --git a/.eslintrc.js b/.eslintrc.js index ac352db40431..2f9a4167ae51 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,6 +31,11 @@ module.exports = { importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'], message: 'Please use PressableWithFeedback and/or PressableWithoutFeedback from src/components/Pressable instead', }, + { + name: 'react-native', + importNames: ['StatusBar'], + message: 'Please use StatusBar from src/libs/StatusBar instead', + }, ], }, ], diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index 643c707da230..6bdf500912c0 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -8,10 +8,34 @@ runs: with: node-version-file: '.nvmrc' cache: npm + cache-dependency-path: | + package-lock.json + desktop/package-lock.json - - name: Install node packages - uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 + - id: cache-node-modules + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }} + + - id: cache-desktop-node-modules + uses: actions/cache@v3 + with: + path: desktop/node_modules + key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json') }} + + - name: Install root project node packages + if: steps.cache-node-modules.outputs.cache-hit != 'true' + uses: nick-fields/retry@v2 with: timeout_minutes: 30 max_attempts: 3 command: npm ci + + - name: Install node packages for desktop submodule + if: steps.cache-desktop-node-modules.outputs.cache-hit != 'true' + uses: nick-fields/retry@v2 + with: + timeout_minutes: 30 + max_attempts: 3 + command: cd desktop && npm ci diff --git a/.github/actions/javascript/validateReassureOutput/action.yml b/.github/actions/javascript/validateReassureOutput/action.yml new file mode 100644 index 000000000000..1b4488757e9c --- /dev/null +++ b/.github/actions/javascript/validateReassureOutput/action.yml @@ -0,0 +1,15 @@ +name: 'Validate Regression Test Output' +description: 'Validates the output of regression tests and determines if a test action should fail.' +inputs: + DURATION_DEVIATION_PERCENTAGE: + description: Allowable percentage deviation for the mean duration in regression test results. + required: true + COUNT_DEVIATION: + description: Allowable deviation for the mean count in regression test results. + required: true + REGRESSION_OUTPUT: + description: Refers to the results obtained from regression tests `.reassure/output.json`. + required: true +runs: + using: 'node16' + main: './index.js' diff --git a/.github/actions/javascript/validateReassureOutput/index.js b/.github/actions/javascript/validateReassureOutput/index.js new file mode 100644 index 000000000000..052f4529b0d5 --- /dev/null +++ b/.github/actions/javascript/validateReassureOutput/index.js @@ -0,0 +1,2626 @@ +/** + * NOTE: This is a compiled file. DO NOT directly edit this file. + */ +module.exports = +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 688: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const core = __nccwpck_require__(186); + +const run = () => { + const regressionOutput = JSON.parse(core.getInput('REGRESSION_OUTPUT', {required: true})); + const countDeviation = core.getInput('COUNT_DEVIATION', {required: true}); + const durationDeviation = core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true}); + + if (regressionOutput.countChanged === undefined || regressionOutput.countChanged.length === 0) { + console.log('No countChanged data available. Exiting...'); + return true; + } + + console.log(`Processing ${regressionOutput.countChanged.length} measurements...`); + + for (let i = 0; i < regressionOutput.countChanged.length; i++) { + const measurement = regressionOutput.countChanged[i]; + const baseline = measurement.baseline; + const current = measurement.current; + + console.log(`Processing measurement ${i + 1}: ${measurement.name}`); + + const renderCountDiff = current.meanCount - baseline.meanCount; + if (renderCountDiff > countDeviation) { + core.setFailed(`Render count difference exceeded the allowed deviation of ${countDeviation}. Current difference: ${renderCountDiff}`); + break; + } else { + console.log(`Render count difference ${renderCountDiff} is within the allowed deviation range of ${countDeviation}.`); + } + + const increasePercentage = ((current.meanDuration - baseline.meanDuration) / baseline.meanDuration) * 100; + if (increasePercentage > durationDeviation) { + core.setFailed(`Duration increase percentage exceeded the allowed deviation of ${durationDeviation}%. Current percentage: ${increasePercentage}%`); + break; + } else { + console.log(`Duration increase percentage ${increasePercentage}% is within the allowed deviation range of ${durationDeviation}%.`); + } + } + + return true; +}; + +if (require.main === require.cache[eval('__filename')]) { + run(); +} + +module.exports = run; + + +/***/ }), + +/***/ 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__(87)); +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__(87)); +const path = __importStar(__nccwpck_require__(622)); +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__(747)); +const os = __importStar(__nccwpck_require__(87)); +const uuid_1 = __nccwpck_require__(521); +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__(622)); +/** + * 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__(87); +const fs_1 = __nccwpck_require__(747); +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__(605)); +const https = __importStar(__nccwpck_require__(211)); +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__(631); +var tls = __nccwpck_require__(16); +var http = __nccwpck_require__(605); +var https = __nccwpck_require__(211); +var events = __nccwpck_require__(614); +var assert = __nccwpck_require__(357); +var util = __nccwpck_require__(669); + + +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 + + +/***/ }), + +/***/ 521: +/***/ ((__unused_webpack_module, __webpack_exports__, __nccwpck_require__) => { + +"use strict"; +// ESM COMPAT FLAG +__nccwpck_require__.r(__webpack_exports__); + +// EXPORTS +__nccwpck_require__.d(__webpack_exports__, { + "NIL": () => /* reexport */ nil, + "parse": () => /* reexport */ esm_node_parse, + "stringify": () => /* reexport */ esm_node_stringify, + "v1": () => /* reexport */ esm_node_v1, + "v3": () => /* reexport */ esm_node_v3, + "v4": () => /* reexport */ esm_node_v4, + "v5": () => /* reexport */ esm_node_v5, + "validate": () => /* reexport */ esm_node_validate, + "version": () => /* reexport */ esm_node_version +}); + +// CONCATENATED MODULE: external "crypto" +const external_crypto_namespaceObject = require("crypto");; +var external_crypto_default = /*#__PURE__*/__nccwpck_require__.n(external_crypto_namespaceObject); + +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/rng.js + +const rnds8Pool = new Uint8Array(256); // # of random values to pre-allocate + +let poolPtr = rnds8Pool.length; +function rng() { + if (poolPtr > rnds8Pool.length - 16) { + external_crypto_default().randomFillSync(rnds8Pool); + poolPtr = 0; + } + + return rnds8Pool.slice(poolPtr, poolPtr += 16); +} +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/regex.js +/* harmony default export */ const regex = (/^(?:[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); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/validate.js + + +function validate(uuid) { + return typeof uuid === 'string' && regex.test(uuid); +} + +/* harmony default export */ const esm_node_validate = (validate); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/stringify.js + +/** + * 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 (!esm_node_validate(uuid)) { + throw TypeError('Stringified UUID is invalid'); + } + + return uuid; +} + +/* harmony default export */ const esm_node_stringify = (stringify); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/v1.js + + // **`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)(); + + 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 || esm_node_stringify(b); +} + +/* harmony default export */ const esm_node_v1 = (v1); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/parse.js + + +function parse(uuid) { + if (!esm_node_validate(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; +} + +/* harmony default export */ const esm_node_parse = (parse); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/v35.js + + + +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'; +const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; +/* harmony default export */ function v35(name, version, hashfunc) { + function generateUUID(value, namespace, buf, offset) { + if (typeof value === 'string') { + value = stringToBytes(value); + } + + if (typeof namespace === 'string') { + namespace = esm_node_parse(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 esm_node_stringify(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; +} +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/md5.js + + +function md5(bytes) { + if (Array.isArray(bytes)) { + bytes = Buffer.from(bytes); + } else if (typeof bytes === 'string') { + bytes = Buffer.from(bytes, 'utf8'); + } + + return external_crypto_default().createHash('md5').update(bytes).digest(); +} + +/* harmony default export */ const esm_node_md5 = (md5); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/v3.js + + +const v3 = v35('v3', 0x30, esm_node_md5); +/* harmony default export */ const esm_node_v3 = (v3); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/v4.js + + + +function v4(options, buf, offset) { + options = options || {}; + const rnds = options.random || (options.rng || rng)(); // 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 esm_node_stringify(rnds); +} + +/* harmony default export */ const esm_node_v4 = (v4); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/sha1.js + + +function sha1(bytes) { + if (Array.isArray(bytes)) { + bytes = Buffer.from(bytes); + } else if (typeof bytes === 'string') { + bytes = Buffer.from(bytes, 'utf8'); + } + + return external_crypto_default().createHash('sha1').update(bytes).digest(); +} + +/* harmony default export */ const esm_node_sha1 = (sha1); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/v5.js + + +const v5 = v35('v5', 0x50, esm_node_sha1); +/* harmony default export */ const esm_node_v5 = (v5); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/nil.js +/* harmony default export */ const nil = ('00000000-0000-0000-0000-000000000000'); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/version.js + + +function version(uuid) { + if (!esm_node_validate(uuid)) { + throw TypeError('Invalid UUID'); + } + + return parseInt(uuid.substr(14, 1), 16); +} + +/* harmony default export */ const esm_node_version = (version); +// CONCATENATED MODULE: ./node_modules/uuid/dist/esm-node/index.js + + + + + + + + + + +/***/ }), + +/***/ 357: +/***/ ((module) => { + +"use strict"; +module.exports = require("assert");; + +/***/ }), + +/***/ 614: +/***/ ((module) => { + +"use strict"; +module.exports = require("events");; + +/***/ }), + +/***/ 747: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs");; + +/***/ }), + +/***/ 605: +/***/ ((module) => { + +"use strict"; +module.exports = require("http");; + +/***/ }), + +/***/ 211: +/***/ ((module) => { + +"use strict"; +module.exports = require("https");; + +/***/ }), + +/***/ 631: +/***/ ((module) => { + +"use strict"; +module.exports = require("net");; + +/***/ }), + +/***/ 87: +/***/ ((module) => { + +"use strict"; +module.exports = require("os");; + +/***/ }), + +/***/ 622: +/***/ ((module) => { + +"use strict"; +module.exports = require("path");; + +/***/ }), + +/***/ 16: +/***/ ((module) => { + +"use strict"; +module.exports = require("tls");; + +/***/ }), + +/***/ 669: +/***/ ((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 +/******/ if(__webpack_module_cache__[moduleId]) { +/******/ return __webpack_module_cache__[moduleId].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 get default export */ +/******/ (() => { +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __nccwpck_require__.n = (module) => { +/******/ var getter = module && module.__esModule ? +/******/ () => module['default'] : +/******/ () => module; +/******/ __nccwpck_require__.d(getter, { a: getter }); +/******/ return getter; +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __nccwpck_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__nccwpck_require__.o(definition, key) && !__nccwpck_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __nccwpck_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __nccwpck_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ __nccwpck_require__.ab = __dirname + "/";/************************************************************************/ +/******/ // module exports must be returned from runtime so entry inlining is disabled +/******/ // startup +/******/ // Load entry module and return exports +/******/ return __nccwpck_require__(688); +/******/ })() +; diff --git a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js new file mode 100644 index 000000000000..da81d88c9885 --- /dev/null +++ b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.js @@ -0,0 +1,46 @@ +const core = require('@actions/core'); + +const run = () => { + const regressionOutput = JSON.parse(core.getInput('REGRESSION_OUTPUT', {required: true})); + const countDeviation = core.getInput('COUNT_DEVIATION', {required: true}); + const durationDeviation = core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true}); + + if (regressionOutput.countChanged === undefined || regressionOutput.countChanged.length === 0) { + console.log('No countChanged data available. Exiting...'); + return true; + } + + console.log(`Processing ${regressionOutput.countChanged.length} measurements...`); + + for (let i = 0; i < regressionOutput.countChanged.length; i++) { + const measurement = regressionOutput.countChanged[i]; + const baseline = measurement.baseline; + const current = measurement.current; + + console.log(`Processing measurement ${i + 1}: ${measurement.name}`); + + const renderCountDiff = current.meanCount - baseline.meanCount; + if (renderCountDiff > countDeviation) { + core.setFailed(`Render count difference exceeded the allowed deviation of ${countDeviation}. Current difference: ${renderCountDiff}`); + break; + } else { + console.log(`Render count difference ${renderCountDiff} is within the allowed deviation range of ${countDeviation}.`); + } + + const increasePercentage = ((current.meanDuration - baseline.meanDuration) / baseline.meanDuration) * 100; + if (increasePercentage > durationDeviation) { + core.setFailed(`Duration increase percentage exceeded the allowed deviation of ${durationDeviation}%. Current percentage: ${increasePercentage}%`); + break; + } else { + console.log(`Duration increase percentage ${increasePercentage}% is within the allowed deviation range of ${durationDeviation}%.`); + } + } + + return true; +}; + +if (require.main === module) { + run(); +} + +module.exports = run; diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh index 641ed01a04af..169efabcec72 100755 --- a/.github/scripts/buildActions.sh +++ b/.github/scripts/buildActions.sh @@ -26,6 +26,7 @@ declare -r GITHUB_ACTIONS=( "$ACTIONS_DIR/verifySignedCommits/verifySignedCommits.js" "$ACTIONS_DIR/authorChecklist/authorChecklist.js" "$ACTIONS_DIR/reviewerChecklist/reviewerChecklist.js" + "$ACTIONS_DIR/validateReassureOutput/validateReassureOutput.js" ) # This will be inserted at the top of all compiled files as a warning to devs. diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 48c8e38fe3e1..bac7ab34920d 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -158,13 +158,13 @@ jobs: bundler-cache: true - uses: actions/cache@v3 + id: cache-pods with: path: ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - name: Install cocoapods + if: steps.cache-pods.outputs.cache-hit != 'true' uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 with: timeout_minutes: 10 diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml new file mode 100644 index 000000000000..03711bd8967c --- /dev/null +++ b/.github/workflows/reassurePerformanceTests.yml @@ -0,0 +1,45 @@ +name: Reassure Performance Tests + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: [staging, production] + +jobs: + perf-tests: + if: ${{ github.actor != 'OSBotify' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + + - name: Setup NodeJS + uses: Expensify/App/.github/actions/composite/setupNode@main + + - name: Run performance testing script + shell: bash + run: | + set -e + BASELINE_BRANCH=${BASELINE_BRANCH:="main"} + git fetch origin + git switch "$BASELINE_BRANCH" + npm install --force + npx reassure --baseline + git switch --detach - + npm install --force + npx reassure --branch + + - name: Read output.json + id: reassure + uses: juliangruber/read-file-action@v1 + with: + path: .reassure/output.json + + - name: Validate output.json + id: validateReassureOutput + uses: Expensify/App/.github/actions/javascript/validateReassureOutput@main + with: + DURATION_DEVIATION_PERCENTAGE: 20 + COUNT_DEVIATION: 0 + REGRESSION_OUTPUT: ${{ steps.reassure.outputs.content }} + diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 9b4bf6d020d6..b75ee2a402e4 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -141,13 +141,13 @@ jobs: bundler-cache: true - uses: actions/cache@v3 + id: cache-pods with: path: ios/Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - name: Install cocoapods + if: steps.cache-pods.outputs.cache-hit != 'true' uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 with: timeout_minutes: 10 diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index fcd7138da7e6..7568dbb55eb0 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -12,10 +12,6 @@ "/": "/settings/*", "comment": "Profile and app settings" }, - { - "/": "/setpassword/*", - "comment": "Passoword setup" - }, { "/": "/details/*", "comment": "Details of another users" diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index d3f5ecfc0a5c..26a943ce62bc 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -67,6 +67,14 @@ jest.doMock('react-native', () => { dimensions = newDimensions; }, }, + + // `runAfterInteractions` method would normally be triggered after the native animation is completed, + // we would have to mock waiting for the animation end and more state changes, + // so it seems easier to just run the callback immediately in tests. + InteractionManager: { + ...ReactNative.InteractionManager, + runAfterInteractions: (callback) => callback(), + }, }, ReactNative, ); diff --git a/android/app/build.gradle b/android/app/build.gradle index 32653aa60385..c29f64297d23 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,8 +106,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001032404 - versionName "1.3.24-4" + versionCode 1001032601 + versionName "1.3.26-1" } splits { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 05302f43ef8d..e79b72f0e904 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -52,7 +52,6 @@ - @@ -64,7 +63,6 @@ - diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 9bae001c2b53..b8ed35b2f663 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -20,7 +20,6 @@ const includeModules = [ 'react-native-gesture-handler', 'react-native-flipper', 'react-native-google-places-autocomplete', - '@react-navigation/drawer', 'react-native-qrcode-svg', 'react-native-view-shot', ].join('|'); @@ -59,13 +58,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ publicPath: '/', }, stats: { - warningsFilter: [ - // @react-navigation for web uses the legacy modules (related to react-native-reanimated) - // This results in 33 warnings with stack traces that appear during build and each time we make a change - // We can't do anything about the warnings, and they only get in the way, so we suppress them - './node_modules/@react-navigation/drawer/lib/module/views/legacy/Drawer.js', - './node_modules/@react-navigation/drawer/lib/module/views/legacy/Overlay.js', - ], + warningsFilter: [], }, plugins: [ new CleanWebpackPlugin(), diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index a6b9a3404ac4..8d608e7a0d02 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -35,7 +35,7 @@ All contributors should be a member of **two** Slack channels: Before requesting an invite to Slack please ensure your Upwork account is active, since we only pay via Upwork (see [below](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#payment-for-contributions)). To request an invite to these two Slack channels, email contributors@expensify.com with the subject `Slack Channel Invites`. We'll send you an invite! -Note: the Expensify team will not be able to respond to direct messages in Slack. +Note: Do not send direct messages to the Expensify team in Slack or Expensify Chat, they will not be able to respond. Note: if you are hired for an Upwork job and have any job-specific questions, please ask in the GitHub issue or pull request. This will ensure that the person addressing your question has as much context as possible. @@ -196,4 +196,4 @@ During communication with Expensify, you will come across a variety of acronyms - **RHP:** Right Hand Panel (on larger screens, pages are often displayed docked to the right side of the screen) - **QA:** Quality Assurance - **GH:** GitHub -- **LGTM:*** Looks good to me \ No newline at end of file +- **LGTM:*** Looks good to me diff --git a/docs/articles/other/Everything-About-Chat.md b/docs/articles/other/Everything-About-Chat.md index 8d4ad5f8740c..521bcd6eca25 100644 --- a/docs/articles/other/Everything-About-Chat.md +++ b/docs/articles/other/Everything-About-Chat.md @@ -23,6 +23,37 @@ In addition to 1:1 and group chat, members of a Workspace or Policy will have ac All workspace members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between workspace members. All workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your policy, and chat with your dedicated Expensify Onboarding Guide. If you have a subscription of 10 or more users, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this same #admins room. Anytime someone on your team, your dedicated setup specialist, or your dedicated account manager makes any changes to your Workspace settings, that update is logged in the #admins room. +## How to format text + +#### Italic +###### To italicize your message, place an underscore on both sides of the text: +*text* + +#### Bold +###### To bold your message, place an asterisk on both sides of the text: +**text** + +#### Strikethrough +###### To strikethrough your message, place a tilde on both sides of the text: +~text~ + +#### Quote +###### To turn your text into a blockquote, add a `>` symbol in front of the text: +> your text + +#### Code +###### To turn your message into code, place a backtick on both sides of the text: +`text` + +#### Codeblock +###### To turn your entire message into code block, place three backticks on both sides of the text: +``` +text +and even more text +``` +#### Heading +###### To turn your message into a heading, place the `#` symbol in front of the text: +## Heading # FAQs ## How do I add more than one person to a chat? diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f3d3e3ef773b..4a2406aaa1b8 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion en CFBundleDisplayName @@ -17,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.24 + 1.3.26 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +32,7 @@ CFBundleVersion - 1.3.24.4 + 1.3.26.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3dd42f91ffbc..591c2f2b999a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.24 + 1.3.26 CFBundleSignature ???? CFBundleVersion - 1.3.24.4 + 1.3.26.1 diff --git a/package-lock.json b/package-lock.json index e275bcd918ec..82d99eaa3ab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.24-4", + "version": "1.3.26-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.24-4", + "version": "1.3.26-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -31,9 +31,8 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-picker/picker": "^2.4.3", - "@react-navigation/drawer": "github:Expensify/react-navigation#react-navigation-drawer-v6.5.0-alpha1-gitpkg", - "@react-navigation/native": "6.0.13", - "@react-navigation/stack": "6.3.1", + "@react-navigation/native": "6.1.6", + "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@ua/react-native-airship": "^15.2.6", "awesome-phonenumber": "^5.4.0", @@ -41,7 +40,7 @@ "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c898563fe851d9a4d594fa9afbdd1ddab5971636", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16", "fbjs": "^3.0.2", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", @@ -7718,13 +7717,14 @@ "license": "MIT" }, "node_modules/@react-navigation/core": { - "version": "6.4.0", - "license": "MIT", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.8.tgz", + "integrity": "sha512-klZ9Mcf/P2j+5cHMoGyIeurEzyBM2Uq9+NoSFrF6sdV5iCWHLFhrCXuhbBiQ5wVLCKf4lavlkd/DDs47PXs9RQ==", "dependencies": { - "@react-navigation/routers": "^6.1.3", + "@react-navigation/routers": "^6.1.8", "escape-string-regexp": "^4.0.0", "nanoid": "^3.1.23", - "query-string": "^7.0.0", + "query-string": "^7.1.3", "react-is": "^16.13.0", "use-latest-callback": "^0.1.5" }, @@ -7745,40 +7745,12 @@ "react": "*" } }, - "node_modules/@react-navigation/drawer": { - "version": "6.5.0-alpha1", - "resolved": "git+ssh://git@github.com/Expensify/react-navigation.git#bee9dc3f6bd03bb24f529efcb9f0d5d5832df6d6", - "license": "MIT", - "dependencies": { - "@react-navigation/elements": "^1.3.6", - "color": "^4.2.3", - "warn-once": "^0.1.0" - }, - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-gesture-handler": ">= 2.0.0", - "react-native-reanimated": "*", - "react-native-safe-area-context": ">= 3.0.0", - "react-native-screens": ">= 3.0.0" - } - }, - "node_modules/@react-navigation/elements": { - "version": "1.3.6", - "license": "MIT", - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-safe-area-context": ">= 3.0.0" - } - }, "node_modules/@react-navigation/native": { - "version": "6.0.13", - "license": "MIT", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.6.tgz", + "integrity": "sha512-14PmSy4JR8HHEk04QkxQ0ZLuqtiQfb4BV9kkMXD2/jI4TZ+yc43OnO6fQ2o9wm+Bq8pY3DxyerC2AjNUz+oH7Q==", "dependencies": { - "@react-navigation/core": "^6.4.0", + "@react-navigation/core": "^6.4.8", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.1.23" @@ -7789,17 +7761,19 @@ } }, "node_modules/@react-navigation/routers": { - "version": "6.1.3", - "license": "MIT", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.8.tgz", + "integrity": "sha512-CEge+ZLhb1HBrSvv4RwOol7EKLW1QoqVIQlE9TN5MpxS/+VoQvP+cLbuz0Op53/iJfYhtXRFd1ZAd3RTRqto9w==", "dependencies": { "nanoid": "^3.1.23" } }, "node_modules/@react-navigation/stack": { - "version": "6.3.1", - "license": "MIT", + "version": "6.3.16", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.16.tgz", + "integrity": "sha512-KTOn9cNuZ6p154Htbl2DiR95Wl+c7niLPRiGs7gjOkyVDGiaGQF9ODNQTYBDE1OxZGHe/EyYc6T2CbmiItLWDg==", "dependencies": { - "@react-navigation/elements": "^1.3.6", + "@react-navigation/elements": "^1.3.17", "color": "^4.2.3", "warn-once": "^0.1.0" }, @@ -7812,6 +7786,17 @@ "react-native-screens": ">= 3.0.0" } }, + "node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz", + "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==", + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + } + }, "node_modules/@react-ng/bounds-observer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", @@ -21185,8 +21170,9 @@ "license": "MIT" }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "license": "MIT", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -24068,8 +24054,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c898563fe851d9a4d594fa9afbdd1ddab5971636", - "integrity": "sha512-WjxHYpqebNsPKJC+SBhgsYNSib+8LptZv/BKt8hc67psJjO9JdrTpAHuoZ0n1lCTQ2DhpDERjqTsQbpUqWbgIg==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16", + "integrity": "sha512-wnx6WiG2NVqIs2m3M0VjL/kw2I1rgWOD06mNp6FwRGeOVC+QIL6hsdJgqf9z9NjBSuhc6THfzbcrAOSGs3kTkw==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -24641,7 +24627,8 @@ }, "node_modules/filter-obj": { "version": "1.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", "engines": { "node": ">=0.10.0" } @@ -35780,10 +35767,11 @@ } }, "node_modules/query-string": { - "version": "7.1.1", - "license": "MIT", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "dependencies": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -39471,7 +39459,8 @@ }, "node_modules/split-on-first": { "version": "1.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", "engines": { "node": ">=6" } @@ -39739,7 +39728,8 @@ }, "node_modules/strict-uri-encode": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", "engines": { "node": ">=4" } @@ -41381,8 +41371,12 @@ } }, "node_modules/use-latest-callback": { - "version": "0.1.5", - "license": "MIT" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.1.6.tgz", + "integrity": "sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg==", + "peerDependencies": { + "react": ">=16.8" + } }, "node_modules/use-sync-external-store": { "version": "1.2.0", @@ -48329,12 +48323,14 @@ "version": "2.0.0" }, "@react-navigation/core": { - "version": "6.4.0", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.8.tgz", + "integrity": "sha512-klZ9Mcf/P2j+5cHMoGyIeurEzyBM2Uq9+NoSFrF6sdV5iCWHLFhrCXuhbBiQ5wVLCKf4lavlkd/DDs47PXs9RQ==", "requires": { - "@react-navigation/routers": "^6.1.3", + "@react-navigation/routers": "^6.1.8", "escape-string-regexp": "^4.0.0", "nanoid": "^3.1.23", - "query-string": "^7.0.0", + "query-string": "^7.1.3", "react-is": "^16.13.0", "use-latest-callback": "^0.1.5" } @@ -48348,40 +48344,41 @@ "stacktrace-parser": "^0.1.10" } }, - "@react-navigation/drawer": { - "version": "git+ssh://git@github.com/Expensify/react-navigation.git#bee9dc3f6bd03bb24f529efcb9f0d5d5832df6d6", - "from": "@react-navigation/drawer@github:Expensify/react-navigation#react-navigation-drawer-v6.5.0-alpha1-gitpkg", - "requires": { - "@react-navigation/elements": "^1.3.6", - "color": "^4.2.3", - "warn-once": "^0.1.0" - } - }, - "@react-navigation/elements": { - "version": "1.3.6", - "requires": {} - }, "@react-navigation/native": { - "version": "6.0.13", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.6.tgz", + "integrity": "sha512-14PmSy4JR8HHEk04QkxQ0ZLuqtiQfb4BV9kkMXD2/jI4TZ+yc43OnO6fQ2o9wm+Bq8pY3DxyerC2AjNUz+oH7Q==", "requires": { - "@react-navigation/core": "^6.4.0", + "@react-navigation/core": "^6.4.8", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.1.23" } }, "@react-navigation/routers": { - "version": "6.1.3", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.8.tgz", + "integrity": "sha512-CEge+ZLhb1HBrSvv4RwOol7EKLW1QoqVIQlE9TN5MpxS/+VoQvP+cLbuz0Op53/iJfYhtXRFd1ZAd3RTRqto9w==", "requires": { "nanoid": "^3.1.23" } }, "@react-navigation/stack": { - "version": "6.3.1", + "version": "6.3.16", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.16.tgz", + "integrity": "sha512-KTOn9cNuZ6p154Htbl2DiR95Wl+c7niLPRiGs7gjOkyVDGiaGQF9ODNQTYBDE1OxZGHe/EyYc6T2CbmiItLWDg==", "requires": { - "@react-navigation/elements": "^1.3.6", + "@react-navigation/elements": "^1.3.17", "color": "^4.2.3", "warn-once": "^0.1.0" + }, + "dependencies": { + "@react-navigation/elements": { + "version": "1.3.17", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz", + "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==", + "requires": {} + } } }, "@react-ng/bounds-observer": { @@ -57356,7 +57353,9 @@ "dev": true }, "decode-uri-component": { - "version": "0.2.0" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "decompress-response": { "version": "6.0.0", @@ -59287,9 +59286,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#c898563fe851d9a4d594fa9afbdd1ddab5971636", - "integrity": "sha512-WjxHYpqebNsPKJC+SBhgsYNSib+8LptZv/BKt8hc67psJjO9JdrTpAHuoZ0n1lCTQ2DhpDERjqTsQbpUqWbgIg==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#c898563fe851d9a4d594fa9afbdd1ddab5971636", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16", + "integrity": "sha512-wnx6WiG2NVqIs2m3M0VjL/kw2I1rgWOD06mNp6FwRGeOVC+QIL6hsdJgqf9z9NjBSuhc6THfzbcrAOSGs3kTkw==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", @@ -59695,7 +59694,9 @@ } }, "filter-obj": { - "version": "1.1.0" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==" }, "finalhandler": { "version": "1.2.0", @@ -67106,9 +67107,11 @@ } }, "query-string": { - "version": "7.1.1", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "requires": { - "decode-uri-component": "^0.2.0", + "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" @@ -69606,7 +69609,9 @@ } }, "split-on-first": { - "version": "1.1.0" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" }, "split-string": { "version": "3.1.0", @@ -69793,7 +69798,9 @@ "dev": true }, "strict-uri-encode": { - "version": "2.0.0" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==" }, "string_decoder": { "version": "1.1.1", @@ -70831,7 +70838,10 @@ "version": "3.1.1" }, "use-latest-callback": { - "version": "0.1.5" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.1.6.tgz", + "integrity": "sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg==", + "requires": {} }, "use-sync-external-store": { "version": "1.2.0", diff --git a/package.json b/package.json index b97e26dcbd82..14f859fe945a 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "new.expensify", - "version": "1.3.24-4", + "version": "1.3.26-1", "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.", "license": "MIT", "private": true, "scripts": { - "postinstall": "npx patch-package && cd desktop && npm install", + "postinstall": "scripts/postInstall.sh", "clean": "npx react-native clean-project-auto", "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --port=8083", "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --port=8082", @@ -30,6 +30,7 @@ "android-build-e2e": "bundle exec fastlane android build_e2e", "test": "TZ=utc jest", "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", + "lint-changed": "eslint --fix $(git diff --diff-filter=AM --name-only main -- \"*.js\")", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", "prettier": "prettier --write .", @@ -66,9 +67,8 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-picker/picker": "^2.4.3", - "@react-navigation/drawer": "github:Expensify/react-navigation#react-navigation-drawer-v6.5.0-alpha1-gitpkg", - "@react-navigation/native": "6.0.13", - "@react-navigation/stack": "6.3.1", + "@react-navigation/native": "6.1.6", + "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@ua/react-native-airship": "^15.2.6", "awesome-phonenumber": "^5.4.0", @@ -76,7 +76,7 @@ "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c898563fe851d9a4d594fa9afbdd1ddab5971636", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#50aacd53fe54ef7131e5cb9c74ee4526b3bcfe16", "fbjs": "^3.0.2", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", diff --git a/patches/@react-navigation+stack+6.3.16.patch b/patches/@react-navigation+stack+6.3.16.patch new file mode 100644 index 000000000000..7bfa8af945f6 --- /dev/null +++ b/patches/@react-navigation+stack+6.3.16.patch @@ -0,0 +1,85 @@ +diff --git a/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx b/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx +index 1e9ee0e..d85c7b4 100644 +--- a/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx ++++ b/node_modules/@react-navigation/stack/src/views/Stack/CardContainer.tsx +@@ -105,14 +105,14 @@ function CardContainer({ + const handleOpen = () => { + const { route } = scene.descriptor; + +- onTransitionEnd({ route }, false); ++ onTransitionEnd({ route }, false, scene.descriptor.navigation.getState()); + onOpenRoute({ route }); + }; + + const handleClose = () => { + const { route } = scene.descriptor; + +- onTransitionEnd({ route }, true); ++ onTransitionEnd({ route }, true, scene.descriptor.navigation.getState()); + onCloseRoute({ route }); + }; + +@@ -120,7 +120,7 @@ function CardContainer({ + const { route } = scene.descriptor; + + onPageChangeStart(); +- onGestureStart({ route }); ++ onGestureStart({ route }, scene.descriptor.navigation.getState()); + }; + + const handleGestureCanceled = () => { +diff --git a/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx b/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx +index 6bbce10..73594d3 100644 +--- a/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx ++++ b/node_modules/@react-navigation/stack/src/views/Stack/StackView.tsx +@@ -385,19 +385,47 @@ export default class StackView extends React.Component { + + private handleTransitionEnd = ( + { route }: { route: Route }, +- closing: boolean +- ) => ++ closing: boolean, ++ state: StackNavigationState ++ ) => { + this.props.navigation.emit({ + type: 'transitionEnd', + data: { closing }, + target: route.key, + }); ++ // Patch introduced to pass information about events to screens lower in the stack, so they could be safely frozen ++ if (state?.index > 1) { ++ this.props.navigation.emit({ ++ type: 'transitionEnd', ++ data: { closing: !closing }, ++ target: state.routes[state.index - 2].key, ++ }); ++ } ++ // We want the screen behind the closing screen to not be frozen ++ if (state?.index > 0) { ++ this.props.navigation.emit({ ++ type: 'transitionEnd', ++ data: { closing: false }, ++ target: state.routes[state.index - 1].key, ++ }); ++ } ++ } + +- private handleGestureStart = ({ route }: { route: Route }) => { ++ private handleGestureStart = ( ++ { route }: { route: Route }, ++ state: StackNavigationState ++ ) => { + this.props.navigation.emit({ + type: 'gestureStart', + target: route.key, + }); ++ // Patch introduced to pass information about events to screens lower in the stack, so they could be safely frozen ++ if (state?.index > 1) { ++ this.props.navigation.emit({ ++ type: 'gestureStart', ++ target: state.routes[state.index - 2].key, ++ }); ++ } + }; + + private handleGestureEnd = ({ route }: { route: Route }) => { diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch index 576858f1f5f7..049a7a09d16a 100644 --- a/patches/react-native-modal+13.0.1.patch +++ b/patches/react-native-modal+13.0.1.patch @@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644 buildPanResponder: () => void; getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number; diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js -index 80f4e75..fe028ab 100644 +index 80f4e75..a88a2ca 100644 --- a/node_modules/react-native-modal/dist/modal.js +++ b/node_modules/react-native-modal/dist/modal.js @@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component { @@ -44,3 +44,12 @@ index 80f4e75..fe028ab 100644 if (this.didUpdateDimensionsEmitter) { this.didUpdateDimensionsEmitter.remove(); } +@@ -525,7 +538,7 @@ export class ReactNativeModal extends React.Component { + } + return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), + this.makeBackdrop(), +- avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView))); ++ avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: 'padding', pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView))); + } + } + ReactNativeModal.propTypes = { diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh new file mode 100755 index 000000000000..02ec3caa5175 --- /dev/null +++ b/scripts/postInstall.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Go to project root +ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)") +cd "$ROOT_DIR" || exit 1 + +# Run patch-package +npx patch-package + +# Install node_modules in subpackages, unless we're in a CI/CD environment, +# where the node_modules for subpackages are cached separately. +# See `.github/actions/composite/setupNode/action.yml` for more context. +if [[ -n ${CI+x} ]]; then + cd desktop || exit 1 + npm install +fi diff --git a/src/App.js b/src/App.js index 5180b646d381..2b47d8ddcd28 100644 --- a/src/App.js +++ b/src/App.js @@ -17,6 +17,7 @@ import SafeArea from './components/SafeArea'; import * as Environment from './libs/Environment/Environment'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import {KeyboardStateProvider} from './components/withKeyboardState'; +import {CurrentReportIdContextProvider} from './components/withCurrentReportId'; // For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx if (window && Environment.isDevelopment()) { @@ -44,6 +45,7 @@ const App = () => ( HTMLEngineProvider, WindowDimensionsProvider, KeyboardStateProvider, + CurrentReportIdContextProvider, PickerStateProvider, ]} > diff --git a/src/CONST.js b/src/CONST.js index f4e2847ba7a5..85b01b943216 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -10,7 +10,6 @@ const USE_EXPENSIFY_URL = 'https://use.expensify.com'; const PLATFORM_OS_MACOS = 'Mac OS'; const PLATFORM_IOS = 'iOS'; const ANDROID_PACKAGE_NAME = 'com.expensify.chat'; -const USA_COUNTRY_NAME = 'United States'; const CURRENT_YEAR = new Date().getFullYear(); const PULL_REQUEST_NUMBER = lodashGet(Config, 'PULL_REQUEST_NUMBER', ''); @@ -162,6 +161,8 @@ const CONST = { }, }, + RIGHT_MODAL_BACKGROUND_OVERLAY_OPACITY: 0.4, + NEW_EXPENSIFY_URL: ACTIVE_EXPENSIFY_URL, APP_DOWNLOAD_LINKS: { ANDROID: `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE_NAME}`, @@ -828,13 +829,20 @@ const CONST = { WIDTH: 320, HEIGHT: 416, }, - NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 256, + DESKTOP_HEADER_PADDING: 12, + CATEGORY_SHORTCUT_BAR_HEIGHT: 32, + SMALL_EMOJI_PICKER_SIZE: { + WIDTH: '100%', + }, + NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300, + NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT_WEB: 200, EMOJI_PICKER_ITEM_HEIGHT: 32, EMOJI_PICKER_HEADER_HEIGHT: 32, RECIPIENT_LOCAL_TIME_HEIGHT: 25, AUTO_COMPLETE_SUGGESTER: { SUGGESTER_PADDING: 6, - ITEM_HEIGHT: 36, + SUGGESTER_INNER_PADDING: 8, + ITEM_HEIGHT: 40, SMALL_CONTAINER_HEIGHT_FACTOR: 2.5, MIN_AMOUNT_OF_ITEMS: 3, MAX_AMOUNT_OF_ITEMS: 5, @@ -1078,6 +1086,7 @@ const CONST = { MID_SUBSCRIPT: 'mid-subscript', LARGE_BORDERED: 'large-bordered', HEADER: 'header', + MENTION_ICON: 'mention-icon', }, OPTION_MODE: { COMPACT: 'compact', @@ -1122,6 +1131,10 @@ const CONST = { // eslint-disable-next-line no-misleading-character-class /[\n\s,/?"{}[\]()&^%$#<>!*\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, + SPACE_OR_EMOJI: + // eslint-disable-next-line no-misleading-character-class + /(\s+|(?:[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3)+)/gu, + // Define the regular expression pattern to match a string starting with an at sign and ending with a space or newline character MENTION_REPLACER: // eslint-disable-next-line no-misleading-character-class @@ -1297,7 +1310,6 @@ const CONST = { TFA_CODE_LENGTH: 6, CHAT_ATTACHMENT_TOKEN_KEY: 'X-Chat-Attachment-Token', - USA_COUNTRY_NAME, SPACE_LENGTH: 1, ALL_COUNTRIES: { @@ -2425,9 +2437,9 @@ const CONST = { }, SPACE_CHARACTER_WIDTH: 4, - // This ID is used in SelectionScraper.js to query the DOM for UnreadActionIndicator's - // div and then remove it from copied contents in the getHTMLOfSelection() method. - UNREAD_ACTION_INDICATOR_ID: 'no-copy-area-unread-action-indicator', + // The attribute used in the SelectionScraper.js helper to query all the DOM elements + // that should be removed from the copied contents in the getHTMLOfSelection() method + SELECTION_SCRAPER_HIDDEN_ELEMENT: 'selection-scrapper-hidden-element', MODERATION: { MODERATOR_DECISION_PENDING: 'pending', MODERATOR_DECISION_PENDING_HIDE: 'pendingHide', @@ -2440,6 +2452,7 @@ const CONST = { FLAG_SEVERITY_HARASSMENT: 'harassment', FLAG_SEVERITY_ASSAULT: 'assault', }, + EMOJI_PICKER_TEXT_INPUT_SIZES: 152, QR: { DEFAULT_LOGO_SIZE_RATIO: 0.25, DEFAULT_LOGO_MARGIN_RATIO: 0.02, diff --git a/src/NAVIGATORS.js b/src/NAVIGATORS.js new file mode 100644 index 000000000000..d9dcf9d3cd52 --- /dev/null +++ b/src/NAVIGATORS.js @@ -0,0 +1,9 @@ +/** + * This is a file containing constants for navigators located directly in the RootStack in AuthScreens file + * The ResponsiveStackNavigator displays stack differently based on these constants + * */ +export default { + CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator', + RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', + FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', +}; diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 41570519ab51..082a1c7efbd5 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -45,6 +45,9 @@ export default { // Contains all the personalDetails the user has access to PERSONAL_DETAILS: 'personalDetails', + // Contains all the personalDetails the user has access to, keyed by accountID + PERSONAL_DETAILS_LIST: 'personalDetailsList', + // Contains all the private personal details of the user PRIVATE_PERSONAL_DETAILS: 'private_personalDetails', @@ -115,6 +118,7 @@ export default { REPORT: 'report_', REPORT_ACTIONS: 'reportActions_', REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', + REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', REPORT_DRAFT_COMMENT_NUMBER_OF_LINES: 'reportDraftCommentNumberOfLines_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', diff --git a/src/ROUTES.js b/src/ROUTES.js index c9075c42d8e1..966c3d0c5a1a 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -64,7 +64,7 @@ export default { NEW_CHAT: 'new/chat', NEW_TASK, REPORT, - REPORT_WITH_ID: 'r/:reportID', + REPORT_WITH_ID: 'r/:reportID?', getReportRoute: (reportID) => `r/${reportID}`, REPORT_WITH_ID_DETAILS_SHARE_CODE: 'r/:reportID/details/shareCode', getReportShareCodeRoute: (reportID) => `r/${reportID}/details/shareCode`, @@ -111,13 +111,14 @@ export default { FLAG_COMMENT: `flag/:reportID/:reportActionID`, getFlagCommentRoute: (reportID, reportActionID) => `flag/${reportID}/${reportActionID}`, SEARCH: 'search', - SET_PASSWORD_WITH_VALIDATE_CODE: 'setpassword/:accountID/:validateCode', DETAILS: 'details', getDetailsRoute: (login) => `details?login=${encodeURIComponent(login)}`, + PROFILE: 'a/:accountID', + getProfileRoute: (accountID) => `a/${accountID}`, REPORT_PARTICIPANTS: 'r/:reportID/participants', getReportParticipantsRoute: (reportID) => `r/${reportID}/participants`, - REPORT_PARTICIPANT: 'r/:reportID/participants/details', - getReportParticipantRoute: (reportID, login) => `r/${reportID}/participants/details?login=${encodeURIComponent(login)}`, + REPORT_PARTICIPANT: 'r/:reportID/participants/a/:accountID', + getReportParticipantRoute: (reportID, accountID) => `r/${reportID}/participants/a/${accountID}`, REPORT_WITH_ID_DETAILS: 'r/:reportID/details', getReportDetailsRoute: (reportID) => `r/${reportID}/details`, REPORT_SETTINGS: 'r/:reportID/settings', diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js index 22f1b951c443..03f293d12f71 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -1,5 +1,4 @@ import React from 'react'; -import {Pressable} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import {propTypes as anchorForAttachmentsOnlyPropTypes, defaultProps as anchorForAttachmentsOnlyDefaultProps} from './anchorForAttachmentsOnlyPropTypes'; @@ -11,6 +10,7 @@ import fileDownload from '../../libs/fileDownload'; import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL'; import {ShowContextMenuContext, showContextMenuForReport} from '../ShowContextMenuContext'; import * as ReportUtils from '../../libs/ReportUtils'; +import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; const propTypes = { /** Press in handler for the link */ @@ -45,7 +45,7 @@ const BaseAnchorForAttachmentsOnly = (props) => { return ( {({anchor, report, action, checkIfContextMenuActive}) => ( - { if (isDownloading) { @@ -57,6 +57,8 @@ const BaseAnchorForAttachmentsOnly = (props) => { onPressIn={props.onPressIn} onPressOut={props.onPressOut} onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + accessibilityLabel={fileName} + accessibilityRole="button" > { shouldShowDownloadIcon shouldShowLoadingSpinnerIcon={isDownloading} /> - + )} ); diff --git a/src/components/AnchorForAttachmentsOnly/index.native.js b/src/components/AnchorForAttachmentsOnly/index.native.js index 86f6d38713c4..4fcdd2e6cff3 100644 --- a/src/components/AnchorForAttachmentsOnly/index.native.js +++ b/src/components/AnchorForAttachmentsOnly/index.native.js @@ -1,14 +1,13 @@ import React from 'react'; import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly'; -import * as StyleUtils from '../../styles/StyleUtils'; import styles from '../../styles/styles'; const AnchorForAttachmentsOnly = (props) => ( ); diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js index 3886b8fab88e..1f95ead89a4c 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js @@ -11,6 +11,7 @@ import * as ContextMenuActions from '../../pages/home/report/ContextMenu/Context import Tooltip from '../Tooltip'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; import styles from '../../styles/styles'; +import * as StyleUtils from '../../styles/StyleUtils'; import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; import {propTypes as anchorForCommentsOnlyPropTypes, defaultProps as anchorForCommentsOnlyDefaultProps} from './anchorForCommentsOnlyPropTypes'; @@ -21,6 +22,9 @@ const propTypes = { /** Press out handler for the link */ onPressOut: PropTypes.func, + // eslint-disable-next-line react/forbid-prop-types + containerStyles: PropTypes.arrayOf(PropTypes.object), + ...anchorForCommentsOnlyPropTypes, ...windowDimensionsPropTypes, }; @@ -28,6 +32,7 @@ const propTypes = { const defaultProps = { onPressIn: undefined, onPressOut: undefined, + containerStyles: [], ...anchorForCommentsOnlyDefaultProps, }; @@ -49,6 +54,7 @@ const BaseAnchorForCommentsOnly = (props) => { return ( { ReportActionContextMenu.showContextMenu( isEmail ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK, diff --git a/src/components/AttachmentCarousel/CarouselActions/index.native.js b/src/components/AttachmentCarousel/CarouselActions.js similarity index 69% rename from src/components/AttachmentCarousel/CarouselActions/index.native.js rename to src/components/AttachmentCarousel/CarouselActions.js index d12cd6bfbb60..3946a613ee16 100644 --- a/src/components/AttachmentCarousel/CarouselActions/index.native.js +++ b/src/components/AttachmentCarousel/CarouselActions.js @@ -1,7 +1,8 @@ import {useEffect} from 'react'; +import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import KeyboardShortcut from '../../../libs/KeyboardShortcut'; -import CONST from '../../../CONST'; +import KeyboardShortcut from '../../libs/KeyboardShortcut'; +import CONST from '../../CONST'; const propTypes = { /** Callback to cycle through attachments */ @@ -13,7 +14,12 @@ const Carousel = (props) => { const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT; const unsubscribeLeftKey = KeyboardShortcut.subscribe( shortcutLeftConfig.shortcutKey, - () => { + (e) => { + if (lodashGet(e, 'target.blur')) { + // prevents focus from highlighting around the modal + e.target.blur(); + } + props.onCycleThroughAttachments(-1); }, shortcutLeftConfig.descriptionKey, @@ -23,7 +29,12 @@ const Carousel = (props) => { const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT; const unsubscribeRightKey = KeyboardShortcut.subscribe( shortcutRightConfig.shortcutKey, - () => { + (e) => { + if (lodashGet(e, 'target.blur')) { + // prevents focus from highlighting around the modal + e.target.blur(); + } + props.onCycleThroughAttachments(1); }, shortcutRightConfig.descriptionKey, diff --git a/src/components/AttachmentCarousel/CarouselActions/index.js b/src/components/AttachmentCarousel/CarouselActions/index.js deleted file mode 100644 index 4ec551daa252..000000000000 --- a/src/components/AttachmentCarousel/CarouselActions/index.js +++ /dev/null @@ -1,42 +0,0 @@ -import {useCallback, useEffect} from 'react'; -import PropTypes from 'prop-types'; - -const propTypes = { - /** Callback to cycle through attachments */ - onCycleThroughAttachments: PropTypes.func.isRequired, -}; - -const Carousel = (props) => { - /** - * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e - */ - const handleKeyPress = useCallback((e) => { - // prevents focus from highlighting around the modal - e.target.blur(); - - if (e.key === 'ArrowLeft') { - props.onCycleThroughAttachments(-1); - } - if (e.key === 'ArrowRight') { - props.onCycleThroughAttachments(1); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - document.addEventListener('keydown', handleKeyPress); - - return () => { - document.removeEventListener('keydown', handleKeyPress); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return null; -}; - -Carousel.propTypes = propTypes; - -export default Carousel; diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js index 6ae3214cd32a..77ab66400ae2 100644 --- a/src/components/AttachmentCarousel/index.js +++ b/src/components/AttachmentCarousel/index.js @@ -20,6 +20,7 @@ import Tooltip from '../Tooltip'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import compose from '../../libs/compose'; import withWindowDimensions from '../withWindowDimensions'; +import reportPropTypes from '../../pages/reportPropTypes'; const propTypes = { /** source is used to determine the starting index in the array of attachments */ @@ -31,6 +32,9 @@ const propTypes = { /** Object of report actions for this report */ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The report currently being looked at */ + report: reportPropTypes.isRequired, + ...withLocalizePropTypes, }; @@ -148,7 +152,7 @@ class AttachmentCarousel extends React.Component { * @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}} */ createInitialState() { - const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions)); + const actions = [ReportActionsUtils.getParentReportAction(this.props.report), ...ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions))]; const attachments = []; const htmlParser = new HtmlParser({ @@ -164,7 +168,7 @@ class AttachmentCarousel extends React.Component { attachments.unshift({ source: tryResolveUrlFromApiRoot(expensifySource || attribs.src), isAuthTokenRequired: Boolean(expensifySource), - file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || attribs.src.split('/').pop()}, + file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]}, }); }, }); @@ -359,7 +363,7 @@ AttachmentCarousel.defaultProps = defaultProps; export default compose( withOnyx({ reportActions: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, canEvict: false, }, }), diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 24a6ecfb3152..393a0085176c 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -16,13 +16,14 @@ import themeColors from '../styles/themes/default'; import compose from '../libs/compose'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import Button from './Button'; -import HeaderWithCloseButton from './HeaderWithCloseButton'; +import HeaderWithBackButton from './HeaderWithBackButton'; import fileDownload from '../libs/fileDownload'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import ConfirmModal from './ConfirmModal'; import HeaderGap from './HeaderGap'; import SafeAreaConsumer from './SafeAreaConsumer'; import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL'; +import reportPropTypes from '../pages/reportPropTypes'; /** * Modal render prop component that exposes modal launching triggers that can be used @@ -57,8 +58,8 @@ const propTypes = { /** Title shown in the header of the modal */ headerTitle: PropTypes.string, - /** The ID of the report that has this attachment */ - reportID: PropTypes.string, + /** The report that has this attachment */ + report: reportPropTypes, ...withLocalizePropTypes, @@ -72,7 +73,7 @@ const defaultProps = { isAuthTokenRequired: false, allowDownload: false, headerTitle: null, - reportID: '', + report: {}, onModalShow: () => {}, onModalHide: () => {}, }; @@ -276,17 +277,20 @@ class AttachmentModal extends PureComponent { propagateSwipe > {this.props.isSmallScreenWidth && } - this.setState({isModalOpen: false})} onCloseButtonPress={() => this.setState({isModalOpen: false})} /> - {this.props.reportID ? ( + {!_.isEmpty(this.props.report) ? ( { * @returns {JSX.Element} */ const renderSuggestionMenuItem = ({item, index}) => ( - StyleUtils.getAutoCompleteSuggestionItemStyle(props.highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.ITEM_HEIGHT, hovered, index)} + hoverDimmingValue={1} onMouseDown={(e) => e.preventDefault()} onPress={() => props.onSelect(index)} onLongPress={() => {}} + accessibilityLabel={props.accessibilityLabelExtractor(item, index)} > {props.renderSuggestionMenuItem(item, index)} - + ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.ITEM_HEIGHT * props.suggestions.length; diff --git a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js index eed9368e534d..6ff330d839c6 100644 --- a/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js +++ b/src/components/AutoCompleteSuggestions/autoCompleteSuggestionsPropTypes.js @@ -24,6 +24,9 @@ const propTypes = { /** Show that we should include ReportRecipientLocalTime view height */ shouldIncludeReportRecipientLocalTimeHeight: PropTypes.bool.isRequired, + + /** create accessibility label for each item */ + accessibilityLabelExtractor: PropTypes.func.isRequired, }; const defaultProps = {}; diff --git a/src/components/AutoEmailLink.js b/src/components/AutoEmailLink.js new file mode 100644 index 000000000000..f99c8ea76f3c --- /dev/null +++ b/src/components/AutoEmailLink.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import {CONST} from 'expensify-common/lib/CONST'; +import Text from './Text'; +import TextLink from './TextLink'; +import styles from '../styles/styles'; + +const propTypes = { + text: PropTypes.string.isRequired, + style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), +}; + +const defaultProps = { + style: [], +}; + +/* + * This is a "utility component", that does this: + * - Checks if a text contains any email. If it does, render it as a mailto: link + * - Else just render it inside `Text` component + */ + +const AutoEmailLink = (props) => ( + + {_.map(props.text.split(CONST.REG_EXP.EXTRACT_EMAIL), (str, index) => { + if (CONST.REG_EXP.EMAIL.test(str)) { + return ( + + {str} + + ); + } + + return ( + + {str} + + ); + })} + +); + +AutoEmailLink.displayName = 'AutoEmailLink'; +AutoEmailLink.propTypes = propTypes; +AutoEmailLink.defaultProps = defaultProps; +export default AutoEmailLink; diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.js index 025a0aa697ea..af113276a2be 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.js @@ -8,7 +8,7 @@ import compose from '../../libs/compose'; import styles from '../../styles/styles'; import themeColors from '../../styles/themes/default'; import Button from '../Button'; -import HeaderWithCloseButton from '../HeaderWithCloseButton'; +import HeaderWithBackButton from '../HeaderWithBackButton'; import Icon from '../Icon'; import * as Expensicons from '../Icon/Expensicons'; import Modal from '../Modal'; @@ -361,9 +361,9 @@ const AvatarCropModal = (props) => { onModalHide={resetState} > {props.isSmallScreenWidth && } - {props.translate('avatarCropModal.description')} {}, + pendingAction: null, + errors: null, }; class AvatarWithImagePicker extends React.Component { @@ -256,24 +275,36 @@ class AvatarWithImagePicker extends React.Component { return ( - this.setState({isMenuVisible: true})}> + this.setState({isMenuVisible: true})} + accessibilityRole="button" + accessibilityLabel={this.props.translate('avatarWithImagePicker.editImage')} + > - - - {this.props.source ? ( - - ) : ( - - )} - - + + + + {this.props.source ? ( + + ) : ( + + )} + + + + {({openPicker}) => ( <> @@ -299,7 +330,7 @@ class AvatarWithImagePicker extends React.Component { )} - + { if (key !== '<') { return; } - this.props.longPressHandlerStateChanged(true); - const timer = setInterval(() => { - this.props.numberPressed(key); + + props.longPressHandlerStateChanged(true); + + const newTimer = setInterval(() => { + props.numberPressed(key); }, 100); - this.setState({timer}); - } + setTimer(newTimer); + }; - render() { - return ( - - {_.map(padNumbers, (row, rowIndex) => ( - - {_.map(row, (column, columnIndex) => { - // Adding margin between buttons except first column to - // avoid unccessary space before the first column. - const marginLeft = columnIndex > 0 ? styles.ml3 : {}; - return ( - )} @@ -447,6 +446,7 @@ function ReportActionItem(props) { ReportActions.clearReportActionErrors(props.report.reportID, props.action)} pendingAction={props.draftMessage ? null : props.action.pendingAction} + shouldHideOnDelete={!ReportActionsUtils.hasCommentThread(props.action)} errors={props.action.errors} errorRowStyles={[styles.ml10, styles.mr2]} needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(props.action)} diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index 05cba64e6236..e606b0c65f69 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -97,7 +97,11 @@ const ReportActionItemFragment = (props) => { } const {html, text} = props.fragment; - if (props.fragment.isDeletedParentAction) { + // Threaded messages display "[Deleted message]" instead of being hidden altogether. + // While offline we display the previous message with a strikethrough style. Once online we want to + // immediately display "[Deleted message]" while the delete action is pending. + + if ((!props.network.isOffline && props.hasCommentThread && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) || props.fragment.isDeletedParentAction) { return ${props.translate('parentReportAction.deletedMessage')}`} />; } @@ -118,7 +122,6 @@ const ReportActionItemFragment = (props) => { return ( diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index a764eafb84b7..aa8639ea071c 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import styles from '../../../styles/styles'; import ReportActionItemFragment from './ReportActionItemFragment'; +import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import reportActionPropTypes from './reportActionPropTypes'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; @@ -35,6 +36,7 @@ const ReportActionItemMessage = (props) => ( key={`actionFragment-${props.action.reportActionID}-${index}`} fragment={fragment} isAttachment={props.action.isAttachment} + hasCommentThread={ReportActionsUtils.hasCommentThread(props.action)} attachmentInfo={props.action.attachmentInfo} pendingAction={props.action.pendingAction} source={lodashGet(props.action, 'originalMessage.source')} diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 96fd6be6ad26..eb5f2fa07434 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -61,13 +61,13 @@ const defaultProps = { report: undefined, }; -const showUserDetails = (email) => { - Navigation.navigate(ROUTES.getDetailsRoute(email)); +const showUserDetails = (accountID) => { + Navigation.navigate(ROUTES.getProfileRoute(accountID)); }; const ReportActionItemSingle = (props) => { const actorEmail = props.action.actorEmail.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const {avatar, displayName, pendingFields} = props.personalDetails[actorEmail] || {}; + const {accountID, avatar, displayName, pendingFields} = props.personalDetails[actorEmail] || {}; const avatarSource = UserUtils.getAvatar(avatar, actorEmail); // Since the display name for a report action message is delivered with the report history as an array of fragments @@ -88,7 +88,7 @@ const ReportActionItemSingle = (props) => { style={[styles.alignSelfStart, styles.mr3]} onPressIn={ControlSelection.block} onPressOut={ControlSelection.unblock} - onPress={() => showUserDetails(actorEmail)} + onPress={() => showUserDetails(accountID)} > {props.shouldShowSubscriptAvatar ? ( @@ -118,7 +118,7 @@ const ReportActionItemSingle = (props) => { style={[styles.flexShrink1, styles.mr1]} onPressIn={ControlSelection.block} onPressOut={ControlSelection.unblock} - onPress={() => showUserDetails(actorEmail)} + onPress={() => showUserDetails(accountID)} > {_.map(personArray, (fragment, index) => ( { // Native mobile does not render updates flatlist the changes even though component did update called. // To notify there something changes we can use extraData prop to flatlist - const extraData = [!props.isDrawerOpen && props.isSmallScreenWidth ? props.newMarkerReportActionID : undefined, ReportUtils.isArchivedRoom(props.report)]; + const extraData = [props.isSmallScreenWidth ? props.newMarkerReportActionID : undefined, ReportUtils.isArchivedRoom(props.report)]; const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(props.personalDetails, props.report, props.currentUserPersonalDetails.login); return ( @@ -199,4 +197,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withDrawerState, withWindowDimensions, withLocalize, withPersonalDetails(), withNetwork(), withCurrentUserPersonalDetails)(ReportActionsList); +export default compose(withWindowDimensions, withLocalize, withPersonalDetails(), withNetwork(), withCurrentUserPersonalDetails)(ReportActionsList); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 7336b78b48e3..6c7cf10da781 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -9,7 +9,6 @@ import Timing from '../../../libs/actions/Timing'; import CONST from '../../../CONST'; import compose from '../../../libs/compose'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import {withDrawerPropTypes} from '../../../components/withDrawerState'; import * as ReportScrollManager from '../../../libs/ReportScrollManager'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import Performance from '../../../libs/Performance'; @@ -21,6 +20,7 @@ import CopySelectionHelper from '../../../components/CopySelectionHelper'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; import * as ReportUtils from '../../../libs/ReportUtils'; import reportPropTypes from '../../reportPropTypes'; +import withNavigationFocus from '../../../components/withNavigationFocus'; import * as ReactionList from './ReactionList/ReactionList'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import getIsReportFullyVisible from '../../../libs/getIsReportFullyVisible'; @@ -48,7 +48,6 @@ const propTypes = { }), ...windowDimensionsPropTypes, - ...withDrawerPropTypes, ...withLocalizePropTypes, }; @@ -95,9 +94,7 @@ class ReportActionsView extends React.Component { } }); - if (this.isReportFullyVisible()) { - this.openReportIfNecessary(); - } + this.openReportIfNecessary(); // This callback is triggered when a new action arrives via Pusher and the event is emitted from Report.js. This allows us to maintain // a single source of truth for the "new action" event instead of trying to derive that a new action has appeared from looking at props. @@ -163,10 +160,6 @@ class ReportActionsView extends React.Component { return true; } - if (this.props.isDrawerOpen !== nextProps.isDrawerOpen) { - return true; - } - if (lodashGet(this.props.report, 'hasOutstandingIOU') !== lodashGet(nextProps.report, 'hasOutstandingIOU')) { return true; } @@ -205,11 +198,10 @@ class ReportActionsView extends React.Component { } } - // If the report was previously hidden by the side bar, or the view is expanded from mobile to desktop layout + // If the view is expanded from mobile to desktop layout // we update the new marker position, mark the report as read, and fetch new report actions - const didSidebarClose = prevProps.isDrawerOpen && !this.props.isDrawerOpen; const didScreenSizeIncrease = prevProps.isSmallScreenWidth && !this.props.isSmallScreenWidth; - const didReportBecomeVisible = isReportFullyVisible && (didSidebarClose || didScreenSizeIncrease); + const didReportBecomeVisible = isReportFullyVisible && didScreenSizeIncrease; if (didReportBecomeVisible) { this.setState({ newMarkerReportActionID: ReportUtils.isUnread(this.props.report) ? ReportUtils.getNewMarkerReportActionID(this.props.report, this.props.reportActions) : '', @@ -225,14 +217,6 @@ class ReportActionsView extends React.Component { }); } - // When the user navigates to the LHN the ReportActionsView doesn't unmount and just remains hidden. - // The next time we navigate to the same report (e.g. by swiping or tapping the LHN row) we want the new marker to clear. - const didSidebarOpen = !prevProps.isDrawerOpen && this.props.isDrawerOpen; - const didUserNavigateToSidebarAfterReadingReport = didSidebarOpen && !ReportUtils.isUnread(this.props.report); - if (didUserNavigateToSidebarAfterReadingReport) { - this.setState({newMarkerReportActionID: ''}); - } - // Checks to see if a report comment has been manually "marked as unread". All other times when the lastReadTime // changes it will be because we marked the entire report as read. const didManuallyMarkReportAsUnread = prevProps.report.lastReadTime !== this.props.report.lastReadTime && ReportUtils.isUnread(this.props.report); @@ -267,7 +251,7 @@ class ReportActionsView extends React.Component { * @returns {Boolean} */ isReportFullyVisible() { - return getIsReportFullyVisible(this.props.isDrawerOpen, this.props.isSmallScreenWidth); + return getIsReportFullyVisible(this.props.isFocused); } // If the report is optimistic (AKA not yet created) we don't need to call openReport again @@ -382,4 +366,4 @@ class ReportActionsView extends React.Component { ReportActionsView.propTypes = propTypes; ReportActionsView.defaultProps = defaultProps; -export default compose(Performance.withRenderTrace({id: ' rendering'}), withWindowDimensions, withLocalize, withNetwork())(ReportActionsView); +export default compose(Performance.withRenderTrace({id: ' rendering'}), withWindowDimensions, withNavigationFocus, withLocalize, withNetwork())(ReportActionsView); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index f0541be88ea4..92838ecf9451 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -57,53 +57,46 @@ const defaultProps = { shouldDisableCompose: false, }; -class ReportFooter extends React.Component { - /** - * @returns {Object} - */ - getChatFooterStyles() { - return {...styles.chatFooter, minHeight: !this.props.isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0}; - } +function ReportFooter(props) { + const chatFooterStyles = {...styles.chatFooter, minHeight: !props.isOffline ? CONST.CHAT_FOOTER_MIN_HEIGHT : 0}; + const isArchivedRoom = ReportUtils.isArchivedRoom(props.report); + const isAllowedToComment = ReportUtils.isAllowedToComment(props.report); + const hideComposer = isArchivedRoom || !_.isEmpty(props.errors) || !isAllowedToComment; - render() { - const isArchivedRoom = ReportUtils.isArchivedRoom(this.props.report); - const isAllowedToComment = ReportUtils.isAllowedToComment(this.props.report); - const hideComposer = isArchivedRoom || !_.isEmpty(this.props.errors) || !isAllowedToComment; - - return ( - <> - {(isArchivedRoom || hideComposer) && ( - - {isArchivedRoom && } - {!this.props.isSmallScreenWidth && ( - {hideComposer && } + return ( + <> + {(isArchivedRoom || hideComposer) && ( + + {isArchivedRoom && } + {!props.isSmallScreenWidth && ( + {hideComposer && } + )} + + )} + {!hideComposer && (props.shouldShowComposeInput || !props.isSmallScreenWidth) && ( + + + {Session.isAnonymousUser() ? ( + + ) : ( + )} - - )} - {!hideComposer && (this.props.shouldShowComposeInput || !this.props.isSmallScreenWidth) && ( - - - {Session.isAnonymousUser() ? ( - - ) : ( - - )} - - - )} - - ); - } + + + )} + + ); } +ReportFooter.displayName = 'ReportFooter'; ReportFooter.propTypes = propTypes; ReportFooter.defaultProps = defaultProps; export default compose( diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index eb1df1b9f5e1..a072402a4ca3 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -5,7 +5,6 @@ import {View} from 'react-native'; import _ from 'underscore'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import {Freeze} from 'react-freeze'; import styles from '../../../styles/styles'; import * as StyleUtils from '../../../styles/StyleUtils'; import ONYXKEYS from '../../../ONYXKEYS'; @@ -28,6 +27,9 @@ import LHNOptionsList from '../../../components/LHNOptionsList/LHNOptionsList'; import SidebarUtils from '../../../libs/SidebarUtils'; import reportPropTypes from '../../reportPropTypes'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; +import withNavigationFocus from '../../../components/withNavigationFocus'; +import withCurrentReportId from '../../../components/withCurrentReportId'; +import withNavigation, {withNavigationPropTypes} from '../../../components/withNavigation'; import Header from '../../../components/Header'; import defaultTheme from '../../../styles/themes/default'; import OptionsListSkeletonView from '../../../components/OptionsListSkeletonView'; @@ -72,9 +74,6 @@ const propTypes = { /** Current reportID from the route in react navigation state object */ reportIDFromRoute: PropTypes.string, - /** Callback when onLayout of sidebar is called */ - onLayout: PropTypes.func, - /** Whether we are viewing below the responsive breakpoint */ isSmallScreenWidth: PropTypes.bool.isRequired, @@ -82,6 +81,7 @@ const propTypes = { priorityMode: PropTypes.string, ...withLocalizePropTypes, + ...withNavigationPropTypes, }; const defaultProps = { @@ -92,7 +92,6 @@ const defaultProps = { avatar: '', }, reportIDFromRoute: '', - onLayout: () => {}, priorityMode: CONST.PRIORITY_MODE.DEFAULT, }; @@ -103,6 +102,15 @@ class SidebarLinks extends React.Component { this.showSearchPage = this.showSearchPage.bind(this); this.showSettingsPage = this.showSettingsPage.bind(this); this.showReportPage = this.showReportPage.bind(this); + + if (this.props.isSmallScreenWidth) { + App.confirmReadyToOpenApp(); + } + } + + componentDidMount() { + App.setSidebarLoaded(); + this.isSidebarLoaded = true; } showSearchPage() { @@ -140,14 +148,13 @@ class SidebarLinks extends React.Component { render() { const isLoading = _.isEmpty(this.props.personalDetails) || _.isEmpty(this.props.chatReports); - const shouldFreeze = this.props.isSmallScreenWidth && !this.props.isDrawerOpen && this.isSidebarLoaded; const optionListItems = SidebarUtils.getOrderedReportIDs(this.props.reportIDFromRoute); - const skeletonPlaceholder = ; + const skeletonPlaceholder = ; return ( @@ -200,28 +207,18 @@ class SidebarLinks extends React.Component { )} - - {isLoading ? ( - skeletonPlaceholder - ) : ( - option.toString() === this.props.reportIDFromRoute)} - onSelectRow={this.showReportPage} - shouldDisableFocusOptions={this.props.isSmallScreenWidth} - optionMode={this.props.priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT} - onLayout={() => { - this.props.onLayout(); - App.setSidebarLoaded(); - this.isSidebarLoaded = true; - }} - /> - )} - + {isLoading ? ( + skeletonPlaceholder + ) : ( + option.toString() === this.props.currentReportId)} + onSelectRow={this.showReportPage} + shouldDisableFocusOptions={this.props.isSmallScreenWidth} + optionMode={this.props.priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT} + /> + )} ); } @@ -303,7 +300,10 @@ const policySelector = (policy) => export default compose( withLocalize, withCurrentUserPersonalDetails, + withNavigationFocus, withWindowDimensions, + withCurrentReportId, + withNavigation, withOnyx({ // Note: It is very important that the keys subscribed to here are the same // keys that are subscribed to at the top of SidebarUtils.js. If there was a key missing from here and data was updated diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index 80d63a1fb1de..bbe4806deb76 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -8,9 +8,7 @@ import ROUTES from '../../../../ROUTES'; import Timing from '../../../../libs/actions/Timing'; import CONST from '../../../../CONST'; import Performance from '../../../../libs/Performance'; -import withDrawerState from '../../../../components/withDrawerState'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions'; -import compose from '../../../../libs/compose'; import sidebarPropTypes from './sidebarPropTypes'; const propTypes = { @@ -60,7 +58,6 @@ class BaseSidebarScreen extends Component { insets={insets} onAvatarClick={this.navigateToSettings} isSmallScreenWidth={this.props.isSmallScreenWidth} - isDrawerOpen={this.props.isDrawerOpen} reportIDFromRoute={this.props.reportIDFromRoute} onLayout={this.props.onLayout} /> @@ -75,4 +72,4 @@ class BaseSidebarScreen extends Component { BaseSidebarScreen.propTypes = propTypes; -export default compose(withWindowDimensions, withDrawerState)(BaseSidebarScreen); +export default withWindowDimensions(BaseSidebarScreen); diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 10b40b26034d..4b05411c412c 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -20,7 +20,6 @@ import ONYXKEYS from '../../../../ONYXKEYS'; import withNavigation from '../../../../components/withNavigation'; import * as Welcome from '../../../../libs/actions/Welcome'; import withNavigationFocus from '../../../../components/withNavigationFocus'; -import withDrawerState from '../../../../components/withDrawerState'; import * as TaskUtils from '../../../../libs/actions/Task'; import * as Session from '../../../../libs/actions/Session'; @@ -103,11 +102,6 @@ class FloatingActionButtonAndPopover extends React.Component { * @return {Boolean} */ didScreenBecomeInactive(prevProps) { - // When the Drawer gets closed and ReportScreen is shown - if (!this.props.isDrawerOpen && prevProps.isDrawerOpen) { - return true; - } - // When any other page is opened over LHN if (!this.props.isFocused && prevProps.isFocused) { return true; @@ -116,32 +110,11 @@ class FloatingActionButtonAndPopover extends React.Component { return false; } - /** - * Check if LHN is inactive. - * Used to prevent FAB menu showing after opening any other pages. - * - * @return {Boolean} - */ - isScreenInactive() { - // When drawer is closed and Report page is open - if (this.props.isSmallScreenWidth && !this.props.isDrawerOpen) { - return true; - } - - // When any other page is open - if (!this.props.isFocused) { - return true; - } - - return false; - } - /** * Method called when we click the floating action button */ showCreateMenu() { - if (this.isScreenInactive()) { - // Prevent showing menu when click FAB icon quickly after opening other pages + if (!this.props.isFocused && this.props.isSmallScreenWidth) { return; } this.setState({ @@ -279,7 +252,7 @@ export default compose( withLocalize, withNavigation, withNavigationFocus, - withDrawerState, + withWindowDimensions, withWindowDimensions, withOnyx({ allPolicies: { diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js index 552b0e2dc180..e93c20ad9815 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.js +++ b/src/pages/home/sidebar/SidebarScreen/index.js @@ -2,6 +2,8 @@ import React, {useRef} from 'react'; import sidebarPropTypes from './sidebarPropTypes'; import BaseSidebarScreen from './BaseSidebarScreen'; import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; +import FreezeWrapper from '../../../../libs/Navigation/FreezeWrapper'; +import withWindowDimensions from '../../../../components/withWindowDimensions'; const SidebarScreen = (props) => { const popoverModal = useRef(null); @@ -21,20 +23,22 @@ const SidebarScreen = (props) => { }; return ( - - - + + + + + ); }; SidebarScreen.propTypes = sidebarPropTypes; SidebarScreen.displayName = 'SidebarScreen'; -export default SidebarScreen; +export default withWindowDimensions(SidebarScreen); diff --git a/src/pages/home/sidebar/SidebarScreen/index.native.js b/src/pages/home/sidebar/SidebarScreen/index.native.js index 7b889bea9b0c..971ea8d8f816 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.native.js +++ b/src/pages/home/sidebar/SidebarScreen/index.native.js @@ -2,17 +2,21 @@ import React from 'react'; import sidebarPropTypes from './sidebarPropTypes'; import BaseSidebarScreen from './BaseSidebarScreen'; import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; +import FreezeWrapper from '../../../../libs/Navigation/FreezeWrapper'; +import withWindowDimensions from '../../../../components/withWindowDimensions'; const SidebarScreen = (props) => ( - - - + + + + + ); SidebarScreen.propTypes = sidebarPropTypes; SidebarScreen.displayName = 'SidebarScreen'; -export default SidebarScreen; +export default withWindowDimensions(SidebarScreen); diff --git a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js index 03a597dcdf3d..df2d90f2fb64 100644 --- a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js +++ b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions'; const sidebarPropTypes = { /** reportID in the current navigation state */ @@ -6,5 +7,6 @@ const sidebarPropTypes = { /** Callback when onLayout of sidebar is called */ onLayout: PropTypes.func, + ...windowDimensionsPropTypes, }; export default sidebarPropTypes; diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 2c799bb72d92..92a737295da2 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -8,11 +8,12 @@ import ONYXKEYS from '../../ONYXKEYS'; import OptionsSelector from '../../components/OptionsSelector'; import Navigation from '../../libs/Navigation/Navigation'; import ScreenWrapper from '../../components/ScreenWrapper'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import compose from '../../libs/compose'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import * as CurrencyUtils from '../../libs/CurrencyUtils'; import {withNetwork} from '../../components/OnyxProvider'; +import ROUTES from '../../ROUTES'; import CONST from '../../CONST'; import themeColors from '../../styles/themes/default'; import * as Expensicons from '../../components/Icon/Expensicons'; @@ -149,9 +150,9 @@ class IOUCurrencySelection extends Component { {({safeAreaPaddingBottomStyle}) => ( <> - Navigation.goBack(ROUTES.getIouRequestRoute(Navigation.getTopmostReportId()))} /> Navigation.goBack(), -}; - -const ModalHeader = (props) => ( - - - {props.shouldShowBackButton && ( - - - - - - )} -
- - - Navigation.dismissModal()} - style={[styles.touchableButtonImage]} - accessibilityRole="button" - accessibilityLabel={props.translate('common.close')} - // disable hover dimming - hoverDimmingValue={1} - pressDimmingValue={0.2} - > - - - - - - -); - -ModalHeader.displayName = 'ModalHeader'; -ModalHeader.propTypes = propTypes; -ModalHeader.defaultProps = defaultProps; -export default withLocalize(ModalHeader); diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index a186d6a41054..93b1470b6179 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import TextInput from '../../components/TextInput'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import ScreenWrapper from '../../components/ScreenWrapper'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import Form from '../../components/Form'; import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; @@ -37,11 +37,11 @@ class MoneyRequestDescriptionPage extends Component { } /** - * Closes the modal and clears the description from Onyx. + * Goes back and clears the description from Onyx. */ - onCloseButtonPress() { + onBackButtonPress() { IOU.setMoneyRequestDescription(''); - Navigation.dismissModal(); + Navigation.goBack(); } /** @@ -62,11 +62,9 @@ class MoneyRequestDescriptionPage extends Component { shouldEnableMaxHeight onEntryTransitionEnd={() => this.descriptionInputRef && this.descriptionInputRef.focus()} > - { * Navigate to the previous request step if possible */ const navigateToPreviousStep = useCallback(() => { + if (currentStepIndex === 0) { + Navigation.dismissModal(); + return; + } + if (currentStepIndex <= 0 && previousStepIndex < 0) { return; } @@ -288,13 +294,12 @@ const MoneyRequestModal = (props) => { const currentStep = steps[currentStepIndex]; const moneyRequestStepIndex = _.indexOf(steps, Steps.MoneyRequestConfirm); const isEditingAmountAfterConfirm = currentStepIndex === 0 && previousStepIndex === _.indexOf(steps, Steps.MoneyRequestConfirm); + const navigateBack = isEditingAmountAfterConfirm ? () => navigateToStep(moneyRequestStepIndex) : navigateToPreviousStep; const reportID = lodashGet(props, 'route.params.reportID', ''); - const shouldShowBackButton = currentStepIndex > 0 || isEditingAmountAfterConfirm; const modalHeader = ( - navigateToStep(moneyRequestStepIndex) : navigateToPreviousStep} + onBackButtonPress={navigateBack} /> ); const amountButtonText = isEditingAmountAfterConfirm ? props.translate('common.save') : props.translate('common.next'); diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js index 19178582b6ba..d4e5cc85d3fb 100644 --- a/src/pages/iou/SplitBillDetailsPage.js +++ b/src/pages/iou/SplitBillDetailsPage.js @@ -7,7 +7,6 @@ import lodashGet from 'lodash/get'; import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; import * as OptionsListUtils from '../../libs/OptionsListUtils'; -import ModalHeader from './ModalHeader'; import ScreenWrapper from '../../components/ScreenWrapper'; import MoneyRequestConfirmationList from '../../components/MoneyRequestConfirmationList'; import personalDetailsPropType from '../personalDetailsPropType'; @@ -18,6 +17,7 @@ import reportPropTypes from '../reportPropTypes'; import withReportOrNotFound from '../home/report/withReportOrNotFound'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import CONST from '../../CONST'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; const propTypes = { /* Onyx Props */ @@ -69,14 +69,13 @@ const SplitBillDetailsPage = (props) => { const payeePersonalDetails = _.filter(participants, (participant) => participant.login === reportAction.actorEmail)[0]; const participantsExcludingPayee = _.filter(participants, (participant) => participant.login !== reportAction.actorEmail); const splitAmount = parseInt(lodashGet(reportAction, 'originalMessage.amount', 0), 10); + const splitComment = lodashGet(reportAction, 'originalMessage.comment'); + const splitCurrency = lodashGet(reportAction, 'originalMessage.currency'); return ( - + { payeePersonalDetails={payeePersonalDetails} participants={participantsExcludingPayee} iouAmount={splitAmount} + iouComment={splitComment} + iouCurrencyCode={splitCurrency} iouType={CONST.IOU.MONEY_REQUEST_TYPE.SPLIT} isReadOnly shouldShowFooter={false} diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index e86edadf5223..daa5af83bbd6 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -1,5 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; import MoneyRequestConfirmationList from '../../../components/MoneyRequestConfirmationList'; import CONST from '../../../CONST'; import optionPropTypes from '../../../components/optionPropTypes'; @@ -23,6 +25,12 @@ const propTypes = { /** IOU type */ iouType: PropTypes.string, + /** Holds data related to IOU view state, rather than the underlying IOU data. */ + iou: PropTypes.shape({ + comment: PropTypes.string, + selectedCurrencyCode: PropTypes.string, + }), + /** Can the participants be modified or not */ canModifyParticipants: PropTypes.bool, @@ -40,6 +48,10 @@ const defaultProps = { iouType: CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, canModifyParticipants: false, policyID: '', + iou: { + comment: '', + selectedCurrencyCode: CONST.CURRENCY.USD, + }, }; const MoneyRequestConfirmPage = (props) => ( @@ -47,6 +59,8 @@ const MoneyRequestConfirmPage = (props) => ( hasMultipleParticipants={props.hasMultipleParticipants} participants={props.participants} iouAmount={props.iouAmount} + iouComment={props.iou.comment} + iouCurrencyCode={props.iou.selectedCurrencyCode} onConfirm={props.onConfirm} onSendMoney={props.onSendMoney} iouType={props.iouType} @@ -61,4 +75,6 @@ MoneyRequestConfirmPage.displayName = 'MoneyRequestConfirmPage'; MoneyRequestConfirmPage.propTypes = propTypes; MoneyRequestConfirmPage.defaultProps = defaultProps; -export default MoneyRequestConfirmPage; +export default withOnyx({ + iou: {key: ONYXKEYS.IOU}, +})(MoneyRequestConfirmPage); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 632906cc408e..54e98729a8e8 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -43,7 +43,7 @@ const defaultProps = { safeAreaPaddingBottomStyle: {}, }; -const MoneyRequestParticipantsPage = (props) => { +function MoneyRequestParticipantsPage(props) { if (props.iou.loading) { return ( @@ -67,7 +67,7 @@ const MoneyRequestParticipantsPage = (props) => { iouType={props.iouType} /> ); -}; +} MoneyRequestParticipantsPage.displayName = 'IOUParticipantsPage'; MoneyRequestParticipantsPage.propTypes = propTypes; diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 79ba4bcb216a..8ec8ea447f0b 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import React from 'react'; import {View, ScrollView} from 'react-native'; -import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import styles from '../../../styles/styles'; @@ -72,11 +72,9 @@ const AboutPage = (props) => { {({safeAreaPaddingBottomStyle}) => ( <> - Navigation.navigate(ROUTES.SETTINGS)} - onCloseButtonPress={() => Navigation.dismissModal(true)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)} /> diff --git a/src/pages/settings/AppDownloadLinks.js b/src/pages/settings/AppDownloadLinks.js index c120bf68872d..3c42e4901616 100644 --- a/src/pages/settings/AppDownloadLinks.js +++ b/src/pages/settings/AppDownloadLinks.js @@ -1,8 +1,7 @@ import _ from 'underscore'; import React from 'react'; import {ScrollView} from 'react-native'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; -import Navigation from '../../libs/Navigation/Navigation'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import CONST from '../../CONST'; import * as Expensicons from '../../components/Icon/Expensicons'; import ScreenWrapper from '../../components/ScreenWrapper'; @@ -14,6 +13,8 @@ import * as Link from '../../libs/actions/Link'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; +import ROUTES from '../../ROUTES'; +import Navigation from '../../libs/Navigation/Navigation'; const propTypes = { ...withLocalizePropTypes, @@ -55,11 +56,9 @@ const AppDownloadLinksPage = (props) => { return ( - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal(true)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_ABOUT)} /> {_.map(menuItems, (item) => ( diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 4e775bcd2cd2..f2c58e5aa2ac 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -11,7 +11,7 @@ import * as Session from '../../libs/actions/Session'; import ONYXKEYS from '../../ONYXKEYS'; import Tooltip from '../../components/Tooltip'; import Avatar from '../../components/Avatar'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import Navigation from '../../libs/Navigation/Navigation'; import * as Expensicons from '../../components/Icon/Expensicons'; import ScreenWrapper from '../../components/ScreenWrapper'; @@ -314,10 +314,7 @@ class InitialSettingsPage extends React.Component { {({safeAreaPaddingBottomStyle}) => ( <> - Navigation.dismissModal(true)} - /> + - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal(true)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_SECURITY)} /> {!_.isEmpty(this.props.account.success) ? ( - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal(true)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_PAYMENTS)} /> { const [payPalMeUsername, setPayPalMeUsername] = useState(''); @@ -45,11 +45,9 @@ const AddPayPalMePage = (props) => { return ( payPalMeInput.current && payPalMeInput.current.focus()}> - Navigation.navigate(ROUTES.SETTINGS_PAYMENTS)} - onCloseButtonPress={() => Navigation.dismissModal(true)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_PAYMENTS)} /> @@ -71,8 +69,10 @@ const AddPayPalMePage = (props) => { /> {props.translate('addPayPalMePage.checkListOf')} - Linking.openURL('https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies')} > @@ -91,7 +91,7 @@ const AddPayPalMePage = (props) => { /> - + diff --git a/src/pages/settings/Payments/ChooseTransferAccountPage.js b/src/pages/settings/Payments/ChooseTransferAccountPage.js index 6fe32df5af15..224b727bf4e5 100644 --- a/src/pages/settings/Payments/ChooseTransferAccountPage.js +++ b/src/pages/settings/Payments/ChooseTransferAccountPage.js @@ -1,7 +1,7 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; import {View} from 'react-native'; -import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import ScreenWrapper from '../../../components/ScreenWrapper'; import Navigation from '../../../libs/Navigation/Navigation'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; @@ -53,11 +53,9 @@ const ChooseTransferAccountPage = (props) => { return ( - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal()} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_PAYMENTS_TRANSFER_BALANCE)} /> (method.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD ? count + 1 : count), - 0, - ); - if (defaultablePaymentMethodCount <= 1) { - return null; - } + const defaultablePaymentMethodCount = _.filter( + filteredPaymentMethods, + (method) => method.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD, + ).length; + return defaultablePaymentMethodCount > 1; +} - return this.props.translate('paymentMethodList.defaultPaymentMethod'); - } +/** + * @param {String} actionPaymentMethodType + * @param {String|Number} activePaymentMethodID + * @param {String} paymentMethod + * @return {Boolean} + */ +function isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod) { + return paymentMethod.accountType === actionPaymentMethodType && paymentMethod.methodID === activePaymentMethodID; +} +function PaymentMethodList(props) { + const {actionPaymentMethodType, activePaymentMethodID, bankAccountList, cardList, filterType, network, onPress, payPalMeData, shouldShowSelectedState, selectedMethodID, translate} = + props; - /** - * @returns {Array} - */ - getFilteredPaymentMethods() { + const filteredPaymentMethods = useMemo(() => { // Hide any billing cards that are not P2P debit cards for now because you cannot make them your default method, or delete them - const filteredCardList = _.filter(this.props.cardList, (card) => card.accountData.additionalData.isP2PDebitCard); - let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(this.props.bankAccountList, filteredCardList, this.props.payPalMeData); + const filteredCardList = _.filter(cardList, (card) => card.accountData.additionalData.isP2PDebitCard); + let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList, filteredCardList, payPalMeData); - if (!_.isEmpty(this.props.filterType)) { - combinedPaymentMethods = _.filter(combinedPaymentMethods, (paymentMethod) => paymentMethod.accountType === this.props.filterType); + if (!_.isEmpty(filterType)) { + combinedPaymentMethods = _.filter(combinedPaymentMethods, (paymentMethod) => paymentMethod.accountType === filterType); } - if (!this.props.network.isOffline) { + if (!network.isOffline) { combinedPaymentMethods = _.filter( combinedPaymentMethods, (paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(paymentMethod.errors), @@ -136,44 +155,23 @@ class PaymentMethodList extends Component { combinedPaymentMethods = _.map(combinedPaymentMethods, (paymentMethod) => ({ ...paymentMethod, - onPress: (e) => this.props.onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID), - iconFill: this.isPaymentMethodActive(paymentMethod) ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null, - wrapperStyle: this.isPaymentMethodActive(paymentMethod) ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null, + onPress: (e) => onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID), + iconFill: isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod) ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null, + wrapperStyle: isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod) + ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] + : null, disabled: paymentMethod.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, })); return combinedPaymentMethods; - } - - /** - * Dismisses the error on the payment method - * @param {Object} item - */ - dismissError(item) { - const paymentList = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.CARD_LIST; - const paymentID = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? lodashGet(item, ['accountData', 'bankAccountID'], '') : lodashGet(item, ['accountData', 'fundID'], ''); - - if (!paymentID) { - Log.info('Unable to clear payment method error: ', item); - return; - } - - if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - PaymentMethods.clearDeletePaymentMethodError(paymentList, paymentID); - } else { - PaymentMethods.clearAddPaymentMethodError(paymentList, paymentID); - } - } + }, [actionPaymentMethodType, activePaymentMethodID, bankAccountList, cardList, filterType, network, onPress, payPalMeData]); /** - * @param {Object} paymentMethod - * @param {String|Number} paymentMethod.methodID - * @param {String} paymentMethod.accountType - * @return {Boolean} + * Render placeholder when there are no payments methods + * + * @return {React.Component} */ - isPaymentMethodActive(paymentMethod) { - return paymentMethod.accountType === this.props.actionPaymentMethodType && paymentMethod.methodID === this.props.activePaymentMethodID; - } + const renderListEmptyComponent = useCallback(() => {translate('paymentMethodList.addFirstPaymentMethod')}, [translate]); /** * Create a menuItem for each passed paymentMethod @@ -183,10 +181,10 @@ class PaymentMethodList extends Component { * * @return {React.Component} */ - renderItem({item}) { - return ( + const renderItem = useCallback( + ({item}) => ( this.dismissError(item)} + onClose={() => dismissError(item)} pendingAction={item.pendingAction} errors={item.errors} errorRowStyles={styles.ph6} @@ -200,59 +198,50 @@ class PaymentMethodList extends Component { iconFill={item.iconFill} iconHeight={item.iconSize} iconWidth={item.iconSize} - badgeText={this.getDefaultBadgeText(item.isDefault)} + badgeText={shouldShowDefaultBadge(filteredPaymentMethods, item.isDefault) ? translate('paymentMethodList.defaultPaymentMethod') : null} wrapperStyle={item.wrapperStyle} - shouldShowSelectedState={this.props.shouldShowSelectedState} - isSelected={this.props.selectedMethodID === item.methodID} + shouldShowSelectedState={shouldShowSelectedState} + isSelected={selectedMethodID === item.methodID} /> - ); - } - - /** - * Show add first payment copy when payment methods are - * - * @return {React.Component} - */ - renderListEmptyComponent() { - return {this.props.translate('paymentMethodList.addFirstPaymentMethod')}; - } - - render() { - return ( - <> - item.key} - ListEmptyComponent={this.renderListEmptyComponent()} - ListHeaderComponent={this.props.listHeaderComponent} - /> - {this.props.shouldShowAddPaymentMethodButton && ( - - {(isOffline) => ( -