diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 337fe7398fb3..1dace19d0a01 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -17073,13 +17073,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -17099,6 +17097,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -17454,12 +17461,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index 0e0168fdb7ae..c5ba4d9ca936 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12314,13 +12314,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -12340,6 +12338,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -12695,12 +12702,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 842deb1cbb5d..0817a8130adc 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11597,13 +11597,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11623,6 +11621,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -11978,12 +11985,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 127fb1fe3dca..12b3165c1ead 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -14567,13 +14567,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -14593,6 +14591,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -14948,12 +14955,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index 77f61f491fec..9a7a0ae62e53 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11558,13 +11558,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11584,6 +11582,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -11939,12 +11946,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 8f9f9deea896..f63cd2a14f5b 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -11831,13 +11831,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11857,6 +11855,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -12212,12 +12219,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 14814367e3cd..7cb768941711 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11660,13 +11660,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11686,6 +11684,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -12041,12 +12048,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/getReleaseBody/index.js b/.github/actions/javascript/getReleaseBody/index.js index 6c746e26a4a4..f6ba78030927 100644 --- a/.github/actions/javascript/getReleaseBody/index.js +++ b/.github/actions/javascript/getReleaseBody/index.js @@ -11604,13 +11604,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11630,6 +11628,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -11985,12 +11992,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index f71b89dc051c..29c010f086c3 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -11558,13 +11558,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11584,6 +11582,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -11939,12 +11946,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 804d3ea610f3..1256b6425640 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -11755,13 +11755,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11781,6 +11779,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -12136,12 +12143,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 0b8eb29f1750..fef09bc25c56 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11657,13 +11657,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11683,6 +11681,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -12038,12 +12045,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index d4341ce37dc4..b211f301eb67 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11568,13 +11568,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11594,6 +11592,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -11949,12 +11956,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index cc1c0b5a581b..b53863ef0c26 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11660,13 +11660,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11686,6 +11684,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -12041,12 +12048,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index aea35331b1d0..e3507f897933 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11600,13 +11600,11 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); class GithubUtils { static internalOctokit; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token) { const Octokit = utils_1.GitHub.plugin(plugin_throttling_1.throttling, plugin_paginate_rest_1.paginateRest); - const token = core.getInput('GITHUB_TOKEN', { required: true }); // Save a copy of octokit used in this class this.internalOctokit = new Octokit((0, utils_1.getOctokitOptions)(token, { throttle: { @@ -11626,6 +11624,15 @@ class GithubUtils { }, })); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', { required: true }); + this.initOctokitWithToken(token); + } /** * Either give an existing instance of Octokit rest or create a new one * @@ -11981,12 +11988,31 @@ class GithubUtils { .then((events) => events.filter((event) => event.event === 'closed')) .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName) { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName) { + return this.octokit.actions + .listArtifactsForRepo({ owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, - per_page: 100, - }).then((artifacts) => artifacts.find((artifact) => artifact.name === artefactName)); + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId) { + return this.octokit.actions + .downloadArtifact({ + owner: CONST_1.default.GITHUB_OWNER, + repo: CONST_1.default.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } exports["default"] = GithubUtils; diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index f445fc368559..73553cb46bff 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -65,13 +65,11 @@ class GithubUtils { static internalOctokit: InternalOctokit | undefined; /** - * Initialize internal octokit - * - * @private + * Initialize internal octokit. + * NOTE: When using GithubUtils in CI, you don't need to call this manually. */ - static initOctokit() { + static initOctokitWithToken(token: string) { const Octokit = GitHub.plugin(throttling, paginateRest); - const token = core.getInput('GITHUB_TOKEN', {required: true}); // Save a copy of octokit used in this class this.internalOctokit = new Octokit( @@ -96,6 +94,16 @@ class GithubUtils { ); } + /** + * Default initialize method assuming running in CI, getting the token from an input. + * + * @private + */ + static initOctokit() { + const token = core.getInput('GITHUB_TOKEN', {required: true}); + this.initOctokitWithToken(token); + } + /** * Either give an existing instance of Octokit rest or create a new one * @@ -521,12 +529,32 @@ class GithubUtils { .then((closedEvents) => closedEvents.at(-1)?.actor?.login ?? ''); } - static getArtifactByName(artefactName: string): Promise { - return this.paginate(this.octokit.actions.listArtifactsForRepo, { - owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, - per_page: 100, - }).then((artifacts: OctokitArtifact[]) => artifacts.find((artifact) => artifact.name === artefactName)); + /** + * Returns a single artifact by name. If none is found, it returns undefined. + */ + static getArtifactByName(artifactName: string): Promise { + return this.octokit.actions + .listArtifactsForRepo({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + per_page: 1, + name: artifactName, + }) + .then((response) => response.data.artifacts[0]); + } + + /** + * Given an artifact ID, returns the download URL to a zip file containing the artifact. + */ + static getArtifactDownloadURL(artifactId: number): Promise { + return this.octokit.actions + .downloadArtifact({ + owner: CONST.GITHUB_OWNER, + repo: CONST.APP_REPO, + artifact_id: artifactId, + archive_format: 'zip', + }) + .then((response) => response.url); } } diff --git a/.github/scripts/verifyPodfile.sh b/.github/scripts/verifyPodfile.sh index 0d04d8f1b3ed..f4b1c5521733 100755 --- a/.github/scripts/verifyPodfile.sh +++ b/.github/scripts/verifyPodfile.sh @@ -39,8 +39,8 @@ fi info "Ensuring correct version of cocoapods is used..." -POD_VERSION_REGEX='([[:digit:]]+\.[[:digit:]]+)(\.[[:digit:]]+)?'; -POD_VERSION_FROM_GEMFILE="$(sed -nr "s/gem \"cocoapods\", \"~> $POD_VERSION_REGEX\"/\1/p" Gemfile)" +POD_VERSION_REGEX='([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+)?'; +POD_VERSION_FROM_GEMFILE="$(sed -nr "s/gem \"cocoapods\", \"= $POD_VERSION_REGEX\"/\1/p" Gemfile)" info "Pod version from Gemfile: $POD_VERSION_FROM_GEMFILE" POD_VERSION_FROM_PODFILE_LOCK="$(sed -nr "s/COCOAPODS: $POD_VERSION_REGEX/\1/p" ios/Podfile.lock)" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a792d069151b..624c00de6831 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -65,6 +65,6 @@ jobs: PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --notes ${{ steps.getReleaseBody.outputs.RELEASE_BODY }} + run: gh release create ${{ env.PRODUCTION_VERSION }} --generate-notes env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 20bf0d257a9b..640d1eaa1172 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -96,7 +96,7 @@ jobs: - name: Archive Android sourcemaps uses: actions/upload-artifact@v3 with: - name: android-sourcemap + name: android-sourcemap-${{ github.ref_name }} path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map - name: Upload Android version to GitHub artifacts @@ -248,7 +248,7 @@ jobs: - name: Archive iOS sourcemaps uses: actions/upload-artifact@v3 with: - name: ios-sourcemap + name: ios-sourcemap-${{ github.ref_name }} path: main.jsbundle.map - name: Upload iOS version to GitHub artifacts @@ -372,8 +372,11 @@ jobs: needs: validateActor if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) && github.event_name == 'push' }} steps: + - name: Checkout + uses: actions/checkout@v4 + - name: 'Deploy HybridApp' - run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true + run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true -f build_version="$(npm run print-version --silent)" env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} diff --git a/.gitignore b/.gitignore index dcbec8a96e46..aa6aad4cc429 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ config/webpack/*.pem .expo dist/ web-build/ + +# Storage location for downloaded app source maps (see scripts/symbolicate-profile.ts) +.sourcemaps/ diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 7527857eeda7..ec8e17dda4cf 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -16,7 +16,7 @@ import './fonts.css'; Onyx.init({ keys: ONYXKEYS, initialKeyStates: { - [ONYXKEYS.NETWORK]: {isOffline: false, isBackendReachable: true}, + [ONYXKEYS.NETWORK]: {isOffline: false}, }, }); diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d4b708ccea4d..0b93611527c5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -57,3 +57,5 @@ compliance with this Code is required to maintain your status as an Expensify co By signing up to participate as an contributor, you are acknowledging your understanding of and consent to (i) what is expected of you under this Code and (ii) notwithstanding anything to the contrary in any agreement you have with Expensify, Expensify’s right, but not obligation, to terminate your participation in the Expensify Contributor Community upon any breach of the Code, as determined in Expensify’s sole Discretion. + +Violations of our two rules may lead to removal from the contributor program. Severe violations can lead to an immediate ban, while lesser ones may result in a formal warning. Multiple warnings will also lead to removal. diff --git a/Gemfile b/Gemfile index 751e05d2d32b..5227deb80865 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source "https://rubygems.org" # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version ruby ">= 2.6.10" -gem "cocoapods", "~> 1.13" +gem "cocoapods", "= 1.15.2" gem "activesupport", ">= 6.1.7.3", "< 7.1.0" gem "fastlane", "~> 2" gem "xcpretty", "~> 0" diff --git a/Gemfile.lock b/Gemfile.lock index e1b4fc2ec7e4..3780235053ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,15 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (6.1.7.7) + activesupport (7.0.8.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) @@ -16,32 +17,33 @@ GEM json (>= 1.5.1) apktools (0.7.4) rubyzip (~> 2.0) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.883.0) - aws-sdk-core (3.190.3) + aws-partitions (1.944.0) + aws-sdk-core (3.197.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.76.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.85.0) + aws-sdk-core (~> 3, >= 3.197.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-s3 (1.152.3) + aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) - cocoapods (1.13.0) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.13.0) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -54,7 +56,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.13.0) + cocoapods-core (1.15.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -65,7 +67,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -77,18 +79,17 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.3) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.109.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -117,15 +118,15 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.219.0) + fastimage (2.3.1) + fastlane (2.221.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -146,10 +147,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -158,12 +159,15 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-aws_s3 (2.1.0) apktools (~> 0.7) aws-sdk-s3 (~> 1) mime-types (~> 3.3) - ffi (1.16.3) + ffi (1.17.0) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) @@ -183,17 +187,17 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.37.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -204,44 +208,47 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) + json (2.7.2) + jwt (2.8.2) + base64 mime-types (3.5.1) mime-types-data (~> 3.2015) mime-types-data (3.2023.1003) - mini_magick (4.12.0) + mini_magick (4.13.1) mini_mime (1.1.5) - minitest (5.20.0) + minitest (5.24.0) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.1) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - optparse (0.4.0) + nkf (0.2.0) + optparse (0.5.0) os (1.1.4) plist (3.7.1) public_suffix (4.0.7) - rake (13.1.0) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.2.9) + strscan rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -249,6 +256,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -262,12 +270,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.9.1) unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -278,7 +283,6 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.13) PLATFORMS arm64-darwin-21 @@ -289,7 +293,7 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) - cocoapods (~> 1.13) + cocoapods (= 1.15.2) fastlane (~> 2) fastlane-plugin-aws_s3 xcpretty (~> 0) @@ -298,4 +302,4 @@ RUBY VERSION ruby 2.6.10p210 BUNDLED WITH - 2.3.22 + 2.4.14 diff --git a/android/app/build.gradle b/android/app/build.gradle index 9e0fcd2e0bc2..d005d82471a9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001048402 - versionName "1.4.84-2" + versionCode 1009000004 + versionName "9.0.0-4" // 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/android/build.gradle b/android/build.gradle index 52c998998ba0..9fc585ab9f05 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,7 +29,7 @@ buildscript { classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.firebase:perf-plugin:1.4.1") // Fullstory integration - classpath ("com.fullstory:gradle-plugin-local:1.47.0") + classpath ("com.fullstory:gradle-plugin-local:1.49.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/assets/images/bed.svg b/assets/images/bed.svg index fd654c036a7c..8ee733733ab2 100644 --- a/assets/images/bed.svg +++ b/assets/images/bed.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/car-with-key.svg b/assets/images/car-with-key.svg index 1586c0dfecfa..e4770fdad970 100644 --- a/assets/images/car-with-key.svg +++ b/assets/images/car-with-key.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/check-circle.svg b/assets/images/check-circle.svg index c13b83cbf281..3f6f1da4f827 100644 --- a/assets/images/check-circle.svg +++ b/assets/images/check-circle.svg @@ -1,13 +1 @@ - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/checkmark-circle.svg b/assets/images/checkmark-circle.svg index 3497548bc1bc..102598b55d8a 100644 --- a/assets/images/checkmark-circle.svg +++ b/assets/images/checkmark-circle.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/credit-card-exclamation.svg b/assets/images/credit-card-exclamation.svg index 67e686516baa..9cf946a56a5c 100644 --- a/assets/images/credit-card-exclamation.svg +++ b/assets/images/credit-card-exclamation.svg @@ -1,14 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/crosshair.svg b/assets/images/crosshair.svg index 357faab49178..66ee21774d02 100644 --- a/assets/images/crosshair.svg +++ b/assets/images/crosshair.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/emptystate__routepending.svg b/assets/images/emptystate__routepending.svg index aba08554d02f..685696f04abf 100644 --- a/assets/images/emptystate__routepending.svg +++ b/assets/images/emptystate__routepending.svg @@ -1,18 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/inbox.svg b/assets/images/inbox.svg index f9059e78ec5a..29ab7f916616 100644 --- a/assets/images/inbox.svg +++ b/assets/images/inbox.svg @@ -1,12 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/money-search.svg b/assets/images/money-search.svg index 90dedae0a2fb..72a77352f861 100644 --- a/assets/images/money-search.svg +++ b/assets/images/money-search.svg @@ -1,16 +1 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/plane.svg b/assets/images/plane.svg index bf4d56875239..635bdc4b1ed7 100644 --- a/assets/images/plane.svg +++ b/assets/images/plane.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/product-illustrations/emptystate__travel.svg b/assets/images/product-illustrations/emptystate__travel.svg index 416b27eb5bee..287f99996860 100644 --- a/assets/images/product-illustrations/emptystate__travel.svg +++ b/assets/images/product-illustrations/emptystate__travel.svg @@ -1,575 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/receipt-slash.svg b/assets/images/receipt-slash.svg index 2af3fcbc60e6..f7e7457e3e64 100644 --- a/assets/images/receipt-slash.svg +++ b/assets/images/receipt-slash.svg @@ -1,12 +1 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg index a96a7e5dc0af..66d2b1e5b0e2 100644 --- a/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg +++ b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg @@ -1,21 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg b/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg index 17ff47e6ca12..26b1ea7f2c31 100644 --- a/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg +++ b/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg @@ -1,57 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__sendmoney.svg b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg index 80393e3c30cf..1975c15d5d24 100644 --- a/assets/images/simple-illustrations/simple-illustration__sendmoney.svg +++ b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg @@ -1,72 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg index e158bc5588cb..6dcb4a422f0a 100644 --- a/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg +++ b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg @@ -1,82 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg index d70d2d1ef552..39b2e4a12542 100644 --- a/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg +++ b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__treasurechest.svg b/assets/images/simple-illustrations/simple-illustration__treasurechest.svg index 2bdee0c7e90f..51718aa5112a 100644 --- a/assets/images/simple-illustrations/simple-illustration__treasurechest.svg +++ b/assets/images/simple-illustrations/simple-illustration__treasurechest.svg @@ -1 +1,59 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/subscription-details__approvedlogo--light.svg b/assets/images/subscription-details__approvedlogo--light.svg index 580ee60c597c..6582fdf13fcd 100644 --- a/assets/images/subscription-details__approvedlogo--light.svg +++ b/assets/images/subscription-details__approvedlogo--light.svg @@ -1,91 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/subscription-details__approvedlogo.svg b/assets/images/subscription-details__approvedlogo.svg index 7722e2526657..73615be28528 100644 --- a/assets/images/subscription-details__approvedlogo.svg +++ b/assets/images/subscription-details__approvedlogo.svg @@ -1,94 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 060bc0313950..e04e589166f4 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,16 +3,13 @@ require('dotenv').config(); const IS_E2E_TESTING = process.env.E2E_TESTING === 'true'; const defaultPresets = ['@babel/preset-react', '@babel/preset-env', '@babel/preset-flow', '@babel/preset-typescript']; -const defaultPlugins = [ +let defaultPlugins = [ // Adding the commonjs: true option to react-native-web plugin can cause styling conflicts ['react-native-web'], '@babel/transform-runtime', '@babel/plugin-proposal-class-properties', - // This will serve to map the classes correctly in FullStory - '@fullstory/babel-plugin-annotate-react', - // We use `transform-class-properties` for transforming ReactNative libraries and do not use it for our own // source code transformation as we do not use class property assignment. 'transform-class-properties', @@ -21,6 +18,19 @@ const defaultPlugins = [ 'react-native-reanimated/plugin', ]; +// The Fullstory annotate plugin generated a few errors when executed in Electron. Let's +// ignore it for desktop builds. +if (!process.env.ELECTRON_ENV && process.env.npm_lifecycle_event !== 'desktop') { + console.debug('This is not a desktop build, adding babel-plugin-annotate-react'); + defaultPlugins.push([ + '@fullstory/babel-plugin-annotate-react', + { + 'react-native-web': true, + native: true, + }, + ]); +} + const webpack = { presets: defaultPresets, plugins: defaultPlugins, @@ -45,7 +55,6 @@ const metro = { '@fullstory/babel-plugin-annotate-react', { native: true, - setFSTagName: true, }, ], diff --git a/docs/articles/new-expensify/expenses/Manually-submit-reports-for-approval.md b/docs/Hidden/Manually-submit-reports-for-approval.md similarity index 100% rename from docs/articles/new-expensify/expenses/Manually-submit-reports-for-approval.md rename to docs/Hidden/Manually-submit-reports-for-approval.md diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index eb59388159bf..29b02d8aeb00 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -524,6 +524,10 @@ button { padding: 0; margin: 0; } + + li { + margin-left: 12px; + } } } } diff --git a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md index 96653bc69763..64ae3997c4f9 100644 --- a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md +++ b/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md @@ -11,6 +11,10 @@ To connect a bank account in New Expensify, you must first enable the Make or Tr 3. Click on **Workflows** 4. Enable **Make or track payments** +![Insert alt text for accessibility here]({{site.url}}/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png){:width="100%"} + +![Insert alt text for accessibility here]({{site.url}}/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png){:width="100%"} + # Step 2: Connect bank account 1. Click Connect bank account 2. Select either Connect online with Plaid (preferred) or Connect manually diff --git a/docs/articles/new-expensify/expensify-card/Add-Expensify-Card-to-Apple-or-Google-Pay.md b/docs/articles/new-expensify/expensify-card/Add-Expensify-Card-to-Apple-or-Google-Pay.md new file mode 100644 index 000000000000..844a688e0011 --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Add-Expensify-Card-to-Apple-or-Google-Pay.md @@ -0,0 +1,32 @@ +--- +title: Add Expensify Card to Apple or Google Pay +description: Pay with your Expensify Card from your Apple or Google Pay wallet +--- +
+ +You can use your Expensify Card for contactless in-person payments by adding it to your digital wallet for Apple Pay (for iOS) or Google Pay (for Android). + +{% include selector.html values="mobile" %} + +{% include option.html value="mobile" %} +**Apple Pay** + +1. Open the Apple Pay app. +2. Tap the + button. +3. Tap **Debit or Credit Card**. +4. Tap **Continue**. +5. Follow the steps provided to add your virtual card. + +**Google Pay** + +1. Open the Google Pay app. +2. Tap **Add to Wallet**. +3. Tap **Payment Card**. +4. Tap **Add new debit or credit card**. +5. Add your virtual card details. + +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png new file mode 100644 index 000000000000..3ff21c1f34cb Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png differ diff --git a/docs/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png new file mode 100644 index 000000000000..dea262434e59 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png differ diff --git a/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_1.png b/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_1.png new file mode 100644 index 000000000000..baa3f8734e8e Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_2.png b/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_2.png new file mode 100644 index 000000000000..83eaadff3bdc Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_3.png b/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_3.png new file mode 100644 index 000000000000..6b43c41c39cf Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R1_CreateWorkspace_3.png differ diff --git a/docs/assets/images/ExpensifyHelp_R1_InviteMembers_1.png b/docs/assets/images/ExpensifyHelp_R1_InviteMembers_1.png new file mode 100644 index 000000000000..0f1883bd8f69 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R1_InviteMembers_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_R1_InviteMembers_2.png b/docs/assets/images/ExpensifyHelp_R1_InviteMembers_2.png new file mode 100644 index 000000000000..c3b8c035c9ed Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R1_InviteMembers_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_R1_InviteMembers_3.png b/docs/assets/images/ExpensifyHelp_R1_InviteMembers_3.png new file mode 100644 index 000000000000..515a111847f8 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R1_InviteMembers_3.png differ diff --git a/docs/assets/images/ExpensifyHelp_R2_Profile_1.png b/docs/assets/images/ExpensifyHelp_R2_Profile_1.png new file mode 100644 index 000000000000..0419c66e7563 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R2_Profile_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_R3_Categories_1.png b/docs/assets/images/ExpensifyHelp_R3_Categories_1.png new file mode 100644 index 000000000000..d82757866d3d Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R3_Categories_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_R4_Tags_1.png b/docs/assets/images/ExpensifyHelp_R4_Tags_1.png new file mode 100644 index 000000000000..a14ea8343578 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R4_Tags_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_R4_Tags_2.png b/docs/assets/images/ExpensifyHelp_R4_Tags_2.png new file mode 100644 index 000000000000..889d845e931a Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R4_Tags_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_R5_Wallet_1.png b/docs/assets/images/ExpensifyHelp_R5_Wallet_1.png new file mode 100644 index 000000000000..64ff3730e695 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_R5_Wallet_1.png differ diff --git a/docs/assets/images/simple-illustration__monitor-remotesync.svg b/docs/assets/images/simple-illustration__monitor-remotesync.svg index e4ed84a35851..f0f6f363036e 100644 --- a/docs/assets/images/simple-illustration__monitor-remotesync.svg +++ b/docs/assets/images/simple-illustration__monitor-remotesync.svg @@ -1,30 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/docs/redirects.csv b/docs/redirects.csv index 13463327d06d..b4912a629918 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -201,3 +201,4 @@ https://help.expensify.com/articles/expensify-classic/workspaces/reports/Report- https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit,https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports https://help.expensify.com/articles/new-expensify/chat/Expensify-Chat-For-Admins,https://help.expensify.com/new-expensify/hubs/chat/ https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account +https://help.expensify.com/articles/new-expensify/expenses/Manually-submit-reports-for-approval,https://help.expensify.com/new-expensify/hubs/expenses/ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 69ef5f90dd5c..3360f1adfad7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.84 + 9.0.0 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.84.2 + 9.0.0.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 534bd3625870..796437f37280 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.84 + 9.0.0 CFBundleSignature ???? CFBundleVersion - 1.4.84.2 + 9.0.0.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5f083aea5ba8..5a4f6310d225 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.84 + 9.0.0 CFBundleVersion - 1.4.84.2 + 9.0.0.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index d72086d4c07b..6330bb3d8d52 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -118,4 +118,4 @@ target 'NotificationServiceExtension' do pod 'AirshipServiceExtension' end -pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz' \ No newline at end of file +pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz' \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b877c56a5581..ddab159714fc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,7 +138,7 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (6.2.1) - - FullStory (1.48.0) + - FullStory (1.49.0) - fullstory_react-native (1.4.2): - FullStory (~> 1.14) - glog @@ -1243,13 +1243,7 @@ PODS: - react-native-config (1.5.0): - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): - - RCT-Folly - - RCTRequired - - RCTTypeSafety - - React - - React-Codegen - - React-RCTFabric - - ReactCommon/turbomodule/core + - React-Core - react-native-document-picker (9.1.1): - RCT-Folly - RCTRequired @@ -1303,6 +1297,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-keyboard-controller (1.12.2): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-launch-arguments (4.0.2): - React - react-native-netinfo (11.2.1): @@ -1852,7 +1865,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.84): + - RNLiveMarkdown (0.1.85): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1870,9 +1883,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.84) + - RNLiveMarkdown/common (= 0.1.85) - Yoga - - RNLiveMarkdown/common (0.1.84): + - RNLiveMarkdown/common (0.1.85): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2098,7 +2111,7 @@ DEPENDENCIES: - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz\"}`)" + - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz\"}`)" - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) @@ -2137,6 +2150,7 @@ DEPENDENCIES: - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-key-command (from `../node_modules/react-native-key-command`) + - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) @@ -2263,7 +2277,7 @@ EXTERNAL SOURCES: FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" FullStory: - :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz fullstory_react-native: :path: "../node_modules/@fullstory/react-native" glog: @@ -2335,6 +2349,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-image-picker" react-native-key-command: :path: "../node_modules/react-native-key-command" + react-native-keyboard-controller: + :path: "../node_modules/react-native-keyboard-controller" react-native-launch-arguments: :path: "../node_modules/react-native-launch-arguments" react-native-netinfo: @@ -2458,7 +2474,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: FullStory: - :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz SPEC CHECKSUMS: Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 @@ -2485,7 +2501,7 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - FullStory: 097347c823c21c655ca25fd8d5e6355a9326ec54 + FullStory: c95f74445f871bc344cdc4a4e4ece61b5554e55d fullstory_react-native: 6cba8a2c054374a24a44dc4310407d9435459cae glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 @@ -2536,11 +2552,12 @@ SPEC CHECKSUMS: react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 - react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c + react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 + react-native-keyboard-controller: 47c01b0741ae5fc84e53cf282e61cfa5c2edb19b react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: 02d31de0e08ab043d48f2a1a8baade109d7b6ca5 react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa @@ -2589,7 +2606,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: bf516c02a4549a059829a3fbb8f51c2e0a3110e7 + RNLiveMarkdown: fff70dc755ed8199a449f61e76cbadec7cd20440 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 @@ -2606,8 +2623,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 -PODFILE CHECKSUM: 66a5c97ae1059e4da1993a4ad95abe5d819f555b +PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/jest/setup.ts b/jest/setup.ts index 416306ce8426..f11a8a4ed631 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -53,3 +53,6 @@ jest.mock('react-native-sound', () => { jest.mock('react-native-share', () => ({ default: jest.fn(), })); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); diff --git a/package-lock.json b/package-lock.json index 5d32d7adf2dc..2ef901f63d43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,26 @@ { "name": "new.expensify", - "version": "1.4.84-2", + "version": "9.0.0-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.84-2", + "version": "9.0.0-4", "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.84", + "@expensify/react-native-live-markdown": "0.1.85", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-annotate-react": "github:fullstorydev/fullstory-babel-plugin-annotate-react#ryanwang/react-native-web-demo", "@fullstory/babel-plugin-react-native": "^1.2.1", "@fullstory/browser": "^2.0.3", "@fullstory/react-native": "^1.4.2", @@ -59,7 +60,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "^2.0.10", + "expensify-common": "2.0.17", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -99,11 +100,12 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.8", + "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.48", + "react-native-onyx": "2.0.52", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -240,6 +242,7 @@ "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "type-fest": "^4.10.2", "typescript": "^5.4.5", "wait-port": "^0.2.9", @@ -3560,9 +3563,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.84", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.84.tgz", - "integrity": "sha512-gyRjmOozNlCCBKoQtEvohV+P4iR6VL4Z5QpuE3SXSE7J77WlCiaCMg5LjWFL8Q3Vn3ZApsQWhReLIXj3kfN9WA==", + "version": "0.1.85", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.85.tgz", + "integrity": "sha512-jeP4JBzN34pGSpjHKM7Zj3d0cqcKbID3//WrqC+SI7SK/1iJT4SdhZptVCxUg+Dcxq5XwzYIhdnhTNimeya0Fg==", "workspaces": [ "parser", "example", @@ -5587,8 +5590,7 @@ }, "node_modules/@fullstory/babel-plugin-annotate-react": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-annotate-react/-/babel-plugin-annotate-react-2.3.0.tgz", - "integrity": "sha512-gYLUL6Tu0exbvTIhK9nSCaztmqBlQAm07Fvtl/nKTc+lxwFkcX9vR8RrdTbyjJZKbPaA5EMlExQ6GeLCXkfm5g==" + "resolved": "git+ssh://git@github.com/fullstorydev/fullstory-babel-plugin-annotate-react.git#25c26dadb644d5355e381a4ea4ca1cd05af4a8f6" }, "node_modules/@fullstory/babel-plugin-react-native": { "version": "1.2.1", @@ -11103,29 +11105,6 @@ "node": ">=14.14" } }, - "node_modules/@storybook/preset-react-webpack/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@storybook/preset-react-webpack/node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@storybook/preview": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-8.0.6.tgz", @@ -12474,8 +12453,9 @@ }, "node_modules/@types/json5": { "version": "0.0.29", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, "node_modules/@types/keyv": { "version": "3.1.4", @@ -20016,6 +19996,18 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "dev": true, @@ -20024,6 +20016,27 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-import/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/eslint-plugin-jest": { "version": "24.7.0", "dev": true, @@ -20769,9 +20782,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.10.tgz", - "integrity": "sha512-+8LCtnR+VxmCjKKkfeR6XGAhVxvwZtQAw3386c1EDGNK1C0bvz3I1kLVMFbulSeibZv6/G33aO6SiW/kwum6Nw==", + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.17.tgz", + "integrity": "sha512-W7xO10/bYF/p0/cUOtzejXJDiLCB/U6JTXVltzOE70xjGgzTSyRotPkEtEItHTvXOS2Wz8jJ262nrGfFMpfisA==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -20785,8 +20798,7 @@ "react-dom": "16.12.0", "semver": "^7.6.0", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "ua-parser-js": "^1.0.37", - "underscore": "1.13.6" + "ua-parser-js": "^1.0.37" } }, "node_modules/expensify-common/node_modules/react": { @@ -31892,6 +31904,16 @@ "version": "5.0.1", "license": "MIT" }, + "node_modules/react-native-keyboard-controller": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.2.tgz", + "integrity": "sha512-10Sy0+neSHGJxOmOxrUJR8TQznnrQ+jTFQtM1PP6YnblNQeAw1eOa+lO6YLGenRr5WuNSMZbks/3Ay0e2yMKLw==", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-reanimated": ">=2.3.0" + } + }, "node_modules/react-native-launch-arguments": { "version": "4.0.2", "license": "MIT", @@ -31939,9 +31961,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.48.tgz", - "integrity": "sha512-qJQTWMzhLD7zy5/9vBZJSlb3//fYVx3obTdsw1tXZDVOZXUcBmd6evA2tzGe5KT8H2sIbvFR1UyvwE03oOqYYg==", + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.52.tgz", + "integrity": "sha512-uXlNhQg1UStx1W/U+9GYtIhLvx3vTIpN1WwE1gsiVxvimnUzKpQX/JBkgpR9b48ZoxsdiZXOT5kKLlqGCa6O1g==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -36376,25 +36398,17 @@ "license": "Apache-2.0" }, "node_modules/tsconfig-paths": { - "version": "3.15.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, - "license": "MIT", "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", + "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" }, - "bin": { - "json5": "lib/cli.js" + "engines": { + "node": ">=6" } }, "node_modules/tsconfig-paths/node_modules/strip-bom": { diff --git a/package.json b/package.json index d603b1f11f45..590972f64299 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.84-2", + "version": "9.0.0-4", "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.", @@ -53,6 +53,7 @@ "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", "symbolicate-release:ios": "scripts/release-profile.ts --platform=ios", "symbolicate-release:android": "scripts/release-profile.ts --platform=android", + "symbolicate-profile": "scripts/symbolicate-profile.ts", "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", @@ -65,13 +66,14 @@ "@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.84", + "@expensify/react-native-live-markdown": "0.1.85", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-annotate-react": "github:fullstorydev/fullstory-babel-plugin-annotate-react#ryanwang/react-native-web-demo", "@fullstory/babel-plugin-react-native": "^1.2.1", "@fullstory/browser": "^2.0.3", "@fullstory/react-native": "^1.4.2", @@ -111,7 +113,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "^2.0.10", + "expensify-common": "2.0.17", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -151,11 +153,12 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.8", + "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.48", + "react-native-onyx": "2.0.52", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -292,6 +295,7 @@ "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "type-fest": "^4.10.2", "typescript": "^5.4.5", "wait-port": "^0.2.9", diff --git a/patches/react-native+0.73.4+016+fixClippedEmojis.patch b/patches/react-native+0.73.4+016+fixClippedEmojis.patch new file mode 100644 index 000000000000..4a14a11c96c0 --- /dev/null +++ b/patches/react-native+0.73.4+016+fixClippedEmojis.patch @@ -0,0 +1,47 @@ +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +index 49a4353..c0158f3 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +@@ -39,6 +39,8 @@ facebook::react::AttributedStringBox RCTAttributedStringBoxFromNSAttributedStrin + + NSString *RCTNSStringFromStringApplyingTextTransform(NSString *string, facebook::react::TextTransform textTransform); + ++void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText); ++ + @interface RCTWeakEventEmitterWrapper : NSObject + @property (nonatomic, assign) facebook::react::SharedEventEmitter eventEmitter; + @end +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +index b3b53c8..b2d43c6 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +@@ -300,7 +300,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex + return [attributes copy]; + } + +-static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) ++void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) + { + __block CGFloat maximumLineHeight = 0; + +@@ -396,7 +396,6 @@ static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) + + [nsAttributedString appendAttributedString:nsAttributedStringFragment]; + } +- RCTApplyBaselineOffset(nsAttributedString); + [nsAttributedString endEditing]; + + return nsAttributedString; +diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +index 368c334..1f06f92 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm ++++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +@@ -261,7 +261,7 @@ - (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attri + [layoutManager addTextContainer:textContainer]; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; +- ++ RCTApplyBaselineOffset(textStorage); + [textStorage addLayoutManager:layoutManager]; + + if (paragraphAttributes.adjustsFontSizeToFit) { diff --git a/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch b/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch new file mode 100644 index 000000000000..1a5b4c40477b --- /dev/null +++ b/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch @@ -0,0 +1,70 @@ +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 88ae3f3..497569a 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 +@@ -36,6 +36,54 @@ static jsi::Value textInputMetricsPayload( + return payload; + }; + ++static jsi::Value textInputMetricsScrollPayload( ++ jsi::Runtime& runtime, ++ const TextInputMetrics& textInputMetrics) { ++ auto payload = jsi::Object(runtime); ++ ++ { ++ auto contentOffset = jsi::Object(runtime); ++ contentOffset.setProperty(runtime, "x", textInputMetrics.contentOffset.x); ++ contentOffset.setProperty(runtime, "y", textInputMetrics.contentOffset.y); ++ payload.setProperty(runtime, "contentOffset", contentOffset); ++ } ++ ++ { ++ auto contentInset = jsi::Object(runtime); ++ contentInset.setProperty(runtime, "top", textInputMetrics.contentInset.top); ++ contentInset.setProperty( ++ runtime, "left", textInputMetrics.contentInset.left); ++ contentInset.setProperty( ++ runtime, "bottom", textInputMetrics.contentInset.bottom); ++ contentInset.setProperty( ++ runtime, "right", textInputMetrics.contentInset.right); ++ payload.setProperty(runtime, "contentInset", contentInset); ++ } ++ ++ { ++ auto contentSize = jsi::Object(runtime); ++ contentSize.setProperty( ++ runtime, "width", textInputMetrics.contentSize.width); ++ contentSize.setProperty( ++ runtime, "height", textInputMetrics.contentSize.height); ++ payload.setProperty(runtime, "contentSize", contentSize); ++ } ++ ++ { ++ auto layoutMeasurement = jsi::Object(runtime); ++ layoutMeasurement.setProperty( ++ runtime, "width", textInputMetrics.layoutMeasurement.width); ++ layoutMeasurement.setProperty( ++ runtime, "height", textInputMetrics.layoutMeasurement.height); ++ payload.setProperty(runtime, "layoutMeasurement", layoutMeasurement); ++ } ++ ++ payload.setProperty(runtime, "zoomScale", textInputMetrics.zoomScale ?: 1); ++ ++ ++ return payload; ++ }; ++ + static jsi::Value textInputMetricsContentSizePayload( + jsi::Runtime& runtime, + const TextInputMetrics& textInputMetrics) { +@@ -140,7 +188,9 @@ void TextInputEventEmitter::onKeyPressSync( + + void TextInputEventEmitter::onScroll( + const TextInputMetrics& textInputMetrics) const { +- dispatchTextInputEvent("scroll", textInputMetrics); ++ dispatchEvent("scroll", [textInputMetrics](jsi::Runtime& runtime) { ++ return textInputMetricsScrollPayload(runtime, textInputMetrics); ++ }); + } + + void TextInputEventEmitter::dispatchTextInputEvent( diff --git a/patches/react-native+0.73.4+017+iOS-fix-whitespace-support-sourcemap.patch b/patches/react-native+0.73.4+017+iOS-fix-whitespace-support-sourcemap.patch new file mode 100644 index 000000000000..e8ca87026282 --- /dev/null +++ b/patches/react-native+0.73.4+017+iOS-fix-whitespace-support-sourcemap.patch @@ -0,0 +1,37 @@ +diff --git a/node_modules/react-native/scripts/react-native-xcode.sh b/node_modules/react-native/scripts/react-native-xcode.sh +index d6c382b..3e1742c 100755 +--- a/node_modules/react-native/scripts/react-native-xcode.sh ++++ b/node_modules/react-native/scripts/react-native-xcode.sh +@@ -104,7 +104,7 @@ fi + + BUNDLE_FILE="$CONFIGURATION_BUILD_DIR/main.jsbundle" + +-EXTRA_ARGS= ++EXTRA_ARGS=() + + case "$PLATFORM_NAME" in + "macosx") +@@ -131,12 +131,12 @@ if [[ $EMIT_SOURCEMAP == true ]]; then + else + PACKAGER_SOURCEMAP_FILE="$SOURCEMAP_FILE" + fi +- EXTRA_ARGS="$EXTRA_ARGS --sourcemap-output $PACKAGER_SOURCEMAP_FILE" ++ EXTRA_ARGS+=("--sourcemap-output" "$PACKAGER_SOURCEMAP_FILE") + fi + + # Hermes doesn't require JS minification. + if [[ $USE_HERMES != false && $DEV == false ]]; then +- EXTRA_ARGS="$EXTRA_ARGS --minify false" ++ EXTRA_ARGS+=("--minify" "false") + fi + + "$NODE_BINARY" $NODE_ARGS "$CLI_PATH" $BUNDLE_COMMAND \ +@@ -147,7 +147,7 @@ fi + --reset-cache \ + --bundle-output "$BUNDLE_FILE" \ + --assets-dest "$DEST" \ +- $EXTRA_ARGS \ ++ "${EXTRA_ARGS[@]}" \ + $EXTRA_PACKAGER_ARGS + + if [[ $USE_HERMES == false ]]; then diff --git a/patches/react-native-keyboard-controller+1.12.2.patch.patch b/patches/react-native-keyboard-controller+1.12.2.patch.patch new file mode 100644 index 000000000000..3c8034354481 --- /dev/null +++ b/patches/react-native-keyboard-controller+1.12.2.patch.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +index 83884d8..5d9e989 100644 +--- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt ++++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +@@ -99,12 +99,12 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R + } + + private fun goToEdgeToEdge(edgeToEdge: Boolean) { +- reactContext.currentActivity?.let { +- WindowCompat.setDecorFitsSystemWindows( +- it.window, +- !edgeToEdge, +- ) +- } ++ // reactContext.currentActivity?.let { ++ // WindowCompat.setDecorFitsSystemWindows( ++ // it.window, ++ // !edgeToEdge, ++ // ) ++ // } + } + + private fun setupKeyboardCallbacks() { +@@ -158,13 +158,13 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R + // region State managers + private fun enable() { + this.goToEdgeToEdge(true) +- this.setupWindowInsets() ++ // this.setupWindowInsets() + this.setupKeyboardCallbacks() + } + + private fun disable() { + this.goToEdgeToEdge(false) +- this.setupWindowInsets() ++ // this.setupWindowInsets() + this.removeKeyboardCallbacks() + } + // endregion \ No newline at end of file diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 000000000000..d6d39822b737 --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + rules: { + // For all these Node.js scripts, we do not want to disable `console` statements + 'no-console': 'off', + + '@lwc/lwc/no-async-await': 'off', + 'no-await-in-loop': 'off', + 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], + }, +}; diff --git a/scripts/release-profile.ts b/scripts/release-profile.ts index 8ec0979f9f9e..cfc7e2cb8838 100755 --- a/scripts/release-profile.ts +++ b/scripts/release-profile.ts @@ -3,21 +3,7 @@ /* eslint-disable no-console */ import {execSync} from 'child_process'; import fs from 'fs'; - -type ArgsMap = Record; - -// Function to parse command-line arguments into a key-value object -function parseCommandLineArguments(): ArgsMap { - const args = process.argv.slice(2); // Skip node and script paths - const argsMap: ArgsMap = {}; - args.forEach((arg) => { - const [key, value] = arg.split('='); - if (key.startsWith('--')) { - argsMap[key.substring(2)] = value; - } - }); - return argsMap; -} +import parseCommandLineArguments from './utils/parseCommandLineArguments'; // Function to find .cpuprofile files in the current directory function findCpuProfileFiles() { diff --git a/scripts/symbolicate-profile.ts b/scripts/symbolicate-profile.ts new file mode 100755 index 000000000000..a100c05029dd --- /dev/null +++ b/scripts/symbolicate-profile.ts @@ -0,0 +1,174 @@ +#!/usr/bin/env ts-node + +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * This script helps to symbolicate a .cpuprofile file that was obtained from a specific (staging) app version (usually provided by a user using the app). + * + * @abstract + * + * 1. When creating a new deployment in our github actions, we upload the source map for android and iOS as artifacts. + * 2. The profiles created by the app on the user's device have the app version encoded in the filename. + * 3. This script takes in a .cpuprofile file, reads the app version from the filename, and downloads the corresponding source map from the artifacts using github's API. + * 4. It then uses the source map to symbolicate the .cpuprofile file using the `react-native-release-profiler` cli. + * + * @note For downloading an artifact a github token is required. + */ +import {execSync} from 'child_process'; +import fs from 'fs'; +import https from 'https'; +import path from 'path'; +import GithubUtils from '@github/libs/GithubUtils'; +import * as Logger from './utils/Logger'; +import parseCommandLineArguments from './utils/parseCommandLineArguments'; + +const argsMap = parseCommandLineArguments(); + +/* ============== INPUT VALIDATION ============== */ + +if (Object.keys(argsMap).length === 0 || argsMap.help !== undefined) { + Logger.log('Symbolicates a .cpuprofile file obtained from a specific app version by downloading the source map from the github action runs.'); + Logger.log('Usage: npm run symbolicate-profile -- --profile= --platform='); + Logger.log('Options:'); + Logger.log(' --profile= The .cpuprofile file to symbolicate'); + Logger.log(' --platform= The platform for which the source map was uploaded'); + Logger.log(' --gh-token Token to use for requests send to the GitHub API. By default tries to pick up from the environment variable GITHUB_TOKEN'); + Logger.log(' --help Display this help message'); + process.exit(0); +} + +if (argsMap.profile === undefined) { + Logger.error('Please specify the .cpuprofile file to symbolicate using --profile='); + process.exit(1); +} +if (!fs.existsSync(argsMap.profile)) { + Logger.error(`File ${argsMap.profile} does not exist.`); + process.exit(1); +} + +if (argsMap.platform === undefined) { + Logger.error('Please specify the platform using --platform=ios or --platform=android'); + process.exit(1); +} + +const githubToken = argsMap.ghToken ?? process.env.GITHUB_TOKEN; +if (githubToken === undefined) { + Logger.error('No GitHub token provided. Either set a GITHUB_TOKEN environment variable or pass it using --gh-token'); + process.exit(1); +} + +GithubUtils.initOctokitWithToken(githubToken); + +/* ============= EXTRACT APP VERSION ============= */ + +// Formatted as "Profile_trace_for_1.4.81-9.cpuprofile" +const appVersionRegex = /\d+\.\d+\.\d+(-\d+)?/; +const appVersion = argsMap.profile.match(appVersionRegex)?.[0]; +if (appVersion === undefined) { + Logger.error('Could not extract the app version from the profile filename.'); + process.exit(1); +} +Logger.info(`Found app version ${appVersion} in the profile filename`); + +/* ============== UTILITY FUNCTIONS ============== */ + +async function getWorkflowRunArtifact() { + const artifactName = `${argsMap.platform}-sourcemap-${appVersion}`; + Logger.info(`Fetching sourcemap artifact with name "${artifactName}"`); + const artifact = await GithubUtils.getArtifactByName(artifactName); + if (artifact === undefined) { + throw new Error(`Could not find the artifact ${artifactName}! Are you sure the deploy step succeeded?`); + } + return artifact.id; +} + +const sourcemapDir = path.resolve(__dirname, '../.sourcemaps'); + +function downloadFile(url: string) { + Logger.log(`Downloading file from URL: ${url}`); + if (!fs.existsSync(sourcemapDir)) { + Logger.info(`Creating download directory ${sourcemapDir}`); + fs.mkdirSync(sourcemapDir); + } + + const destination = path.join(sourcemapDir, `${argsMap.platform}-sourcemap-${appVersion}.zip`); + const file = fs.createWriteStream(destination); + return new Promise((resolve, reject) => { + https + .get(url, (response) => { + response.pipe(file); + file.on('finish', () => { + file.close(); + Logger.success(`Downloaded file to ${destination}`); + resolve(destination); + }); + }) + .on('error', (error) => { + fs.unlink(destination, () => { + reject(error); + }); + }); + }); +} + +function unpackZipFile(zipPath: string) { + Logger.info(`Unpacking file ${zipPath}`); + const command = `unzip -o ${zipPath} -d ${sourcemapDir}`; + execSync(command, {stdio: 'inherit'}); + Logger.info(`Deleting zip file ${zipPath}`); + return new Promise((resolve, reject) => { + fs.unlink(zipPath, (error) => (error ? reject(error) : resolve())); + }); +} + +const localSourceMapPath = path.join(sourcemapDir, `${appVersion}-${argsMap.platform}.map`); +function renameDownloadedSourcemapFile() { + const androidName = 'index.android.bundle.map'; + const iosName = 'main.jsbundle.map'; + const downloadSourcemapPath = path.join(sourcemapDir, argsMap.platform === 'ios' ? iosName : androidName); + + if (!fs.existsSync(downloadSourcemapPath)) { + Logger.error(`Could not find the sourcemap file ${downloadSourcemapPath}`); + process.exit(1); + } + + Logger.info(`Renaming sourcemap file to ${localSourceMapPath}`); + fs.renameSync(downloadSourcemapPath, localSourceMapPath); +} + +// Symbolicate using the downloaded source map +function symbolicateProfile() { + const command = `npx react-native-release-profiler --local ${argsMap.profile} --sourcemap-path ${localSourceMapPath}`; + execSync(command, {stdio: 'inherit'}); +} + +async function fetchAndProcessArtifact() { + const artifactId = await getWorkflowRunArtifact(); + const downloadUrl = await GithubUtils.getArtifactDownloadURL(artifactId); + const zipPath = await downloadFile(downloadUrl); + await unpackZipFile(zipPath); + renameDownloadedSourcemapFile(); +} + +/* ============== MAIN SCRIPT ============== */ + +async function runAsyncScript() { + // Step: check if source map locally already exists (if so we can skip the download) + if (fs.existsSync(localSourceMapPath)) { + Logger.success(`Found local source map at ${localSourceMapPath}`); + Logger.info('Skipping download step'); + } else { + // Step: Download the source map for the app version and then symbolicate the profile: + try { + await fetchAndProcessArtifact(); + } catch (error) { + Logger.error(error); + process.exit(1); + } + } + + // Finally, symbolicate the profile + symbolicateProfile(); +} + +runAsyncScript(); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000000..2d548a3aa2ce --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "ts-node": { + "require": ["tsconfig-paths/register"] + } +} diff --git a/scripts/utils/Logger.ts b/scripts/utils/Logger.ts new file mode 100644 index 000000000000..a851f11ff74f --- /dev/null +++ b/scripts/utils/Logger.ts @@ -0,0 +1,37 @@ +const COLOR_DIM = '\x1b[2m'; +const COLOR_RESET = '\x1b[0m'; +const COLOR_YELLOW = '\x1b[33m'; +const COLOR_RED = '\x1b[31m'; +const COLOR_GREEN = '\x1b[32m'; + +const log = (...args: unknown[]) => { + console.debug(...args); +}; + +const info = (...args: unknown[]) => { + log('▶️', ...args); +}; + +const success = (...args: unknown[]) => { + const lines = ['✅', COLOR_GREEN, ...args, COLOR_RESET]; + log(...lines); +}; + +const warn = (...args: unknown[]) => { + const lines = ['⚠️', COLOR_YELLOW, ...args, COLOR_RESET]; + log(...lines); +}; + +const note = (...args: unknown[]) => { + const lines = [COLOR_DIM, ...args, COLOR_RESET]; + log(...lines); +}; + +const error = (...args: unknown[]) => { + const lines = ['🔴', COLOR_RED, ...args, COLOR_RESET]; + log(...lines); +}; + +const formatLink = (name: string | number, url: string) => `\x1b]8;;${url}\x1b\\${name}\x1b]8;;\x1b\\`; + +export {log, info, warn, note, error, success, formatLink}; diff --git a/scripts/utils/parseCommandLineArguments.ts b/scripts/utils/parseCommandLineArguments.ts new file mode 100644 index 000000000000..9bb1d340335e --- /dev/null +++ b/scripts/utils/parseCommandLineArguments.ts @@ -0,0 +1,19 @@ +type ArgsMap = Record; + +// Function to parse command-line arguments into a key-value object +export default function parseCommandLineArguments(): ArgsMap { + const args = process.argv.slice(2); // Skip node and script paths + const argsMap: ArgsMap = {}; + args.forEach((arg) => { + const [key, value] = arg.split('='); + if (key.startsWith('--')) { + const name = key.substring(2); + argsMap[name] = value; + // User may provide a help arg without any value + if (name.toLowerCase() === 'help' && !value) { + argsMap[name] = 'true'; + } + } + }); + return argsMap; +} diff --git a/src/App.tsx b/src/App.tsx index 9eda57816e9d..1ce17ea095bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import {PortalProvider} from '@gorhom/portal'; import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import {KeyboardProvider} from 'react-native-keyboard-controller'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; @@ -84,6 +85,7 @@ function App({url}: AppProps) { FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, + KeyboardProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 9311816c38a2..71ef5e26f7ae 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -356,12 +356,15 @@ const CONST = { CHRONOS_IN_CASH: 'chronosInCash', DEFAULT_ROOMS: 'defaultRooms', VIOLATIONS: 'violations', + DUPE_DETECTION: 'dupeDetection', REPORT_FIELDS: 'reportFields', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', SPOTNANA_TRAVEL: 'spotnanaTravel', ACCOUNTING_ON_NEW_EXPENSIFY: 'accountingOnNewExpensify', XERO_ON_NEW_EXPENSIFY: 'xeroOnNewExpensify', + NETSUITE_ON_NEW_EXPENSIFY: 'netsuiteOnNewExpensify', + REPORT_FIELDS_FEATURE: 'reportFieldsFeature', }, BUTTON_STATES: { DEFAULT: 'default', @@ -560,10 +563,8 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, USE_EXPENSIFY_URL, - STATUS_EXPENSIFY_URL: 'https://status.expensify.com', GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', - GOOGLE_CLOUD_URL: 'https://clients3.google.com/generate_204', IMAGE_BASE64_MATCH: 'base64', DEEPLINK_BASE_URL: 'new-expensify://', PDF_VIEWER_URL: '/pdf/web/viewer.html', @@ -940,6 +941,7 @@ const CONST = { COMMENT_LENGTH_DEBOUNCE_TIME: 500, SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, + UNREAD_UPDATE_DEBOUNCE_TIME: 300, }, SEARCH_TABLE_COLUMNS: { RECEIPT: 'receipt', @@ -1059,7 +1061,7 @@ const CONST = { MAX_RETRY_WAIT_TIME_MS: 10 * 1000, PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, - BACKEND_CHECK_INTERVAL_MS: 60 * 1000, + RECHECK_INTERVAL_MS: 60 * 1000, MAX_REQUEST_RETRIES: 10, NETWORK_STATUS: { ONLINE: 'online', @@ -1071,7 +1073,7 @@ const CONST = { DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, - DEFAULT_NETWORK_DATA: {isOffline: false, isBackendReachable: true}, + DEFAULT_NETWORK_DATA: {isOffline: false}, FORMS: { LOGIN_FORM: 'LoginForm', VALIDATE_CODE_FORM: 'ValidateCodeForm', @@ -1246,6 +1248,8 @@ const CONST = { MAX_AMOUNT_OF_SUGGESTIONS: 20, MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5, HERE_TEXT: '@here', + SUGGESTION_BOX_MAX_SAFE_DISTANCE: 38, + BIG_SCREEN_SUGGESTION_WIDTH: 300, }, COMPOSER_MAX_HEIGHT: 125, CHAT_FOOTER_SECONDARY_ROW_HEIGHT: 15, @@ -1796,6 +1800,7 @@ const CONST = { // Here we will add other connections names when we add support for them QBO: 'quickbooksOnline', XERO: 'xero', + NETSUITE: 'netsuite', }, SYNC_STAGE_NAME: { STARTING_IMPORT_QBO: 'startingImportQBO', @@ -4880,9 +4885,10 @@ type Country = keyof typeof CONST.ALL_COUNTRIES; type IOUType = ValueOf; type IOUAction = ValueOf; type IOURequestType = ValueOf; +type FeedbackSurveyOptionID = ValueOf, 'ID'>>; type SubscriptionType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID}; export default CONST; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index ddc4b5f88a69..458f1e3c5d24 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -146,10 +146,10 @@ function Expensify({ // Initialize this client as being an active client ActiveClientManager.init(); - // Used for the offline indicator appearing when someone is offline or backend is unreachable - const unsubscribeNetworkStatus = NetworkConnection.subscribeToNetworkStatus(); + // Used for the offline indicator appearing when someone is offline + const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); - return () => unsubscribeNetworkStatus(); + return unsubscribeNetInfo; }, []); useEffect(() => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0d22d3714fe6..8a20032b4f91 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -151,8 +151,8 @@ const ONYXKEYS = { /** Whether the user has tried focus mode yet */ NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', - /** Whether the user has been shown the hold educational interstitial yet */ - NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', + /** Whether the user has dismissed the hold educational interstitial */ + NVP_DISMISSED_HOLD_USE_EXPLANATION: 'nvp_dismissedHoldUseExplanation', /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', @@ -169,6 +169,21 @@ const ONYXKEYS = { /** The NVP with the last action taken (for the Quick Action Button) */ NVP_QUICK_ACTION_GLOBAL_CREATE: 'nvp_quickActionGlobalCreate', + /** The start date (yyyy-MM-dd HH:mm:ss) of the workspace owner’s free trial period. */ + NVP_FIRST_DAY_FREE_TRIAL: 'nvp_private_firstDayFreeTrial', + + /** The end date (yyyy-MM-dd HH:mm:ss) of the workspace owner’s free trial period. */ + NVP_LAST_DAY_FREE_TRIAL: 'nvp_private_lastDayFreeTrial', + + /** ID associated with the payment card added by the user. */ + NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID', + + /** The amount owed by the workspace’s owner. */ + NVP_PRIVATE_AMOUNT_OWNED: 'nvp_private_amountOwed', + + /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ + NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -376,6 +391,10 @@ const ONYXKEYS = { // Search Page related SNAPSHOT: 'snapshot_', + + // Shared NVPs + /** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */ + SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_', }, /** List of Form ids */ @@ -589,6 +608,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; }; type OnyxValuesMapping = { @@ -635,7 +655,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BLOCKED_FROM_CHAT]: string; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; - [ONYXKEYS.NVP_HOLD_USE_EXPLAINED]: boolean; + [ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean; [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; @@ -699,6 +719,11 @@ type OnyxValuesMapping = { [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; + [ONYXKEYS.NVP_BILLING_FUND_ID]: number; + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: number; + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/TIMEZONES.ts b/src/TIMEZONES.ts index 69ef89e7467e..0fb340a2d88d 100644 --- a/src/TIMEZONES.ts +++ b/src/TIMEZONES.ts @@ -551,6 +551,17 @@ const timezoneBackwardMap: Record> = { 'US/Pacific': 'America/Los_Angeles', 'US/Samoa': 'Pacific/Pago_Pago', 'W-SU': 'Europe/Moscow', + CET: 'Europe/Paris', + CST6CDT: 'America/Chicago', + EET: 'Europe/Sofia', + EST: 'America/Cancun', + EST5EDT: 'America/New_York', + HST: 'Pacific/Honolulu', + MET: 'Europe/Paris', + MST: 'America/Phoenix', + MST7MDT: 'America/Denver', + PST8PDT: 'America/Los_Angeles', + WET: 'Europe/Lisbon', }; export {timezoneBackwardMap}; diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 61e9a2d1860a..c670202c0350 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -138,35 +138,35 @@ function PaymentCardForm({ const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); const [currency, setCurrency] = useState(CONST.CURRENCY.USD); - const validate = (formValues: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS); + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS); - if (formValues.nameOnCard && !ValidationUtils.isValidLegalName(formValues.nameOnCard)) { - errors.nameOnCard = label.error.nameOnCard; + if (values.nameOnCard && !ValidationUtils.isValidLegalName(values.nameOnCard)) { + errors.nameOnCard = translate('addDebitCardPage.error.invalidName'); } - if (formValues.cardNumber && !ValidationUtils.isValidDebitCard(formValues.cardNumber.replace(/ /g, ''))) { - errors.cardNumber = label.error.cardNumber; + if (values.cardNumber && !ValidationUtils.isValidDebitCard(values.cardNumber.replace(/ /g, ''))) { + errors.cardNumber = translate('addDebitCardPage.error.debitCardNumber'); } - if (formValues.expirationDate && !ValidationUtils.isValidExpirationDate(formValues.expirationDate)) { - errors.expirationDate = label.error.expirationDate; + if (values.expirationDate && !ValidationUtils.isValidExpirationDate(values.expirationDate)) { + errors.expirationDate = translate('addDebitCardPage.error.expirationDate'); } - if (formValues.securityCode && !ValidationUtils.isValidSecurityCode(formValues.securityCode)) { - errors.securityCode = label.error.securityCode; + if (values.securityCode && !ValidationUtils.isValidSecurityCode(values.securityCode)) { + errors.securityCode = translate('addDebitCardPage.error.securityCode'); } - if (formValues.addressStreet && !ValidationUtils.isValidAddress(formValues.addressStreet)) { - errors.addressStreet = label.error.addressStreet; + if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { + errors.addressStreet = translate('addDebitCardPage.error.addressStreet'); } - if (formValues.addressZipCode && !ValidationUtils.isValidZipCode(formValues.addressZipCode)) { - errors.addressZipCode = label.error.addressZipCode; + if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { + errors.addressZipCode = translate('addDebitCardPage.error.addressZipCode'); } - if (!formValues.acceptTerms) { - errors.acceptTerms = 'common.error.acceptTerms'; + if (!values.acceptTerms) { + errors.acceptTerms = translate('common.error.acceptTerms'); } return errors; @@ -285,6 +285,7 @@ function PaymentCardForm({ inputStyle={isHovered && styles.cursorPointer} hideFocusedState caretHidden + disableKeyboard /> )} diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx index a1430615e37b..a112b36705c3 100644 --- a/src/components/AddPlaidBankAccount.tsx +++ b/src/components/AddPlaidBankAccount.tsx @@ -172,6 +172,7 @@ function AddPlaidBankAccount({ })); const {icon, iconSize, iconStyles} = getBankIcon({styles}); const plaidErrors = plaidData?.errors; + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const plaidDataErrorMessage = !isEmptyObject(plaidErrors) ? (Object.values(plaidErrors)[0] as string) : ''; const bankName = plaidData?.bankName; diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 296ecce7d092..27822fb390a6 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -3,7 +3,6 @@ import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import type {MaybePhraseKey} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -76,7 +75,7 @@ function AddressForm({ const zipSampleFormat = (country && (CONST.COUNTRY_ZIP_REGEX_DATA[country] as CountryZipRegex)?.samples) ?? ''; - const zipFormat: MaybePhraseKey = ['common.zipCodeExampleFormat', {zipSampleFormat}]; + const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); const isUSAForm = country === CONST.COUNTRY.US; @@ -87,50 +86,53 @@ function AddressForm({ * @returns - An object containing the errors for each inputID */ - const validator = useCallback((values: FormOnyxValues): Errors => { - const errors: Errors & { - zipPostCode?: string | string[]; - } = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const; - - // Check "State" dropdown is a valid state if selected Country is USA - if (values.country === CONST.COUNTRY.US && !values.state) { - errors.state = 'common.error.fieldRequired'; - } - - // Add "Field required" errors if any required field is empty - requiredFields.forEach((fieldKey) => { - const fieldValue = values[fieldKey] ?? ''; - if (ValidationUtils.isRequiredFulfilled(fieldValue)) { - return; + const validator = useCallback( + (values: FormOnyxValues): Errors => { + const errors: Errors & { + zipPostCode?: string | string[]; + } = {}; + const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const; + + // Check "State" dropdown is a valid state if selected Country is USA + if (values.country === CONST.COUNTRY.US && !values.state) { + errors.state = translate('common.error.fieldRequired'); } - errors[fieldKey] = 'common.error.fieldRequired'; - }); + // Add "Field required" errors if any required field is empty + requiredFields.forEach((fieldKey) => { + const fieldValue = values[fieldKey] ?? ''; + if (ValidationUtils.isRequiredFulfilled(fieldValue)) { + return; + } + + errors[fieldKey] = translate('common.error.fieldRequired'); + }); - // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; + // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object + const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; - // The postal code system might not exist for a country, so no regex either for them. - const countrySpecificZipRegex = countryRegexDetails?.regex; - const countryZipFormat = countryRegexDetails?.samples ?? ''; + // The postal code system might not exist for a country, so no regex either for them. + const countrySpecificZipRegex = countryRegexDetails?.regex; + const countryZipFormat = countryRegexDetails?.samples ?? ''; - ErrorUtils.addErrorMessage(errors, 'firstName', 'bankAccount.error.firstName'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('bankAccount.error.firstName')); - if (countrySpecificZipRegex) { - if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { - if (ValidationUtils.isRequiredFulfilled(values.zipPostCode?.trim())) { - errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', countryZipFormat]; - } else { - errors.zipPostCode = 'common.error.fieldRequired'; + if (countrySpecificZipRegex) { + if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { + if (ValidationUtils.isRequiredFulfilled(values.zipPostCode?.trim())) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat); + } else { + errors.zipPostCode = translate('common.error.fieldRequired'); + } } + } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); } - } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { - errors.zipPostCode = 'privatePersonalDetails.error.incorrectZipFormat'; - } - return errors; - }, []); + return errors; + }, + [translate], + ); return ( void; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Hint text to display */ hint?: string; diff --git a/src/components/AmountPicker/types.ts b/src/components/AmountPicker/types.ts index f7025685d840..5069893f8186 100644 --- a/src/components/AmountPicker/types.ts +++ b/src/components/AmountPicker/types.ts @@ -1,6 +1,5 @@ import type {AmountFormProps} from '@components/AmountForm'; import type {MenuItemBaseProps} from '@components/MenuItem'; -import type {MaybePhraseKey} from '@libs/Localize'; type AmountSelectorModalProps = { /** Whether the modal is visible */ @@ -24,7 +23,7 @@ type AmountPickerProps = { title?: string | ((value?: string) => string); /** Form Error description */ - errorText?: MaybePhraseKey; + errorText?: string; /** Callback to call when the input changes */ onInputChange?: (value: string | undefined) => void; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 3db946ce387e..ae09757b66e6 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -18,7 +18,6 @@ import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import useNativeDriver from '@libs/useNativeDriver'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; @@ -359,28 +358,6 @@ function AttachmentModal({ [isValidFile, getModalType, isDirectoryCheck], ); - /** - * In order to gracefully hide/show the confirm button when the keyboard - * opens/closes, apply an animation to fade the confirm button out/in. And since - * we're only updating the opacity of the confirm button, we must also conditionally - * disable it. - * - * @param shouldFadeOut If true, fade out confirm button. Otherwise fade in. - */ - const updateConfirmButtonVisibility = useCallback( - (shouldFadeOut: boolean) => { - setIsConfirmButtonDisabled(shouldFadeOut); - const toValue = shouldFadeOut ? 0 : 1; - - Animated.timing(confirmButtonFadeAnimation, { - toValue, - duration: 100, - useNativeDriver, - }).start(); - }, - [confirmButtonFadeAnimation], - ); - /** * close the modal */ @@ -547,7 +524,7 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} - onToggleKeyboard={updateConfirmButtonVisibility} + onToggleKeyboard={setIsConfirmButtonDisabled} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} fallbackSource={fallbackSource} @@ -559,7 +536,7 @@ function AttachmentModal({ ))} {/* If we have an onConfirm method show a confirmation button */} - {!!onConfirm && ( + {!!onConfirm && !isConfirmButtonDisabled && ( {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index f2325eda532d..38abe075ef81 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -4,20 +4,23 @@ import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import {getReport} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; import type {ReportAction, ReportActions} from '@src/types/onyx'; +import type {Note} from '@src/types/onyx/Report'; /** * Constructs the initial component state from report actions */ function extractAttachments( type: ValueOf, - {reportID, accountID, parentReportAction, reportActions}: {reportID?: string; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry}, + { + privateNotes, + accountID, + parentReportAction, + reportActions, + }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry}, ) { - const report = getReport(reportID); - const privateNotes = report?.privateNotes; const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index aad307073c0f..15740725c42e 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -33,7 +33,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let targetAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {reportID: report.reportID, accountID}); + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID}); } else { targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions}); } diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 947569538d32..eeac97bc5fa5 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -59,7 +59,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let targetAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {reportID: report.reportID, accountID}); + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID}); } else { targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined}); } @@ -91,7 +91,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate(targetAttachments[initialPage]); } } - }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate, accountID, report.reportID, type]); + }, [report.privateNotes, reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate, accountID, type]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts new file mode 100644 index 000000000000..5bb671c5edac --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts @@ -0,0 +1,5 @@ +function getBottomSuggestionPadding(): number { + return 16; +} + +export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts new file mode 100644 index 000000000000..3ad9bbe7b152 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts @@ -0,0 +1,5 @@ +function getBottomSuggestionPadding(): number { + return 0; +} + +export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx new file mode 100644 index 000000000000..9848d77e479e --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx @@ -0,0 +1,33 @@ +import {Portal} from '@gorhom/portal'; +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; +import useStyleUtils from '@hooks/useStyleUtils'; +import getBottomSuggestionPadding from './getBottomSuggestionPadding'; +import type {AutoCompleteSuggestionsPortalProps} from './types'; + +function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps) { + const StyleUtils = useStyleUtils(); + const styles = useMemo(() => StyleUtils.getBaseAutoCompleteSuggestionContainerStyle({left, width, bottom: bottom + getBottomSuggestionPadding()}), [StyleUtils, left, width, bottom]); + + if (!width) { + return null; + } + + return ( + + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + width={width} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + + + ); +} + +AutoCompleteSuggestionsPortal.displayName = 'AutoCompleteSuggestionsPortal'; + +export default AutoCompleteSuggestionsPortal; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx new file mode 100644 index 000000000000..2d1d533c2859 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import type {ReactElement} from 'react'; +import ReactDOM from 'react-dom'; +import {View} from 'react-native'; +import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; +import useStyleUtils from '@hooks/useStyleUtils'; +import getBottomSuggestionPadding from './getBottomSuggestionPadding'; +import type {AutoCompleteSuggestionsPortalProps} from './types'; + +/** + * On the mobile-web platform, when long-pressing on auto-complete suggestions, + * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). + * The desired pattern for all platforms is to do nothing on long-press. + * On the native platform, tapping on auto-complete suggestions will not blur the main input. + */ + +function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false { + const StyleUtils = useStyleUtils(); + + const bodyElement = document.querySelector('body'); + + const componentToRender = ( + + width={width} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); + + return ( + !!width && + bodyElement && + ReactDOM.createPortal( + {componentToRender}, + bodyElement, + ) + ); +} + +AutoCompleteSuggestionsPortal.displayName = 'AutoCompleteSuggestionsPortal'; + +export default AutoCompleteSuggestionsPortal; +export type {AutoCompleteSuggestionsPortalProps}; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts new file mode 100644 index 000000000000..61fa3e8dcd48 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts @@ -0,0 +1,13 @@ +import type {AutoCompleteSuggestionsProps} from '@components/AutoCompleteSuggestions/types'; + +type ExternalProps = Omit, 'measureParentContainerAndReportCursor'>; + +type AutoCompleteSuggestionsPortalProps = ExternalProps & { + left: number; + width: number; + bottom: number; + measuredHeightOfSuggestionRows: number; +}; + +// eslint-disable-next-line import/prefer-default-export +export type {AutoCompleteSuggestionsPortalProps}; diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 4c11f1f0e35c..70d70a8c1844 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,49 +1,32 @@ import type {ReactElement} from 'react'; import React, {useCallback, useEffect, useRef} from 'react'; import {FlatList} from 'react-native-gesture-handler'; -import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; +import type {AutoCompleteSuggestionsPortalProps} from './AutoCompleteSuggestionsPortal'; +import type {RenderSuggestionMenuItemProps} from './types'; -const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: boolean): number => { - if (isSuggestionPickerLarge) { - if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { - // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available - return CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - if (numRows > 2) { - // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible - return CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; -}; - -/** - * On the mobile-web platform, when long-pressing on auto-complete suggestions, - * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). - * The desired pattern for all platforms is to do nothing on long-press. - * On the native platform, tapping on auto-complete suggestions will not blur the main input. - */ +type ExternalProps = Omit, 'left' | 'bottom'>; function BaseAutoCompleteSuggestions({ - highlightedSuggestionIndex, + highlightedSuggestionIndex = 0, onSelect, accessibilityLabelExtractor, renderSuggestionMenuItem, suggestions, - isSuggestionPickerLarge, keyExtractor, -}: AutoCompleteSuggestionsProps) { + measuredHeightOfSuggestionRows, +}: ExternalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); + const prevRowHeightRef = useRef(measuredHeightOfSuggestionRows); + const fadeInOpacity = useSharedValue(0); const scrollRef = useRef>(null); /** * Render a suggestion menu item component. @@ -56,7 +39,6 @@ function BaseAutoCompleteSuggestions({ onMouseDown={(e) => e.preventDefault()} onPress={() => onSelect(index)} onLongPress={() => {}} - shouldUseHapticsOnLongPress={false} accessibilityLabel={accessibilityLabelExtractor(item, index)} > {renderSuggestionMenuItem(item, index)} @@ -66,26 +48,45 @@ function BaseAutoCompleteSuggestions({ ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); + + const animatedStyles = useAnimatedStyle(() => ({ + opacity: fadeInOpacity.value, + ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value), + })); useEffect(() => { - rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { - duration: 100, - easing: Easing.inOut(Easing.ease), - }); - }, [suggestions.length, isSuggestionPickerLarge, rowHeight]); + if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) { + fadeInOpacity.value = withTiming(1, { + duration: 70, + easing: Easing.inOut(Easing.ease), + }); + rowHeight.value = measuredHeightOfSuggestionRows; + } else { + fadeInOpacity.value = 1; + rowHeight.value = withTiming(measuredHeightOfSuggestionRows, { + duration: 100, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }); + } + + prevRowHeightRef.current = measuredHeightOfSuggestionRows; + }, [suggestions.length, rowHeight, measuredHeightOfSuggestionRows, prevRowHeightRef, fadeInOpacity]); useEffect(() => { if (!scrollRef.current) { return; } - scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); + // When using cursor control (moving the cursor with the space bar on the keyboard) on Android, moving the cursor too fast may cause an error. + try { + scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); + } catch (e) { + // eslint-disable-next-line no-console + } }, [highlightedSuggestionIndex]); return ( { if (DeviceCapabilities.hasHoverSupport()) { return; diff --git a/src/components/AutoCompleteSuggestions/index.native.tsx b/src/components/AutoCompleteSuggestions/index.native.tsx deleted file mode 100644 index fbfa7d953581..000000000000 --- a/src/components/AutoCompleteSuggestions/index.native.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {Portal} from '@gorhom/portal'; -import React from 'react'; -import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; -import type {AutoCompleteSuggestionsProps} from './types'; - -function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { - return ( - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {...props} /> - - ); -} - -AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; - -export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index c7f2aaea4d82..8634d6dd0ca0 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -1,38 +1,134 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import {View} from 'react-native'; +import React, {useEffect} from 'react'; +import useKeyboardState from '@hooks/useKeyboardState'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; -import type {AutoCompleteSuggestionsProps} from './types'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import CONST from '@src/CONST'; +import AutoCompleteSuggestionsPortal from './AutoCompleteSuggestionsPortal'; +import type {AutoCompleteSuggestionsProps, MeasureParentContainerAndCursor} from './types'; -function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { - const StyleUtils = useStyleUtils(); - const {windowHeight, windowWidth} = useWindowDimensions(); - const [{width, left, bottom}, setContainerState] = React.useState({ +const measureHeightOfSuggestionRows = (numRows: number, canBeBig: boolean): number => { + if (canBeBig) { + if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { + // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available + return CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + if (numRows > 2) { + // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible + return CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; +}; +function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSpaceAboveForSmall: boolean): boolean { + return isEnoughSpaceAboveForBig || isEnoughSpaceAboveForSmall; +} + +/** + * On the mobile-web platform, when long-pressing on auto-complete suggestions, + * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). + * The desired pattern for all platforms is to do nothing on long-press. + * On the native platform, tapping on auto-complete suggestions will not blur the main input. + */ +function AutoCompleteSuggestions({measureParentContainerAndReportCursor = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const containerRef = React.useRef(null); + const isInitialRender = React.useRef(true); + const isSuggestionAboveRef = React.useRef(false); + const leftValue = React.useRef(0); + const prevLeftValue = React.useRef(0); + const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions(); + const [suggestionHeight, setSuggestionHeight] = React.useState(0); + const [containerState, setContainerState] = React.useState({ width: 0, left: 0, bottom: 0, }); + const StyleUtils = useStyleUtils(); + const insets = useSafeAreaInsets(); + const {keyboardHeight} = useKeyboardState(); + const {paddingBottom: bottomInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); - React.useEffect(() => { - if (!measureParentContainer) { + useEffect(() => { + const container = containerRef.current; + if (!container) { + return () => {}; + } + container.onpointerdown = (e) => { + if (DeviceCapabilities.hasHoverSupport()) { + return; + } + e.preventDefault(); + }; + return () => (container.onpointerdown = null); + }, []); + + const suggestionsLength = props.suggestions.length; + + useEffect(() => { + if (!measureParentContainerAndReportCursor) { return; } - measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); - }, [measureParentContainer, windowHeight, windowWidth]); - const componentToRender = ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - /> - ); + measureParentContainerAndReportCursor(({x, y, width, scrollValue, cursorCoordinates}: MeasureParentContainerAndCursor) => { + const xCoordinatesOfCursor = x + cursorCoordinates.x; + const leftValueForBigScreen = + xCoordinatesOfCursor + CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH > windowWidth + ? windowWidth - CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH + : xCoordinatesOfCursor; + + let bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - (keyboardHeight || bottomInset); + const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; + + const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true); + const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false); + const isEnoughSpaceAboveForBig = windowHeight - bottomValue - contentMaxHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; + const isEnoughSpaceAboveForSmall = windowHeight - bottomValue - contentMinHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; - const bodyElement = document.querySelector('body'); + const newLeftValue = isSmallScreenWidth ? x : leftValueForBigScreen; + // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup + const isAdjustmentNeeded = Math.abs(prevLeftValue.current - leftValueForBigScreen) > 150; + if (isInitialRender.current || isAdjustmentNeeded) { + isSuggestionAboveRef.current = isSuggestionRenderedAbove(isEnoughSpaceAboveForBig, isEnoughSpaceAboveForSmall); + leftValue.current = newLeftValue; + isInitialRender.current = false; + prevLeftValue.current = newLeftValue; + } + let measuredHeight = 0; + if (isSuggestionAboveRef.current && isEnoughSpaceAboveForBig) { + // calculation for big suggestion box above the cursor + measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); + } else if (isSuggestionAboveRef.current && isEnoughSpaceAboveForSmall) { + // calculation for small suggestion box above the cursor + measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, false); + } else { + // calculation for big suggestion box below the cursor + measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); + bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + setSuggestionHeight(measuredHeight); + setContainerState({ + left: leftValue.current, + bottom: bottomValue, + width: widthValue, + }); + }); + }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset]); + + if (containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) { + return null; + } return ( - !!width && bodyElement && ReactDOM.createPortal({componentToRender}, bodyElement) + ); } diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 61d614dcf2e4..48bb6b713032 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -1,6 +1,15 @@ import type {ReactElement} from 'react'; -type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; +type MeasureParentContainerAndCursor = { + x: number; + y: number; + width: number; + height: number; + scrollValue: number; + cursorCoordinates: {x: number; y: number}; +}; + +type MeasureParentContainerAndCursorCallback = (props: MeasureParentContainerAndCursor) => void; type RenderSuggestionMenuItemProps = { item: TSuggestion; @@ -31,8 +40,8 @@ type AutoCompleteSuggestionsProps = { /** create accessibility label for each item */ accessibilityLabelExtractor: (item: TSuggestion, index: number) => string; - /** Meaures the parent container's position and dimensions. */ - measureParentContainer?: (callback: MeasureParentContainerCallback) => void; + /** Measures the parent container's position and dimensions. Also add a cursor coordinates */ + measureParentContainerAndReportCursor?: (props: MeasureParentContainerAndCursorCallback) => void; }; -export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; +export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps, MeasureParentContainerAndCursorCallback, MeasureParentContainerAndCursor}; diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index dd169576186e..db62aa9e1441 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -3,7 +3,6 @@ import React, {useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; @@ -41,7 +40,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { style?: StyleProp; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; diff --git a/src/components/ClientSideLoggingToolMenu/index.android.tsx b/src/components/ClientSideLoggingToolMenu/index.android.tsx index 298299e37fb9..aa1bc215b719 100644 --- a/src/components/ClientSideLoggingToolMenu/index.android.tsx +++ b/src/components/ClientSideLoggingToolMenu/index.android.tsx @@ -1,10 +1,47 @@ -/** - * Since client-side logging is currently supported on web and desktop natively right now, - * this menu will be hidden in iOS and Android. - * See comment here: https://github.com/Expensify/App/issues/43256#issuecomment-2154610196 - */ +import React, {useState} from 'react'; +import RNFetchBlob from 'react-native-blob-util'; +import Share from 'react-native-share'; +import type {Log} from '@libs/Console'; +import localFileCreate from '@libs/localFileCreate'; +import CONST from '@src/CONST'; +import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu'; + function ClientSideLoggingToolMenu() { - return null; + const [file, setFile] = useState<{path: string; newFileName: string; size: number}>(); + + const createAndSaveFile = (logs: Log[]) => { + localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => { + RNFetchBlob.MediaCollection.copyToMediaStore( + { + name: localFile.newFileName, + parentFolder: '', + mimeType: 'text/plain', + }, + 'Download', + localFile.path, + ); + setFile(localFile); + }); + }; + + const shareLogs = () => { + if (!file) { + return; + } + Share.open({ + url: `file://${file.path}`, + }); + }; + + return ( + setFile(undefined)} + onDisableLogging={createAndSaveFile} + onShareLogs={shareLogs} + displayPath={`${CONST.DOWNLOADS_PATH}/${file?.newFileName ?? ''}`} + /> + ); } ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu'; diff --git a/src/components/ClientSideLoggingToolMenu/index.ios.tsx b/src/components/ClientSideLoggingToolMenu/index.ios.tsx index 298299e37fb9..78ffccf612a2 100644 --- a/src/components/ClientSideLoggingToolMenu/index.ios.tsx +++ b/src/components/ClientSideLoggingToolMenu/index.ios.tsx @@ -1,10 +1,40 @@ -/** - * Since client-side logging is currently supported on web and desktop natively right now, - * this menu will be hidden in iOS and Android. - * See comment here: https://github.com/Expensify/App/issues/43256#issuecomment-2154610196 - */ +import React, {useState} from 'react'; +import Share from 'react-native-share'; +import useEnvironment from '@hooks/useEnvironment'; +import type {Log} from '@libs/Console'; +import getDownloadFolderPathSuffixForIOS from '@libs/getDownloadFolderPathSuffixForIOS'; +import localFileCreate from '@libs/localFileCreate'; +import CONST from '@src/CONST'; +import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu'; + function ClientSideLoggingToolMenu() { - return null; + const [file, setFile] = useState<{path: string; newFileName: string; size: number}>(); + const {environment} = useEnvironment(); + + const createFile = (logs: Log[]) => { + localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => { + setFile(localFile); + }); + }; + + const shareLogs = () => { + if (!file) { + return; + } + Share.open({ + url: `file://${file.path}`, + }); + }; + + return ( + setFile(undefined)} + onDisableLogging={createFile} + onShareLogs={shareLogs} + displayPath={`${CONST.NEW_EXPENSIFY_PATH}${getDownloadFolderPathSuffixForIOS(environment)}/${file?.newFileName ?? ''}`} + /> + ); } ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu'; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 5bd8aa9175d3..3a8a4e724948 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -91,6 +91,8 @@ function Composer( | { start: number; end?: number; + positionX?: number; + positionY?: number; } | undefined >({ diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 0ff91111bd07..9c7a5a215c1c 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -3,6 +3,12 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelection type TextSelection = { start: number; end?: number; + positionX?: number; + positionY?: number; +}; +type CustomSelectionChangeEvent = NativeSyntheticEvent & { + positionX?: number; + positionY?: number; }; type ComposerProps = TextInputProps & { @@ -45,7 +51,7 @@ type ComposerProps = TextInputProps & { autoFocus?: boolean; /** Update selection position on change */ - onSelectionChange?: (event: NativeSyntheticEvent) => void; + onSelectionChange?: (event: CustomSelectionChangeEvent) => void; /** Selection Object */ selection?: TextSelection; @@ -75,4 +81,4 @@ type ComposerProps = TextInputProps & { isGroupPolicyReport?: boolean; }; -export type {TextSelection, ComposerProps}; +export type {TextSelection, ComposerProps, CustomSelectionChangeEvent}; diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 002c0c6d4b0a..62fdc85687e1 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -4,7 +4,6 @@ import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -13,7 +12,7 @@ import MenuItemWithTopDescription from './MenuItemWithTopDescription'; type CountrySelectorProps = { /** Form error text. e.g when no country is selected */ - errorText?: MaybePhraseKey; + errorText?: string; /** Callback called when the country changes. */ onInputChange?: (value?: string) => void; diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index 3f72bbf429aa..564d2eeb8c75 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -7,7 +7,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isReceiptError} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; -import type {MaybePhraseKey} from '@libs/Localize'; import * as Localize from '@libs/Localize'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import Icon from './Icon'; @@ -23,7 +22,7 @@ type DotIndicatorMessageProps = { * timestamp: 'message', * } */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; @@ -45,12 +44,12 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica } // Fetch the keys, sort them, and map through each key to get the corresponding message - const sortedMessages: Array = Object.keys(messages) + const sortedMessages: Array = Object.keys(messages) .sort() - .map((key) => messages[key]); - + .map((key) => messages[key]) + .filter((message): message is string | ReceiptError => message !== null); // Removing duplicates using Set and transforming the result into an array - const uniqueMessages: Array = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); + const uniqueMessages: Array = [...new Set(sortedMessages)].map((message) => message); const isErrorMessage = type === 'error'; diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 1c0306741048..3781507b544c 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -7,10 +7,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; +import type {MeasureParentContainerAndCursorCallback} from './AutoCompleteSuggestions/types'; import Text from './Text'; -type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; - type EmojiSuggestionsProps = { /** The index of the highlighted emoji */ highlightedEmojiIndex?: number; @@ -33,8 +32,8 @@ type EmojiSuggestionsProps = { /** Stores user's preferred skin tone */ preferredSkinToneIndex: number; - /** Meaures the parent container's position and dimensions. */ - measureParentContainer: (callback: MeasureParentContainerCallback) => void; + /** Measures the parent container's position and dimensions. Also add cursor coordinates */ + measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; }; /** @@ -42,7 +41,15 @@ type EmojiSuggestionsProps = { */ const keyExtractor = (item: Emoji, index: number): string => `${item.name}+${index}}`; -function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { +function EmojiSuggestions({ + emojis, + onSelect, + prefix, + isEmojiPickerLarge, + preferredSkinToneIndex, + highlightedEmojiIndex = 0, + measureParentContainerAndReportCursor = () => {}, +}: EmojiSuggestionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); /** @@ -85,7 +92,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr onSelect={onSelect} isSuggestionPickerLarge={isEmojiPickerLarge} accessibilityLabelExtractor={keyExtractor} - measureParentContainer={measureParentContainer} + measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} /> ); } diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index 3b7d6475262b..925448046076 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -5,11 +5,13 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import type {FeedbackSurveyOptionID} from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import FixedFooter from './FixedFooter'; import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; import SingleOptionSelector from './SingleOptionSelector'; import Text from './Text'; +import TextInput from './TextInput'; type FeedbackSurveyProps = { /** Title of the survey */ @@ -19,14 +21,14 @@ type FeedbackSurveyProps = { description: string; /** Callback to be called when the survey is submitted */ - onSubmit: (reason: Option) => void; + onSubmit: (reason: FeedbackSurveyOptionID, note?: string) => void; /** Styles for the option row element */ optionRowStyles?: StyleProp; }; type Option = { - key: string; + key: FeedbackSurveyOptionID; label: TranslationPaths; }; @@ -44,6 +46,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: Feedbac const selectCircleStyles: StyleProp = {borderColor: theme.border}; const [reason, setReason] = useState