diff --git a/.github/.eslintrc.js b/.github/.eslintrc.js index d1f75405f7a2..41fc57fb9829 100644 --- a/.github/.eslintrc.js +++ b/.github/.eslintrc.js @@ -7,5 +7,6 @@ module.exports = { 'no-await-in-loop': 'off', 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], 'no-continue': 'off', + 'no-restricted-imports': 'off', }, }; diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 9b4b5dac69f5..612a2457d630 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -17017,6 +17017,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index cfbe438022a0..7bdbafc0b722 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12258,6 +12258,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index e0cb48b0a9c4..74cd1509fbfa 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11541,6 +11541,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index d9fa61b08448..6e7237e7cd93 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -14353,6 +14353,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index c1cca94b6cbd..82bf90ef6d2b 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11502,6 +11502,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts index c15036c93232..b9d01702e66e 100644 --- a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts +++ b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.ts @@ -19,26 +19,32 @@ async function run() { // eslint-disable-next-line @typescript-eslint/naming-convention workflow_id: 'platformDeploy.yml', status: 'completed', - event: isProductionDeploy ? 'release' : 'push', }) ).data.workflow_runs // Note: we filter out cancelled runs instead of looking only for success runs // because if a build fails on even one platform, then it will have the status 'failure' .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); - // Find the most recent deploy workflow for which at least one of the build jobs finished successfully. + // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); while ( - lastSuccessfulDeploy && - !( - await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + lastSuccessfulDeploy?.head_branch && + (( + await GithubUtils.octokit.repos.getReleaseByTag({ owner: github.context.repo.owner, repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', + tag: lastSuccessfulDeploy.head_branch, }) - ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success') + ).data.prerelease === isProductionDeploy || + !( + await GithubUtils.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: lastSuccessfulDeploy.id, + filter: 'latest', + }) + ).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) ) { console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); lastSuccessfulDeploy = completedDeploys.shift(); diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 3173dd2358eb..05ae086fcc24 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11514,21 +11514,25 @@ async function run() { // eslint-disable-next-line @typescript-eslint/naming-convention workflow_id: 'platformDeploy.yml', status: 'completed', - event: isProductionDeploy ? 'release' : 'push', })).data.workflow_runs // Note: we filter out cancelled runs instead of looking only for success runs // because if a build fails on even one platform, then it will have the status 'failure' .filter((workflowRun) => workflowRun.conclusion !== 'cancelled'); - // Find the most recent deploy workflow for which at least one of the build jobs finished successfully. + // Find the most recent deploy workflow targeting the correct environment, for which at least one of the build jobs finished successfully let lastSuccessfulDeploy = completedDeploys.shift(); - while (lastSuccessfulDeploy && - !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + while (lastSuccessfulDeploy?.head_branch && + ((await GithubUtils_1.default.octokit.repos.getReleaseByTag({ owner: github.context.repo.owner, repo: github.context.repo.repo, - // eslint-disable-next-line @typescript-eslint/naming-convention - run_id: lastSuccessfulDeploy.id, - filter: 'latest', - })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success')) { + tag: lastSuccessfulDeploy.head_branch, + })).data.prerelease === isProductionDeploy || + !(await GithubUtils_1.default.octokit.actions.listJobsForWorkflowRun({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // eslint-disable-next-line @typescript-eslint/naming-convention + run_id: lastSuccessfulDeploy.id, + filter: 'latest', + })).data.jobs.some((job) => job.name.startsWith('Build and deploy') && job.conclusion === 'success'))) { console.log(`Deploy was not a success: ${lastSuccessfulDeploy.html_url}, looking at the next one`); lastSuccessfulDeploy = completedDeploys.shift(); } @@ -11636,6 +11640,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index aff2a13da163..7b7ff20ef426 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -2769,6 +2769,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index f1c2054cca1d..8580842b380c 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11604,6 +11604,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index 19acda9b7474..9e823e8da5ae 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11502,6 +11502,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 06d569d6fb5a..9f97e4a72d20 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -6555,6 +6555,1174 @@ function isPlainObject(o) { exports.isPlainObject = isPlainObject; +/***/ }), + +/***/ 5902: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var hashClear = __nccwpck_require__(1789), + hashDelete = __nccwpck_require__(712), + hashGet = __nccwpck_require__(5395), + hashHas = __nccwpck_require__(5232), + hashSet = __nccwpck_require__(7320); + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +module.exports = Hash; + + +/***/ }), + +/***/ 6608: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var listCacheClear = __nccwpck_require__(9792), + listCacheDelete = __nccwpck_require__(7716), + listCacheGet = __nccwpck_require__(5789), + listCacheHas = __nccwpck_require__(9386), + listCacheSet = __nccwpck_require__(7399); + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +module.exports = ListCache; + + +/***/ }), + +/***/ 881: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getNative = __nccwpck_require__(4479), + root = __nccwpck_require__(9882); + +/* Built-in method references that are verified to be native. */ +var Map = getNative(root, 'Map'); + +module.exports = Map; + + +/***/ }), + +/***/ 938: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var mapCacheClear = __nccwpck_require__(1610), + mapCacheDelete = __nccwpck_require__(6657), + mapCacheGet = __nccwpck_require__(1372), + mapCacheHas = __nccwpck_require__(609), + mapCacheSet = __nccwpck_require__(5582); + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +module.exports = MapCache; + + +/***/ }), + +/***/ 9213: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Built-in value references. */ +var Symbol = root.Symbol; + +module.exports = Symbol; + + +/***/ }), + +/***/ 6752: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var eq = __nccwpck_require__(1901); + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +module.exports = assocIndexOf; + + +/***/ }), + +/***/ 7497: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Symbol = __nccwpck_require__(9213), + getRawTag = __nccwpck_require__(923), + objectToString = __nccwpck_require__(4200); + +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); +} + +module.exports = baseGetTag; + + +/***/ }), + +/***/ 411: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isFunction = __nccwpck_require__(7799), + isMasked = __nccwpck_require__(9058), + isObject = __nccwpck_require__(3334), + toSource = __nccwpck_require__(6928); + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used for built-in method references. */ +var funcProto = Function.prototype, + objectProto = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +module.exports = baseIsNative; + + +/***/ }), + +/***/ 8380: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var root = __nccwpck_require__(9882); + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +module.exports = coreJsData; + + +/***/ }), + +/***/ 2085: +/***/ ((module) => { + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +module.exports = freeGlobal; + + +/***/ }), + +/***/ 9980: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var isKeyable = __nccwpck_require__(3308); + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +module.exports = getMapData; + + +/***/ }), + +/***/ 4479: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseIsNative = __nccwpck_require__(411), + getValue = __nccwpck_require__(3542); + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +module.exports = getNative; + + +/***/ }), + +/***/ 923: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Symbol = __nccwpck_require__(9213); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; +} + +module.exports = getRawTag; + + +/***/ }), + +/***/ 3542: +/***/ ((module) => { + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +module.exports = getValue; + + +/***/ }), + +/***/ 1789: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; +} + +module.exports = hashClear; + + +/***/ }), + +/***/ 712: +/***/ ((module) => { + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; +} + +module.exports = hashDelete; + + +/***/ }), + +/***/ 5395: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; +} + +module.exports = hashGet; + + +/***/ }), + +/***/ 5232: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); +} + +module.exports = hashHas; + + +/***/ }), + +/***/ 7320: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var nativeCreate = __nccwpck_require__(3041); + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} + +module.exports = hashSet; + + +/***/ }), + +/***/ 3308: +/***/ ((module) => { + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +module.exports = isKeyable; + + +/***/ }), + +/***/ 9058: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var coreJsData = __nccwpck_require__(8380); + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +module.exports = isMasked; + + +/***/ }), + +/***/ 9792: +/***/ ((module) => { + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; + this.size = 0; +} + +module.exports = listCacheClear; + + +/***/ }), + +/***/ 7716: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** Used for built-in method references. */ +var arrayProto = Array.prototype; + +/** Built-in value references. */ +var splice = arrayProto.splice; + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; +} + +module.exports = listCacheDelete; + + +/***/ }), + +/***/ 5789: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +module.exports = listCacheGet; + + +/***/ }), + +/***/ 9386: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +module.exports = listCacheHas; + + +/***/ }), + +/***/ 7399: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var assocIndexOf = __nccwpck_require__(6752); + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +module.exports = listCacheSet; + + +/***/ }), + +/***/ 1610: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Hash = __nccwpck_require__(5902), + ListCache = __nccwpck_require__(6608), + Map = __nccwpck_require__(881); + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; +} + +module.exports = mapCacheClear; + + +/***/ }), + +/***/ 6657: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; +} + +module.exports = mapCacheDelete; + + +/***/ }), + +/***/ 1372: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +module.exports = mapCacheGet; + + +/***/ }), + +/***/ 609: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +module.exports = mapCacheHas; + + +/***/ }), + +/***/ 5582: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getMapData = __nccwpck_require__(9980); + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; +} + +module.exports = mapCacheSet; + + +/***/ }), + +/***/ 3041: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var getNative = __nccwpck_require__(4479); + +/* Built-in method references that are verified to be native. */ +var nativeCreate = getNative(Object, 'create'); + +module.exports = nativeCreate; + + +/***/ }), + +/***/ 4200: +/***/ ((module) => { + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString.call(value); +} + +module.exports = objectToString; + + +/***/ }), + +/***/ 9882: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var freeGlobal = __nccwpck_require__(2085); + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +module.exports = root; + + +/***/ }), + +/***/ 6928: +/***/ ((module) => { + +/** Used for built-in method references. */ +var funcProto = Function.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +module.exports = toSource; + + +/***/ }), + +/***/ 1901: +/***/ ((module) => { + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +module.exports = eq; + + +/***/ }), + +/***/ 7799: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var baseGetTag = __nccwpck_require__(7497), + isObject = __nccwpck_require__(3334); + +/** `Object#toString` result references. */ +var asyncTag = '[object AsyncFunction]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + proxyTag = '[object Proxy]'; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; +} + +module.exports = isFunction; + + +/***/ }), + +/***/ 3334: +/***/ ((module) => { + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +module.exports = isObject; + + +/***/ }), + +/***/ 9885: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var MapCache = __nccwpck_require__(938); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ +function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; +} + +// Expose `MapCache`. +memoize.Cache = MapCache; + +module.exports = memoize; + + /***/ }), /***/ 467: @@ -11500,6 +12668,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); /* eslint-disable @typescript-eslint/naming-convention, import/no-import-module-exports */ const core = __importStar(__nccwpck_require__(2186)); const github_1 = __nccwpck_require__(5438); +const memoize_1 = __importDefault(__nccwpck_require__(9885)); const ActionUtils = __importStar(__nccwpck_require__(6981)); const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); @@ -11535,6 +12704,7 @@ async function commentPR(PR, message) { } } const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; +const getCommit = (0, memoize_1.default)(GithubUtils_1.default.octokit.git.getCommit); async function run() { const prList = ActionUtils.getJSONInput('PR_LIST', { required: true }).map((num) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true }); @@ -11573,25 +12743,11 @@ async function run() { } return; } - // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag const { data: recentTags } = await GithubUtils_1.default.octokit.repos.listTags({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, per_page: 100, }); - const currentTag = recentTags.find((tag) => tag.name === version); - if (!currentTag) { - const err = `Could not find tag matching ${version}`; - console.error(err); - core.setFailed(err); - return; - } - const { data: commit } = await GithubUtils_1.default.octokit.git.getCommit({ - owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, - commit_sha: currentTag.commit.sha, - }); - const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); for (const prNumber of prList) { /* * Determine who the deployer for the PR is. The "deployer" for staging deploys is: @@ -11604,7 +12760,28 @@ async function run() { repo: CONST_1.default.APP_REPO, pull_number: prNumber, }); - const deployer = isCP ? commit.committer.name : pr.merged_by?.login; + // Check for the CP Staging label on the issue to see if it was cherry-picked + const isCP = pr.labels.some(({ name: labelName }) => labelName === CONST_1.default.LABELS.CP_STAGING); + // Determine the deployer. For most PRs it will be whoever merged the PR. + // For CPs it will be whoever created the tag for the PR (i.e: whoever triggered the CP) + let deployer = pr.merged_by?.login; + if (isCP) { + for (const tag of recentTags) { + const { data: commit } = await getCommit({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + commit_sha: tag.commit.sha, + }); + const prNumForCPMergeCommit = commit.message.match(/Merge pull request #(\d+)[\S\s]*\(cherry picked from commit .*\)/); + if (prNumForCPMergeCommit?.at(1) === String(prNumber)) { + const cpActor = commit.message.match(/.*\(CP triggered by (.*)\)/)?.at(1); + if (cpActor) { + deployer = cpActor; + } + break; + } + } + } const title = pr.title; const deployMessage = deployer ? getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title) : ''; await commentPR(prNumber, deployMessage); @@ -11709,6 +12886,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 53018cbb035e..71a5c7d5c6ee 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -2,6 +2,7 @@ import * as core from '@actions/core'; import {context} from '@actions/github'; import type {RequestError} from '@octokit/types'; +import memoize from 'lodash/memoize'; import * as ActionUtils from '@github/libs/ActionUtils'; import CONST from '@github/libs/CONST'; import GithubUtils from '@github/libs/GithubUtils'; @@ -42,6 +43,8 @@ async function commentPR(PR: number, message: string) { const workflowURL = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; +const getCommit = memoize(GithubUtils.octokit.git.getCommit); + async function run() { const prList = (ActionUtils.getJSONInput('PR_LIST', {required: true}) as string[]).map((num) => Number.parseInt(num, 10)); const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: true}) as boolean; @@ -88,25 +91,11 @@ async function run() { return; } - // First find out if this is a normal staging deploy or a CP by looking at the commit message on the tag const {data: recentTags} = await GithubUtils.octokit.repos.listTags({ owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, per_page: 100, }); - const currentTag = recentTags.find((tag) => tag.name === version); - if (!currentTag) { - const err = `Could not find tag matching ${version}`; - console.error(err); - core.setFailed(err); - return; - } - const {data: commit} = await GithubUtils.octokit.git.getCommit({ - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - commit_sha: currentTag.commit.sha, - }); - const isCP = /[\S\s]*\(cherry picked from commit .*\)/.test(commit.message); for (const prNumber of prList) { /* @@ -120,7 +109,30 @@ async function run() { repo: CONST.APP_REPO, pull_number: prNumber, }); - const deployer = isCP ? commit.committer.name : pr.merged_by?.login; + + // Check for the CP Staging label on the issue to see if it was cherry-picked + const isCP = pr.labels.some(({name: labelName}) => labelName === CONST.LABELS.CP_STAGING); + + // Determine the deployer. For most PRs it will be whoever merged the PR. + // For CPs it will be whoever created the tag for the PR (i.e: whoever triggered the CP) + let deployer = pr.merged_by?.login; + if (isCP) { + for (const tag of recentTags) { + const {data: commit} = await getCommit({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + commit_sha: tag.commit.sha, + }); + const prNumForCPMergeCommit = commit.message.match(/Merge pull request #(\d+)[\S\s]*\(cherry picked from commit .*\)/); + if (prNumForCPMergeCommit?.at(1) === String(prNumber)) { + const cpActor = commit.message.match(/.*\(CP triggered by (.*)\)/)?.at(1); + if (cpActor) { + deployer = cpActor; + } + break; + } + } + } const title = pr.title; const deployMessage = deployer ? getDeployMessage(deployer, isCP ? 'Cherry-picked' : 'Deployed', title) : ''; diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 6c47718584ce..4f62879a4419 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11601,6 +11601,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 7f7c4ecc38ac..c14b825e1198 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -18089,6 +18089,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 75d40871926c..83131f363ef8 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11512,6 +11512,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 0cec1bc183f0..2a0977db8016 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11604,6 +11604,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 0d441b9f52d3..49a4341b84af 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11544,6 +11544,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/libs/CONST.ts b/.github/libs/CONST.ts index a46f4afc421c..499ff15e5aeb 100644 --- a/.github/libs/CONST.ts +++ b/.github/libs/CONST.ts @@ -14,6 +14,7 @@ const CONST = { DEPLOY_BLOCKER: 'DeployBlockerCash', INTERNAL_QA: 'InternalQA', HELP_WANTED: 'Help Wanted', + CP_STAGING: 'CP Staging', }, ACTIONS: { CREATED: 'created', diff --git a/.github/scripts/verifyPodfile.sh b/.github/scripts/verifyPodfile.sh index 2c9a7dee672a..ff67b11c8657 100755 --- a/.github/scripts/verifyPodfile.sh +++ b/.github/scripts/verifyPodfile.sh @@ -63,6 +63,7 @@ if ! SPEC_DIRS=$(yq '.["EXTERNAL SOURCES"].[].":path" | select( . == "*node_modu cleanupAndExit 1 fi +# Retrieve a list of podspec paths from react-native config if ! read_lines_into_array PODSPEC_PATHS < <(npx react-native config | jq --raw-output '.dependencies[].platforms.ios.podspecPath | select ( . != null)'); then error "Error: could not parse podspec paths from react-native config command" cleanupAndExit 1 diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index dd2c92e95568..1772d5d309cc 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -82,7 +82,6 @@ jobs: id: cherryPick run: | echo "Attempting to cherry-pick ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}" - git config user.name ${{ github.actor }} if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then echo "🎉 No conflicts! CP was a success, PR can be automerged 🎉" echo "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT" @@ -93,7 +92,7 @@ jobs: GIT_MERGE_AUTOEDIT=no git cherry-pick --continue echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT" fi - git config user.name OSBotify + git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})" - name: Push changes run: | @@ -122,6 +121,11 @@ jobs: env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} + - name: Label PR with CP Staging + run: gh pr edit ${{ inputs.PULL_REQUEST_NUMBER }} --add-label 'CP Staging' + env: + GITHUB_TOKEN: ${{ github.token }} + - name: "Announces a CP failure in the #announce Slack room" uses: 8398a7/action-slack@v3 if: ${{ failure() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3f5a8881f244..a20a53678712 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,11 +23,11 @@ jobs: OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} - - name: Tag version - run: git tag "$(npm run print-version --silent)" + - name: Get current app version + run: echo "STAGING_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: 🚀 Push tags to trigger staging deploy 🚀 - run: git push --tags + - name: 🚀 Create prerelease to trigger staging deploy 🚀 + run: gh release create ${{ env.STAGING_VERSION }} --title ${{ env.STAGING_VERSION }} --generate-notes --prerelease - name: Warn deployers if staging deploy failed if: ${{ failure() }} @@ -68,8 +68,8 @@ jobs: - name: Get current app version run: echo "PRODUCTION_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --title ${{ env.PRODUCTION_VERSION }} --generate-notes + - name: 🚀 Edit the release to be no longer a prerelease to deploy production 🚀 + run: gh release edit ${{ env.PRODUCTION_VERSION }} --prerelease=false --latest env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml index cb5dc6d28b32..47df9b4285b9 100644 --- a/.github/workflows/deployBlocker.yml +++ b/.github/workflows/deployBlocker.yml @@ -39,7 +39,7 @@ jobs: channel: '#expensify-open-source', attachments: [{ color: "#DB4545", - text: '💥 We have found a New Expensify Deploy Blocker, if you have any idea which PR could be causing this, please comment in the issue: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>' + text: '💥 New Deploy Blocker: <${{ github.event.issue.html_url }}|${{ env.GH_ISSUE_TITLE }}>. If you have any idea which PR could be causing this, please comment in the issue.' }] } env: diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 3cb6cd8e5d81..397122930652 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -1,15 +1,12 @@ name: Build and deploy android, desktop, iOS, and web clients -# This workflow is run when any tag is published +# This workflow is run when a release or prerelease is created on: - push: - tags: - - '*' release: - types: [created] + types: [prereleased, released] env: - SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} + SHOULD_DEPLOY_PRODUCTION: ${{ github.event.action == 'released' }} concurrency: group: ${{ github.workflow }}-${{ github.event_name }} @@ -36,7 +33,7 @@ jobs: deployChecklist: name: Create or update deploy checklist uses: ./.github/workflows/createDeployChecklist.yml - if: ${{ github.event_name != 'release' }} + if: ${{ github.event.action != 'released' }} needs: validateActor secrets: inherit @@ -226,7 +223,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd ios && bundle exec pod install + command: scripts/pod-install.sh - name: Decrypt AppStore profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 5cfe5e213d2f..e5ccdfa53076 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -4,7 +4,7 @@ name: Process new code merged to main on: push: branches: [main] - paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**, workflow_tests/**] + paths-ignore: [docs/**, contributingGuides/**, jest/**, tests/**] jobs: typecheck: diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a0489a52711b..d4a25a63952b 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] - paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, workflow_tests/**, '**.md', '**.sh'] + paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, '**.md', '**.sh'] jobs: perf-tests: diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 30a30918f4f6..42d946cece95 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -3,7 +3,7 @@ name: Send Reassure Performance Tests to Graphite on: push: branches: [main] - paths-ignore: [docs/**, contributingGuides/**, jest/**, workflow_tests/**] + paths-ignore: [docs/**, contributingGuides/**, jest/**] jobs: perf-tests: @@ -36,7 +36,7 @@ jobs: - name: Get and save graphite string id: saveGraphiteString uses: ./.github/actions/javascript/getGraphiteString - with: + with: PR_NUMBER: ${{ steps.getMergedPullRequest.outputs.number }} - name: Send graphite data diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 024f5b712a3f..da4225f0e4be 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -184,7 +184,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd ios && bundle exec pod install --verbose + command: scripts/pod-install.sh - name: Decrypt AdHoc profile run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AdHoc.mobileprovision NewApp_AdHoc.mobileprovision.gpg diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml deleted file mode 100644 index f65319f14be4..000000000000 --- a/.github/workflows/testGithubActionsWorkflows.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Test GitHub Actions workflows - -on: - workflow_dispatch: - workflow_call: - pull_request: - types: [opened, reopened, synchronize] - branches-ignore: [staging, production] - paths: ['.github/**'] - -jobs: - testGHWorkflows: - if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} - runs-on: ubuntu-latest - env: - CI: true - name: test GitHub Workflows - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Node - uses: Expensify/App/.github/actions/composite/setupNode@main - - - name: Setup Homebrew - uses: Homebrew/actions/setup-homebrew@master - - - name: Login to GitHub Container Regstry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: OSBotify - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Act - run: brew install act - - - name: Set ACT_BINARY - run: echo "ACT_BINARY=$(which act)" >> "$GITHUB_ENV" - - - name: Run tests - run: npm run workflow-test diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 3bfc0ed28d1a..32c9e35315b3 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -34,7 +34,7 @@ jobs: # - git diff is used to see the files that were added on this branch # - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main # - wc counts the words in the result of the intersection - count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) + count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) if [ "$count_new_js" -gt "0" ]; then echo "ERROR: Found new JavaScript files in the project; use TypeScript instead." exit 1 diff --git a/.gitignore b/.gitignore index aa6aad4cc429..e33ec43a01fe 100644 --- a/.gitignore +++ b/.gitignore @@ -110,10 +110,6 @@ tsconfig.tsbuildinfo # Mock-github /repo/ -# Workflow test logs -/workflow_tests/logs/ -/workflow_tests/repo/ - # Yalc .yalc yalc.lock diff --git a/android/app/build.gradle b/android/app/build.gradle index 80148aa2b981..9eb4d45f390e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -108,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009001803 - versionName "9.0.18-3" + versionCode 1009001900 + versionName "9.0.19-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/cards-and-domains.svg b/assets/images/cards-and-domains.svg deleted file mode 100644 index a6a3918f6423..000000000000 --- a/assets/images/cards-and-domains.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 0ddaafda2d82..61baec9d9f1c 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -57,7 +57,7 @@ The 168 hours (aka 7 days) will be measured by calculating the time between when A job could be fixing a bug or working on a new feature. There are two ways you can find a job that you can contribute to: #### Finding a job that Expensify posted -This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. +This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/nx/search/jobs/?nbs=1&q=expensify%20react%20native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. >**Note:** Our problem solving approach at Expensify is to focus on high value problems and avoid small optimizations with results that are difficult to measure. We also prefer to identify and solve problems at their root. Given that, please ensure all proposed jobs fix a specific problem in a measurable way with evidence so they are easy to evaluate. Here's an example of a good problem/solution: > diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md index 191d49b046dd..787602337bd2 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -9,7 +9,7 @@ description: Coming Soon When a report exports successfully, a message is posted in the expense’s related chat room: -QBO_help_01 +![Confirmation message posted in the expense chat room](https://help.expensify.com/assets/images/QBO_help_01.png){:width="100%"} ## What happens if I manually export a report that has already been exported? diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md index 1dbf43c4854f..b132a75e9297 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md @@ -17,16 +17,15 @@ An error on a report will prevent it from automatically exporting. To resolve this, open the expense and make the required changes. Then an admin must manually export the report to QuickBooks Online by clicking on Details > Export: -QBO_help_02 +![Click the Export button found in the Details tab](https://help.expensify.com/assets/images/QBO_help_02.png){:width="100%"} -QBO_help_03 +![Select QuickBooks Online in the Export tab](https://help.expensify.com/assets/images/QBO_help_03.png){:width="100%"} ## Unable to manually export a report To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking “Export” will lead to an empty page, as the data is not yet available for export: -QBO_help_04 - +![If the Report is in the Open status, the Not Ready to Export message shows](https://help.expensify.com/assets/images/QBO_help_04.png){:width="100%"} ### How to resolve: diff --git a/docs/articles/new-expensify/connections/xero/Configure-Xero.md b/docs/articles/new-expensify/connections/xero/Configure-Xero.md index 0c65db1b4fd9..218e81c98707 100644 --- a/docs/articles/new-expensify/connections/xero/Configure-Xero.md +++ b/docs/articles/new-expensify/connections/xero/Configure-Xero.md @@ -3,4 +3,23 @@ title: Configure Xero description: Coming soon --- -# Coming soon +# FAQ + +## How do I know if a report successfully exported to Xero? + +When a report exports successfully, a message is posted in the related Expensify Chat room. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_01.png){:width="100%"} + +## What happens if I manually export a report that has already been exported? + +When an admin manually exports a report, Expensify will warn them if the report has already been exported. If the admin chooses to export it again, it will create a duplicate report in Xero. You will need to delete the duplicate entries from within Xero. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_05.png){:width="100%"} + +## What happens to existing reports that have already been approved and reimbursed if I enable Auto Sync? + +- If Auto Sync was disabled when your Workspace was linked to Xero, enabling it won’t impact existing reports that haven’t been exported. +- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in Xero during the next sync. +- If a report has been exported and marked as paid in Xero, it will be automatically marked as reimbursed in Expensify during the next sync. +- If a report has not yet been exported to Xero, it won’t be automatically exported. diff --git a/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting b/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting new file mode 100644 index 000000000000..0c69493f3935 --- /dev/null +++ b/docs/articles/new-expensify/connections/xero/Xero-Troubleshooting @@ -0,0 +1,37 @@ +--- +title: Xero Troubleshooting +description: A list of common Xero errors and how to resolve them +--- + +## Report won’t automatically export to Xero + +If an error occurs during an automatic export to Xero: + +- You’ll receive an email detailing the error. +- The error will appear in the related Workspace Chat, indicated by a red dot next to the report. +- For auto-sync errors, a message will be posted in the related #admins room. The message contains a link to the workspace’s accounting settings where an explanation for the error appears next to the connection. + +An error on a report will prevent it from automatically exporting. + +## How to resolve + +Open the expense and make the required changes. Then an admin must manually export the report to Xero by clicking the heading at the top of the expense and selecting Export. Then they’ll select Xero. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_02.png){:width="100%"} + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_03.png){:width="100%"} + +## Unable to manually export a report + +To export a report, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, clicking Export will lead to a notification that the data is not yet available for export. + +![Insert alt text for accessibility here]({{site.url}}/assets/images/Xero_help_04.png){:width="100%"} + +## How to resolve + +Open the report and make the required changes: + +- If the report is in the Open status, ensure that it is submitted. +- If the report is in the Processing status, an admin or approver will need to approve it. + +Once complete, an admin must manually export the report to Xero by clicking the heading at the top of the expense and selecting Export. Then they’ll select Xero. diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md index df112259edbb..f06c436449eb 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md @@ -10,7 +10,9 @@ To export your expense data to a CSV, 1. Click the **[Search](https://new.expensify.com/search/all?sortBy=date&sortOrder=desc)** tab in the bottom left menu. 2. Select the checkbox to the left of the expenses or reports you wish to export. - 3. Click **# selected** at the top-right and select **Download**. + 3. Click **# selected** at the top-right and select **Download**. + +![Select the expenses to download]({{site.url}}/assets/images/Export-Expenses.png){:width="100%"} The CSV download will save locally to your device with the file naming prefix _“Expensify.”_ This file provides the following data for each expense: - Date diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png new file mode 100644 index 000000000000..3dcf92d028ab Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png new file mode 100644 index 000000000000..cafb106e897e Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png new file mode 100644 index 000000000000..08b553857110 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_CreateWorkspace_3.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_1.png b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png new file mode 100644 index 000000000000..cba73c2ce150 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_2.png b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png new file mode 100644 index 000000000000..e09b8ac5b2b0 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_InviteMembers_3.png b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png new file mode 100644 index 000000000000..999e6785ae5f Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_InviteMembers_3.png differ diff --git a/docs/assets/images/Xero_help_01.png b/docs/assets/images/Xero_help_01.png new file mode 100644 index 000000000000..ce05ea83c925 Binary files /dev/null and b/docs/assets/images/Xero_help_01.png differ diff --git a/docs/assets/images/Xero_help_02.png b/docs/assets/images/Xero_help_02.png new file mode 100644 index 000000000000..c2d556c7aed0 Binary files /dev/null and b/docs/assets/images/Xero_help_02.png differ diff --git a/docs/assets/images/Xero_help_03.png b/docs/assets/images/Xero_help_03.png new file mode 100644 index 000000000000..30616ffd3d64 Binary files /dev/null and b/docs/assets/images/Xero_help_03.png differ diff --git a/docs/assets/images/Xero_help_04.png b/docs/assets/images/Xero_help_04.png new file mode 100644 index 000000000000..d0e950d3968a Binary files /dev/null and b/docs/assets/images/Xero_help_04.png differ diff --git a/docs/assets/images/Xero_help_05.png b/docs/assets/images/Xero_help_05.png new file mode 100644 index 000000000000..be65e9c62960 Binary files /dev/null and b/docs/assets/images/Xero_help_05.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c1a7e4cd5141..3a62b6e6ad7d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.18 + 9.0.19 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.18.3 + 9.0.19.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 247216432c35..edd672a2685c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.18 + 9.0.19 CFBundleSignature ???? CFBundleVersion - 9.0.18.3 + 9.0.19.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 234508f8ab16..b14950632d1d 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.18 + 9.0.19 CFBundleVersion - 9.0.18.3 + 9.0.19.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4776096973bc..f5c825e63868 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1871,7 +1871,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.107): + - RNLiveMarkdown (0.1.111): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1889,9 +1889,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.107) + - RNLiveMarkdown/common (= 0.1.111) - Yoga - - RNLiveMarkdown/common (0.1.107): + - RNLiveMarkdown/common (0.1.111): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2614,7 +2614,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: f0c641a0bcf5fdea3ec1bb52a64b30ff88d25c1f + RNLiveMarkdown: cf2707e6050a3548bde4f66bd752d721f91e8ab6 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3 diff --git a/package-lock.json b/package-lock.json index d57c0e5c1f56..efc714e4b60e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.18-3", + "version": "9.0.19-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.18-3", + "version": "9.0.19-0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.107", + "@expensify/react-native-live-markdown": "^0.1.111", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.61", + "expensify-common": "2.0.64", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -102,7 +102,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.57", + "react-native-onyx": "2.0.64", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -3951,9 +3951,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.107", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.107.tgz", - "integrity": "sha512-0Yhqo1azCu3cTmzv/KkILZX2yPiyFUZNRx+AdMdT18pMxpqTAuBtFV4HM44rlimmpT3vgwQ1F/0C0AfRAk5dZA==", + "version": "0.1.111", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.111.tgz", + "integrity": "sha512-oBRKAGA6Cv+e/D+Z5YduKL7jnD0RJC26SSyUDNMfj11Y3snG0ayi4+XKjVtfbEor9Qb/54WxM8QgEAolxcZ7Xg==", "workspaces": [ "parser", "example", @@ -25942,9 +25942,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.61", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.61.tgz", - "integrity": "sha512-X900glu2M/m2ggF9xlYlrrihNiwYN6cscYi7WmWp1yGzhGe5VFT+w033doJD1I8JLygtkZoV/xVMY4Porexrxw==", + "version": "2.0.64", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.64.tgz", + "integrity": "sha512-+P9+SMPlY799b2l4A3LQ1dle+KvJXcZ01vAFxIDHni4L2Gc1QyddPKLejbwjOrkGqgl3muoR9cwuX/o+QYlYxA==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -37345,16 +37345,16 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.57.tgz", - "integrity": "sha512-+/XndOz9kjCvUAYltq6wJbTsPcof+FZz6eFx0cpu/cDEHaYpjNoPWRKhWgWewg5wTYwu7SWl9aYSShRGVUsZWg==", + "version": "2.0.64", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.64.tgz", + "integrity": "sha512-RFYiEQBFw9610iTGLXIZ1nQMWuf8VyVEMqiRMLpao75+VnbD6lzh0z7Uuj1eoKMDkjeXJhsPP3rh2MkLnqruug==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=20.14.0", + "node": ">=20.15.1", "npm": ">=10.7.0" }, "peerDependencies": { diff --git a/package.json b/package.json index b8a636b02707..e6ac30712a9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.18-3", + "version": "9.0.19-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -14,7 +14,7 @@ "clean": "npx react-native clean-project-auto", "android": "scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev --active-arch-only", "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"", - "pod-install": "cd ios && bundle exec pod install", + "pod-install": "scripts/pod-install.sh", "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", @@ -59,8 +59,6 @@ "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts", "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", - "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", - "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.ts", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", "e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/", "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", @@ -71,7 +69,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.107", + "@expensify/react-native-live-markdown": "^0.1.111", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -113,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.61", + "expensify-common": "2.0.64", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -160,7 +158,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.57", + "react-native-onyx": "2.0.64", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", diff --git a/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch b/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch deleted file mode 100644 index 0107cb7eb660..000000000000 --- a/patches/react-native+0.73.4+024+Add-onPaste-to-TextInput.patch +++ /dev/null @@ -1,674 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 55b770d..6d86715 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -@@ -464,6 +464,21 @@ export type NativeProps = $ReadOnly<{| - |}>, - >, - -+ /** -+ * Invoked when the user performs the paste action. -+ */ -+ onPaste?: ?DirectEventHandler< -+ $ReadOnly<{| -+ target: Int32, -+ items: $ReadOnlyArray< -+ $ReadOnly<{| -+ type: string, -+ data: string, -+ |}>, -+ >, -+ |}>, -+ >, -+ - /** - * The string that will be rendered before text input has been entered. - */ -@@ -668,6 +683,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { - topScroll: { - registrationName: 'onScroll', - }, -+ topPaste: { -+ registrationName: 'onPaste', -+ }, - }, - validAttributes: { - maxFontSizeMultiplier: true, -@@ -719,6 +737,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { - textBreakStrategy: true, - onScroll: true, - onContentSizeChange: true, -+ onPaste: true, - disableFullscreenUI: true, - includeFontPadding: true, - fontWeight: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index 8e60c9e..ee383a4 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -@@ -97,6 +97,9 @@ const RCTTextInputViewConfig = { - topChangeSync: { - registrationName: 'onChangeSync', - }, -+ topPaste: { -+ registrationName: 'onPaste', -+ }, - topClear: { - registrationName: 'onClear', - }, -@@ -165,6 +168,7 @@ const RCTTextInputViewConfig = { - onSelectionChange: true, - onContentSizeChange: true, - onScroll: true, -+ onPaste: true, - onChangeSync: true, - onKeyPressSync: true, - onTextInput: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -index 26a477f..280cbe2 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -@@ -483,6 +483,16 @@ export interface TextInputTextInputEventData { - range: {start: number; end: number}; - } - -+/** -+ * @see TextInputProps.onPaste -+ */ -+export interface TextInputPasteEventData extends TargetedEvent { -+ items: Array<{ -+ type: string; -+ data: string; -+ }>; -+} -+ - /** - * @see https://reactnative.dev/docs/textinput#props - */ -@@ -811,6 +821,13 @@ export interface TextInputProps - | ((e: NativeSyntheticEvent) => void) - | undefined; - -+ /** -+ * Invoked when the user performs the paste action. -+ */ -+ onPaste?: -+ | ((e: NativeSyntheticEvent) => void) -+ | undefined; -+ - /** - * The string that will be rendered before text input has been entered - */ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -index 9adbfe9..b46437d 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -@@ -94,6 +94,18 @@ export type EditingEvent = SyntheticEvent< - |}>, - >; - -+export type PasteEvent = SyntheticEvent< -+ $ReadOnly<{| -+ target: number, -+ items: $ReadOnlyArray< -+ $ReadOnly<{| -+ type: string, -+ data: string, -+ |}>, -+ >, -+ |}>, -+>; -+ - type DataDetectorTypesType = - | 'phoneNumber' - | 'link' -@@ -796,6 +808,11 @@ export type Props = $ReadOnly<{| - */ - onScroll?: ?(e: ScrollEvent) => mixed, - -+ /** -+ * Invoked when the user performs the paste action. -+ */ -+ onPaste?: ?(e: PasteEvent) => mixed, -+ - /** - * The string that will be rendered before text input has been entered. - */ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 346acaa..3ab56a2 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -132,6 +132,18 @@ export type EditingEvent = SyntheticEvent< - |}>, - >; - -+export type PasteEvent = SyntheticEvent< -+ $ReadOnly<{| -+ target: number, -+ items: $ReadOnlyArray< -+ $ReadOnly<{| -+ type: string, -+ data: string, -+ |}>, -+ >, -+ |}>, -+>; -+ - type DataDetectorTypesType = - | 'phoneNumber' - | 'link' -diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm -index 582b49c..ac31cb1 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm -@@ -13,6 +13,9 @@ - #import - #import - -+#import -+#import -+ - @implementation RCTUITextView { - UILabel *_placeholderView; - UITextView *_detachedTextView; -@@ -166,7 +169,30 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO - - (void)paste:(id)sender - { - _textWasPasted = YES; -- [super paste:sender]; -+ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; -+ if (clipboard.hasImages) { -+ for (NSItemProvider *itemProvider in [clipboard itemProviders]) { -+ if ([itemProvider canLoadObjectOfClass:[UIImage class]]) { -+ NSString *identifier = itemProvider.registeredTypeIdentifiers.firstObject; -+ if (identifier != nil) { -+ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); -+ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); -+ NSString *fileName = [NSString stringWithFormat:@"%@.%@", itemProvider.suggestedName ?: @"file", fileExtension]; -+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; -+ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; -+ NSData *fileData = [clipboard dataForPasteboardType:identifier]; -+ [fileData writeToFile:filePath atomically:YES]; -+ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; -+ } -+ break; -+ } -+ } -+ } else { -+ if (clipboard.hasStrings) { -+ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; -+ } -+ [super paste:sender]; -+ } - } - - // Turn off scroll animation to fix flaky scrolling. -@@ -258,6 +284,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender - return NO; - } - -+ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { -+ return YES; -+ } -+ - return [super canPerformAction:action withSender:sender]; - } - -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h -index 7187177..748c4cc 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h -@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN - - (void)textInputDidChange; - - - (void)textInputDidChangeSelection; -+- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data; - - @optional - -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h -index f1c32e6..0ce9dfe 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h -@@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN - - - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; - - (void)selectedTextRangeWasSet; -+- (void)didPaste:(NSString *)type withData:(NSString *)data; - - @end - -@@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN - - (instancetype)initWithTextView:(UITextView *)backedTextInputView; - - - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; -+- (void)didPaste:(NSString *)type withData:(NSString *)data; - - @end - -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm -index 9dca6a5..bc43ab8 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm -@@ -147,6 +147,11 @@ - (void)selectedTextRangeWasSet - [self textFieldProbablyDidChangeSelection]; - } - -+- (void)didPaste:(NSString *)type withData:(NSString *)data -+{ -+ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; -+} -+ - #pragma mark - Generalization - - - (void)textFieldProbablyDidChangeSelection -@@ -302,4 +307,9 @@ - (void)textViewProbablyDidChangeSelection - [_backedTextInputView.textInputDelegate textInputDidChangeSelection]; - } - -+- (void)didPaste:(NSString *)type withData:(NSString *)data -+{ -+ [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; -+} -+ - @end -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h -index 209947d..5092dbd 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h -@@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN - @property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync; - @property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput; - @property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll; -+@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; - - @property (nonatomic, assign) NSInteger mostRecentEventCount; - @property (nonatomic, assign, readonly) NSInteger nativeEventCount; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm -index b0d71dc..2e42fc9 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm -@@ -562,6 +562,26 @@ - (void)textInputDidChangeSelection - }); - } - -+- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data -+{ -+ if (!_onPaste) { -+ return; -+ } -+ -+ NSMutableArray *items = [NSMutableArray new]; -+ [items addObject:@{ -+ @"type" : type, -+ @"data" : data, -+ }]; -+ -+ NSDictionary *payload = @{ -+ @"target" : self.reactTag, -+ @"items" : items, -+ }; -+ -+ _onPaste(payload); -+} -+ - - (void)updateLocalData - { - [self enforceTextAttributesIfNeeded]; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm -index 4785987..16a9b8e 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm -@@ -67,6 +67,7 @@ @implementation RCTBaseTextInputViewManager { - RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) - RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock) - RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) -+RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock) - - RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) - -diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -index 4d0afd9..a6afc7b 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -+++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -@@ -12,6 +12,9 @@ - #import - #import - -+#import -+#import -+ - @implementation RCTUITextField { - RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; - NSDictionary *_defaultTextAttributes; -@@ -139,6 +142,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender - return NO; - } - -+ if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { -+ return YES; -+ } -+ - return [super canPerformAction:action withSender:sender]; - } - -@@ -204,7 +211,30 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO - - (void)paste:(id)sender - { - _textWasPasted = YES; -- [super paste:sender]; -+ UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; -+ if (clipboard.hasImages) { -+ for (NSItemProvider *itemProvider in [clipboard itemProviders]) { -+ if ([itemProvider canLoadObjectOfClass:[UIImage class]]) { -+ NSString *identifier = itemProvider.registeredTypeIdentifiers.firstObject; -+ if (identifier != nil) { -+ NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); -+ NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); -+ NSString *fileName = [NSString stringWithFormat:@"%@.%@", itemProvider.suggestedName ?: @"file", fileExtension]; -+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; -+ NSURL *fileURL = [NSURL fileURLWithPath:filePath]; -+ NSData *fileData = [clipboard dataForPasteboardType:identifier]; -+ [fileData writeToFile:filePath atomically:YES]; -+ [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; -+ } -+ break; -+ } -+ } -+ } else { -+ if (clipboard.hasStrings) { -+ [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; -+ } -+ [super paste:sender]; -+ } - } - - #pragma mark - Layout -diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -index 70754bf..3ab2c6a 100644 ---- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm -@@ -426,6 +426,13 @@ - (void)textInputDidChangeSelection - } - } - -+- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data -+{ -+ if (_eventEmitter) { -+ static_cast(*_eventEmitter).onPaste(std::string([type UTF8String]), std::string([data UTF8String])); -+ } -+} -+ - #pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate) - - - (void)scrollViewDidScroll:(UIScrollView *)scrollView -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java -new file mode 100644 -index 0000000..62f7e35 ---- /dev/null -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.java -@@ -0,0 +1,17 @@ -+/* -+ * Copyright (c) Meta Platforms, Inc. and affiliates. -+ * -+ * This source code is licensed under the MIT license found in the -+ * LICENSE file in the root directory of this source tree. -+ */ -+ -+package com.facebook.react.views.textinput; -+ -+/** -+ * Implement this interface to be informed of paste event in the -+ * ReactTextEdit This is used by the ReactTextInputManager to forward events -+ * from the EditText to JS -+ */ -+interface PasteWatcher { -+ public void onPaste(String type, String data); -+} -\ No newline at end of file -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -index 081f2b8..ff91d47 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -@@ -9,14 +9,17 @@ package com.facebook.react.views.textinput; - - import static com.facebook.react.uimanager.UIManagerHelper.getReactContext; - --import android.content.ClipData; - import android.content.ClipboardManager; -+import android.content.ClipData; -+import android.content.ClipDescription; -+import android.content.ContentResolver; - import android.content.Context; - import android.graphics.Color; - import android.graphics.Paint; - import android.graphics.Rect; - import android.graphics.Typeface; - import android.graphics.drawable.Drawable; -+import android.net.Uri; - import android.os.Build; - import android.os.Bundle; - import android.text.Editable; -@@ -110,6 +113,7 @@ public class ReactEditText extends AppCompatEditText { - private @Nullable SelectionWatcher mSelectionWatcher; - private @Nullable ContentSizeWatcher mContentSizeWatcher; - private @Nullable ScrollWatcher mScrollWatcher; -+ private @Nullable PasteWatcher mPasteWatcher; - private InternalKeyListener mKeyListener; - private boolean mDetectScrollMovement = false; - private boolean mOnKeyPress = false; -@@ -153,6 +157,7 @@ public class ReactEditText extends AppCompatEditText { - mKeyListener = new InternalKeyListener(); - } - mScrollWatcher = null; -+ mPasteWatcher = null; - mTextAttributes = new TextAttributes(); - - applyTextAttributes(); -@@ -307,10 +312,31 @@ public class ReactEditText extends AppCompatEditText { - */ - @Override - public boolean onTextContextMenuItem(int id) { -- if (id == android.R.id.paste) { -+ if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - id = android.R.id.pasteAsPlainText; -- } else { -+ if (mPasteWatcher != null) { -+ ClipboardManager clipboardManager = -+ (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); -+ ClipData clipData = clipboardManager.getPrimaryClip(); -+ String type = null; -+ String data = null; -+ if (clipData.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { -+ type = ClipDescription.MIMETYPE_TEXT_PLAIN; -+ data = clipData.getItemAt(0).getText().toString(); -+ } else { -+ Uri itemUri = clipData.getItemAt(0).getUri(); -+ if (itemUri != null) { -+ ContentResolver cr = getReactContext(this).getContentResolver(); -+ type = cr.getType(itemUri); -+ data = itemUri.toString(); -+ } -+ } -+ if (type != null && data != null) { -+ mPasteWatcher.onPaste(type, data); -+ } -+ } -+ } else if (id == android.R.id.paste) { - ClipboardManager clipboard = - (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData previousClipData = clipboard.getPrimaryClip(); -@@ -389,6 +415,10 @@ public class ReactEditText extends AppCompatEditText { - mScrollWatcher = scrollWatcher; - } - -+ public void setPasteWatcher(PasteWatcher pasteWatcher) { -+ mPasteWatcher = pasteWatcher; -+ } -+ - /** - * Attempt to set a selection or fail silently. Intentionally meant to handle bad inputs. - * EventCounter is the same one used as with text. -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index 53e5c49..26dc163 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -277,6 +277,9 @@ public class ReactTextInputManager extends BaseViewManager { -+ -+ private static final String EVENT_NAME = "topPaste"; -+ -+ private String mType; -+ private String mData; -+ -+ @Deprecated -+ public ReactTextInputPasteEvent(int viewId, String type, String data) { -+ this(ViewUtil.NO_SURFACE_ID, viewId, type, data); -+ } -+ -+ public ReactTextInputPasteEvent(int surfaceId, int viewId, String type, String data) { -+ super(surfaceId, viewId); -+ mType = type; -+ mData = data; -+ } -+ -+ @Override -+ public String getEventName() { -+ return EVENT_NAME; -+ } -+ -+ @Override -+ public boolean canCoalesce() { -+ return false; -+ } -+ -+ @Nullable -+ @Override -+ protected WritableMap getEventData() { -+ WritableMap eventData = Arguments.createMap(); -+ -+ WritableArray items = Arguments.createArray(); -+ WritableMap item = Arguments.createMap(); -+ item.putString("type", mType); -+ item.putString("data", mData); -+ items.pushMap(item); -+ -+ eventData.putArray("items", items); -+ -+ return eventData; -+ } -+} -\ No newline at end of file -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp -index 1c10b11..de51df9 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp -@@ -198,6 +198,19 @@ void TextInputEventEmitter::onScroll( - }); - } - -+void TextInputEventEmitter::onPaste(const std::string& type, const std::string& data) const { -+ dispatchEvent("onPaste", [type, data](jsi::Runtime& runtime) { -+ auto payload = jsi::Object(runtime); -+ auto items = jsi::Array(runtime, 1); -+ auto item = jsi::Object(runtime); -+ item.setProperty(runtime, "type", type); -+ item.setProperty(runtime, "data", data); -+ items.setValueAtIndex(runtime, 0, item); -+ payload.setProperty(runtime, "items", items); -+ return payload; -+ }); -+} -+ - void TextInputEventEmitter::dispatchTextInputEvent( - const std::string& name, - const TextInputMetrics& textInputMetrics, -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h -index bc5e624..07ccabc 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h -@@ -48,6 +48,7 @@ class TextInputEventEmitter : public ViewEventEmitter { - void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; - void onKeyPressSync(const KeyPressMetrics& keyPressMetrics) const; - void onScroll(const TextInputMetrics& textInputMetrics) const; -+ void onPaste(const std::string& type, const std::string& data) const; - - private: - void dispatchTextInputEvent( diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh new file mode 100755 index 000000000000..cb2976d64587 --- /dev/null +++ b/scripts/pod-install.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# This script ensures pod installs respect Podfile.lock as the source of truth. +# Specifically, the podspecs for pods listed under the 'EXTERNAL SOURCES' key in the Podfile.lock are cached in the `ios/Pods/Local Podspecs` directory. +# While caching results in significantly faster installs, if a cached podspec doesn't match the version in Podfile.lock, pod install will fail. +# To prevent this, this script will find and delete any mismatched cached podspecs before running pod install + +# Exit immediately if any command exits with a non-zero status +set -e + +# Go to project root +START_DIR="$(pwd)" +ROOT_DIR="$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)")" +cd "$ROOT_DIR" || exit 1 + +# Cleanup and exit +# param - status code +function cleanupAndExit { + cd "$START_DIR" || exit 1 + exit "$1" +} + +source scripts/shellUtils.sh + +# Check if bundle is installed +if ! bundle --version > /dev/null 2>&1; then + error 'bundle is not installed. Please install bundle and try again' + cleanupAndExit 1 +fi + +# Check if jq is installed +if ! jq --version > /dev/null 2>&1; then + error 'jq is not installed. Please install jq and try again' + cleanupAndExit 1 +fi + +# Check if yq is installed +if ! yq --version > /dev/null 2>&1; then + error 'yq is not installed. Please install yq and try again' + cleanupAndExit 1 +fi + +CACHED_PODSPEC_DIR='ios/Pods/Local Podspecs' +if [ -d "$CACHED_PODSPEC_DIR" ]; then + info "Verifying pods from Podfile.lock match local podspecs..." + + # Convert podfile.lock to json since yq is missing some features of jq (namely, if/else) + PODFILE_LOCK_AS_JSON="$(yq -o=json ios/Podfile.lock)" + + # Retrieve a list of pods and their versions from Podfile.lock + declare PODS_FROM_LOCKFILE + if ! read_lines_into_array PODS_FROM_LOCKFILE < <(jq -r '.PODS | map (if (.|type) == "object" then keys[0] else . end) | .[]' < <(echo "$PODFILE_LOCK_AS_JSON")); then + error "Error: Could not parse pod versions from Podfile.lock" + cleanupAndExit 1 + fi + + for CACHED_PODSPEC_PATH in "$CACHED_PODSPEC_DIR"/*; do + if [ -f "$CACHED_PODSPEC_PATH" ]; then + # The next two lines use bash parameter expansion to get just the pod name from the path + # i.e: `ios/Pods/Local Podspecs/hermes-engine.podspec.json` to just `hermes-engine` + # It extracts the part of the string between the last `/` and the first `.` + CACHED_POD_NAME="${CACHED_PODSPEC_PATH##*/}" + CACHED_POD_NAME="${CACHED_POD_NAME%%.*}" + + info "🫛 Verifying local pod $CACHED_POD_NAME" + CACHED_POD_VERSION="$(jq -r '.version' < <(cat "$CACHED_PODSPEC_PATH"))" + for POD_FROM_LOCKFILE in "${PODS_FROM_LOCKFILE[@]}"; do + # Extract the pod name and version that was parsed from the lockfile. POD_FROM_LOCKFILE looks like `PodName (version)` + IFS=' ' read -r POD_NAME_FROM_LOCKFILE POD_VERSION_FROM_LOCKFILE <<< "$POD_FROM_LOCKFILE" + if [[ "$CACHED_POD_NAME" == "$POD_NAME_FROM_LOCKFILE" ]]; then + if [[ "$POD_VERSION_FROM_LOCKFILE" != "($CACHED_POD_VERSION)" ]]; then + clear_last_line + info "⚠️ found mismatched pod: $CACHED_POD_NAME, removing local podspec $CACHED_PODSPEC_PATH" + rm "$CACHED_PODSPEC_PATH" + echo -e "\n" + fi + break + fi + done + clear_last_line + fi + done +fi + +cd ios || cleanupAndExit 1 +bundle exec pod install + +# Go back to where we started +cleanupAndExit 0 diff --git a/scripts/shellUtils.sh b/scripts/shellUtils.sh index fa44f2ee7d3a..c1ceace09d0a 100644 --- a/scripts/shellUtils.sh +++ b/scripts/shellUtils.sh @@ -41,6 +41,11 @@ function title { printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET" } +# Function to clear the last printed line +clear_last_line() { + echo -ne "\033[1A\033[K" +} + function assert_equal { if [[ "$1" != "$2" ]]; then error "Assertion failed: $1 is not equal to $2" diff --git a/src/CONST.ts b/src/CONST.ts index b2748765f54d..bfafa031dd10 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -970,6 +970,8 @@ const CONST = { HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', + SWITCH_REPORT_FROM_PREVIEW: 'switch_report_from_preview', + SWITCH_REPORT_THREAD: 'switch_report_thread', SIDEBAR_LOADED: 'sidebar_loaded', LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 0a14d18a2324..edcdabed9101 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -174,8 +174,10 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s if (isHEIC && targetAssetUri) { manipulateAsync(targetAssetUri, [], {format: SaveFormat.JPEG}) .then((manipResult) => { + const uri = manipResult.uri; const convertedAsset = { - uri: manipResult.uri, + uri, + name: uri.substring(uri.lastIndexOf('/') + 1).split('?')[0], type: 'image/jpeg', width: manipResult.width, height: manipResult.height, diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index c932c5d798f7..68a8c56c4df9 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,9 +1,8 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; import React, {useCallback, useMemo, useRef} from 'react'; -import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native'; +import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData} from 'react-native'; import {StyleSheet} from 'react-native'; -import type {FileObject} from '@components/AttachmentModal'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; @@ -21,7 +20,6 @@ const excludeReportMentionStyle: Array = ['mentionReport']; function Composer( { onClear: onClearProp = () => {}, - onPasteFile = () => {}, isDisabled = false, maxLines, isComposerFullSize = false, @@ -71,19 +69,6 @@ function Composer( }, [onClearProp], ); - const pasteFile = useCallback( - (e: NativeSyntheticEvent) => { - const clipboardContent = e.nativeEvent.items[0]; - if (clipboardContent.type === 'text/plain') { - return; - } - const fileURI = clipboardContent.data; - const fileName = fileURI.split('/').pop(); - const file: FileObject = {uri: fileURI, name: fileName, type: clipboardContent.type}; - onPasteFile(file); - }, - [onPasteFile], - ); const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]); @@ -105,7 +90,6 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} - onPaste={pasteFile} onBlur={(e) => { if (!isFocused) { // eslint-disable-next-line react-compiler/react-compiler diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 8287e2de3f2d..e6d8a882f3b8 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,5 +1,4 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; -import type {FileObject} from '@components/AttachmentModal'; type TextSelection = { start: number; @@ -38,7 +37,7 @@ type ComposerProps = Omit & { onChangeText?: (numberOfLines: string) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file: FileObject) => void; + onPasteFile?: (file: File) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types diff --git a/src/components/ConnectToNetSuiteButton/index.tsx b/src/components/ConnectToNetSuiteButton/index.tsx index 928bc01f12c1..24c19689da23 100644 --- a/src/components/ConnectToNetSuiteButton/index.tsx +++ b/src/components/ConnectToNetSuiteButton/index.tsx @@ -1,12 +1,12 @@ import React, {useRef, useState} from 'react'; import type {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {removePolicyConnection} from '@libs/actions/connections'; @@ -15,7 +15,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {isControlPolicy} from '@libs/PolicyUtils'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ConnectToNetSuiteButtonProps} from './types'; @@ -23,7 +22,7 @@ function ConnectToNetSuiteButton({policyID, shouldDisconnectIntegrationBeforeCon const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const policy = usePolicy(policyID); const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); diff --git a/src/components/ConnectToSageIntacctButton/index.tsx b/src/components/ConnectToSageIntacctButton/index.tsx index 6c6523ad6e75..fa6b26216c4c 100644 --- a/src/components/ConnectToSageIntacctButton/index.tsx +++ b/src/components/ConnectToSageIntacctButton/index.tsx @@ -1,12 +1,12 @@ import React, {useRef, useState} from 'react'; import type {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {removePolicyConnection} from '@libs/actions/connections'; @@ -15,7 +15,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {isControlPolicy} from '@libs/PolicyUtils'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {PolicyConnectionName} from '@src/types/onyx/Policy'; @@ -30,7 +29,7 @@ function ConnectToSageIntacctButton({policyID, shouldDisconnectIntegrationBefore const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const policy = usePolicy(policyID); const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 1efa526355f1..178267de8429 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -23,7 +23,6 @@ function EmptyStateComponent({ subtitle, headerStyles, headerContentStyles, - emptyStateForegroundStyles, minModalHeight = 400, }: EmptyStateComponentProps) { const styles = useThemeStyles(); @@ -85,8 +84,7 @@ function EmptyStateComponent({ shouldAnimate={false} /> - - + {HeaderComponent} diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts index 94860893c2c4..80fac980864f 100644 --- a/src/components/EmptyStateComponent/types.ts +++ b/src/components/EmptyStateComponent/types.ts @@ -19,7 +19,6 @@ type SharedProps = { headerStyles?: StyleProp; headerMediaType: T; headerContentStyles?: StyleProp; - emptyStateForegroundStyles?: StyleProp; minModalHeight?: number; }; diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index b518acb069f3..5f7bcd7753c8 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -204,7 +204,7 @@ function FeatureTrainingModal({ {renderIllustration()} - {title && description && ( + {!!title && !!description && ( {title} {description} @@ -220,7 +220,7 @@ function FeatureTrainingModal({ onInputChange={toggleWillShowAgain} /> )} - {helpText && ( + {!!helpText && (