diff --git a/.eslintrc.js b/.eslintrc.js index 1f23ae22ca7e..135252825dcf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -100,7 +100,6 @@ module.exports = { __DEV__: 'readonly', }, rules: { - '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', @@ -156,6 +155,7 @@ module.exports = { fixMixedExportsWithInlineTypeSpecifier: false, }, ], + '@typescript-eslint/no-use-before-define': ['error', {functions: false}], // ESLint core rules 'es/no-nullish-coalescing-operators': 'off', diff --git a/.github/actions/javascript/bumpVersion/bumpVersion.ts b/.github/actions/javascript/bumpVersion/bumpVersion.ts index ed4828367cf2..eba79c7c9edb 100644 --- a/.github/actions/javascript/bumpVersion/bumpVersion.ts +++ b/.github/actions/javascript/bumpVersion/bumpVersion.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core'; import {exec as originalExec} from 'child_process'; import fs from 'fs'; +import type {PackageJson} from 'type-fest'; import {promisify} from 'util'; import {generateAndroidVersionCode, updateAndroidVersion, updateiOSVersion} from '@github/libs/nativeVersionUpdater'; import * as versionUpdater from '@github/libs/versionUpdater'; @@ -19,7 +20,7 @@ function updateNativeVersions(version: string) { .then(() => { console.log('Successfully updated Android!'); }) - .catch((err) => { + .catch((err: string | Error) => { console.error('Error updating Android'); core.setFailed(err); }); @@ -47,8 +48,12 @@ if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } -const {version: previousVersion} = JSON.parse(fs.readFileSync('./package.json').toString()); -const newVersion = versionUpdater.incrementVersion(previousVersion, semanticVersionLevel); +const {version: previousVersion}: PackageJson = JSON.parse(fs.readFileSync('./package.json').toString()); +if (!previousVersion) { + core.setFailed('Error: Could not read package.json'); +} + +const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel); console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`); updateNativeVersions(newVersion); diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index d4a085fc9ddf..e1a5cf13a8d9 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -3478,7 +3478,10 @@ if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } const { version: previousVersion } = JSON.parse(fs_1.default.readFileSync('./package.json').toString()); -const newVersion = versionUpdater.incrementVersion(previousVersion, semanticVersionLevel); +if (!previousVersion) { + core.setFailed('Error: Could not read package.json'); +} +const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel); console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`); updateNativeVersions(newVersion); console.log(`Setting npm version to ${newVersion}`); diff --git a/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts b/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts index bf94b136ce43..d51d68796070 100644 --- a/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts +++ b/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts @@ -60,7 +60,7 @@ const run = function (): Promise { core.setOutput('HAS_DEPLOY_BLOCKERS', false); } }) - .catch((error) => { + .catch((error: string | Error) => { console.error('A problem occurred while trying to communicate with the GitHub API', error); core.setFailed(error); }); diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index 57a941105f90..c486fdbd39f3 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -28,7 +28,7 @@ const run = () => { // Extract timestamp, Graphite accepts timestamp in seconds if (current.metadata?.creationDate) { - timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000); + timestamp = Math.floor(new Date(current.metadata.creationDate as string).getTime() / 1000); } if (current.name && current.meanDuration && current.meanCount && timestamp) { diff --git a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts index dc1e99d1e3b8..262b603124fa 100644 --- a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts +++ b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core'; import {readFileSync} from 'fs'; +import type {PackageJson} from 'type-fest'; import * as versionUpdater from '@github/libs/versionUpdater'; const semverLevel = core.getInput('SEMVER_LEVEL', {required: true}); @@ -7,6 +8,10 @@ if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVEL core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); } -const {version: currentVersion} = JSON.parse(readFileSync('./package.json', 'utf8')); -const previousVersion = versionUpdater.getPreviousVersion(currentVersion, semverLevel); +const {version: currentVersion}: PackageJson = JSON.parse(readFileSync('./package.json', 'utf8')); +if (!currentVersion) { + core.setFailed('Error: Could not read package.json'); +} + +const previousVersion = versionUpdater.getPreviousVersion(currentVersion ?? '', semverLevel); core.setOutput('PREVIOUS_VERSION', previousVersion); diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index f372f0fdaf99..8eac2f62f03e 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -2728,7 +2728,10 @@ if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVELS).inclu core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); } const { version: currentVersion } = JSON.parse((0, fs_1.readFileSync)('./package.json', 'utf8')); -const previousVersion = versionUpdater.getPreviousVersion(currentVersion, semverLevel); +if (!currentVersion) { + core.setFailed('Error: Could not read package.json'); +} +const previousVersion = versionUpdater.getPreviousVersion(currentVersion ?? '', semverLevel); core.setOutput('PREVIOUS_VERSION', previousVersion); diff --git a/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts b/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts index aabc6b33086a..f57ef6c36a04 100644 --- a/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts +++ b/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts @@ -90,7 +90,7 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) { getNumberOfItemsFromReviewerChecklist() .then(checkIssueForCompletedChecklist) - .catch((err) => { + .catch((err: string | Error) => { console.error(err); core.setFailed(err); }); diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 5aa0d1daf342..6da0ecba158c 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -52,7 +52,7 @@ function detectCycle(): boolean { fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) .pipe(parser) - .on('data', (row) => { + .on('data', (row: [string, string]) => { // Create a directed graph of sourceURL -> targetURL addEdge(row[0], row[1]); }) diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 10723d5efa04..7e7d55ac5d2e 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -15,6 +15,10 @@ on: type: string required: true +concurrency: + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-e2e + cancel-in-progress: true + jobs: buildBaseline: runs-on: ubuntu-latest-xl diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 50e886942c98..da7757fcbfa8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: paths: ['**.js', '**.ts', '**.tsx', '**.json', '**.mjs', '**.cjs', 'config/.editorconfig', '.watchmanconfig', '.imgbotconfig'] concurrency: - group: "${{ github.ref }}-lint" + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-lint cancel-in-progress: true jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c76425a40fbf..d6b346cb3995 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: paths: ['**.js', '**.ts', '**.tsx', '**.sh', 'package.json', 'package-lock.json'] concurrency: - group: "${{ github.ref }}-jest" + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-jest cancel-in-progress: true jobs: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 88d4d24a5723..476b01f87b07 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -7,6 +7,10 @@ on: branches-ignore: [staging, production] paths: ['**.js', '**.ts', '**.tsx', 'package.json', 'package-lock.json', 'tsconfig.json'] +concurrency: + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-typecheck + cancel-in-progress: true + jobs: typecheck: if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 9e5a237afd1e..1a663b5fa08a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,12 +2,21 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" apply plugin: "com.google.firebase.firebase-perf" +apply plugin: "fullstory" apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. */ + +/* Fullstory settings */ +fullstory { + org 'o-1WN56P-na1' + enabledVariants 'all' + logcatLevel 'debug' +} + react { /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' @@ -98,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047404 - versionName "1.4.74-4" + versionCode 1001047606 + versionName "1.4.76-6" // 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" @@ -162,7 +171,7 @@ android { signingConfig null // buildTypes take precedence over productFlavors when it comes to the signing configuration, // thus we need to manually set the signing config, so that the e2e uses the debug config again. - // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. + // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. productFlavors.all { flavor -> // All release builds should be signed with the release config ... flavor.signingConfig signingConfigs.release diff --git a/android/build.gradle b/android/build.gradle index 10600480d8bb..52c998998ba0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,6 +20,7 @@ buildscript { repositories { google() mavenCentral() + maven {url "https://maven.fullstory.com"} } dependencies { classpath("com.android.tools.build:gradle") @@ -27,6 +28,9 @@ buildscript { classpath("com.google.gms:google-services:4.3.4") 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") + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") @@ -70,7 +74,7 @@ allprojects { // 'mapbox' is the fixed username for Mapbox's Maven repository. username = 'mapbox' - // The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property. + // The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property. // Run "npm run setup-mapbox-sdk" to set this property in «USER_HOME»/.gradle/gradle.properties // Example gradle.properties entry: diff --git a/assets/images/receipt-plus.svg b/assets/images/receipt-plus.svg new file mode 100644 index 000000000000..3907da65c472 --- /dev/null +++ b/assets/images/receipt-plus.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/babel.config.js b/babel.config.js index 9f8b7a711d78..3023d37df7e0 100644 --- a/babel.config.js +++ b/babel.config.js @@ -35,6 +35,17 @@ const metro = { ['@babel/plugin-proposal-private-property-in-object', {loose: true}], // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation 'react-native-reanimated/plugin', + + /* Fullstory */ + '@fullstory/react-native', + [ + '@fullstory/babel-plugin-annotate-react', + { + native: true, + setFSTagName: true, + }, + ], + // Import alias for native devices [ 'module-resolver', diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 7cafafca9973..9d397b9557a3 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -98,7 +98,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): {from: 'web/apple-touch-icon.png'}, {from: 'assets/images/expensify-app-icon.svg'}, {from: 'web/manifest.json'}, - {from: 'web/gtm.js'}, + {from: 'web/thirdPartyScripts.js'}, {from: 'assets/css', to: 'css'}, {from: 'assets/fonts/web', to: 'fonts'}, {from: 'assets/sounds', to: 'sounds'}, diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 13f7592b65e1..aec527edabe0 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -95,9 +95,10 @@ Additionally, if you want to discuss an idea with the open source community with #### Propose a solution for the job 4. You can propose solutions on any issue at any time, but if you propose solutions to jobs before the `Help Wanted` label is applied, you do so at your own risk. Proposals will not be reviewed until the label is added and there is always a chance that we might not add the label or hire an external contributor for the job. -5. After you reproduce the issue, complete the [proposal template here](./PROPOSAL_TEMPLATE.md) and post it as a comment in the corresponding GitHub issue (linked in the Upwork job). +5. Contributors should **not** submit proposals on issues when they have assigned issues or PRs that are awaiting an action from them. If so, they will be in violation of Rule #1 (Get Shit Done) in our [Code of Conduct](https://github.com/Expensify/App/blob/main/CODE_OF_CONDUCT.md) and will receive a warning. Multiple warnings can lead to removal from the program. +6. After you reproduce the issue, complete the [proposal template here](./PROPOSAL_TEMPLATE.md) and post it as a comment in the corresponding GitHub issue (linked in the Upwork job). - Note: Before submitting a proposal on an issue, be sure to read any other existing proposals. ALL NEW PROPOSALS MUST BE DIFFERENT FROM EXISTING PROPOSALS. The *difference* should be important, meaningful or considerable. -6. Refrain from leaving additional comments until someone from the Contributor-Plus team and / or someone from Expensify provides feedback on your proposal (do not create a pull request yet). +7. Refrain from leaving additional comments until someone from the Contributor-Plus team and / or someone from Expensify provides feedback on your proposal (do not create a pull request yet). - Do not leave more than one proposal. - Do not make extensive changes to your current proposal until after it has been reviewed. - If you want to make an entirely new proposal or update an existing proposal, please go back and edit your original proposal, then post a new comment to the issue in this format to alert everyone that it has been updated: @@ -105,8 +106,8 @@ Additionally, if you want to discuss an idea with the open source community with ## Proposal [Updated](link to proposal) ``` -7. If your proposal is accepted by the Expensify engineer assigned to the issue, Expensify will hire you on Upwork and assign the GitHub issue to you. -8. Once hired, post a comment in the Github issue stating when you expect to have your PR ready for review. +8. If your proposal is accepted by the Expensify engineer assigned to the issue, Expensify will hire you on Upwork and assign the GitHub issue to you. +9. Once hired, post a comment in the Github issue stating when you expect to have your PR ready for review. #### Begin coding your solution in a pull request 9. When you are ready to start, fork the repository and create a new branch. diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 22b1dea61bae..f3e928da8cb0 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -242,7 +242,7 @@ const foo: object = [1, 2, 3]; // TypeScript does not error If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. -> Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. +> Even though `string` is specified as a key, `Record` type can still accept objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. ```ts function logObject(object: Record) { diff --git a/desktop/ELECTRON_EVENTS.ts b/desktop/ELECTRON_EVENTS.ts index 607ad7b21580..b06794567c7d 100644 --- a/desktop/ELECTRON_EVENTS.ts +++ b/desktop/ELECTRON_EVENTS.ts @@ -9,6 +9,10 @@ const ELECTRON_EVENTS = { KEYBOARD_SHORTCUTS_PAGE: 'keyboard-shortcuts-page', START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', + DOWNLOAD: 'download', + DOWNLOAD_COMPLETED: 'download-completed', + DOWNLOAD_FAILED: 'download-started', + DOWNLOAD_CANCELED: 'download-canceled', SILENT_UPDATE: 'silent-update', } as const; diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index 487e528a7485..74b91c4634a1 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -16,10 +16,19 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.REQUEST_VISIBILITY, ELECTRON_EVENTS.START_UPDATE, ELECTRON_EVENTS.LOCALE_UPDATED, + ELECTRON_EVENTS.DOWNLOAD, ELECTRON_EVENTS.SILENT_UPDATE, ] as const; -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR] as const; +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ + ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, + ELECTRON_EVENTS.UPDATE_DOWNLOADED, + ELECTRON_EVENTS.FOCUS, + ELECTRON_EVENTS.BLUR, + ELECTRON_EVENTS.DOWNLOAD_COMPLETED, + ELECTRON_EVENTS.DOWNLOAD_FAILED, + ELECTRON_EVENTS.DOWNLOAD_CANCELED, +] as const; const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; @@ -67,7 +76,7 @@ contextBridge.exposeInMainWorld('electron', { } // Deliberately strip event as it includes `sender` - ipcRenderer.on(channel, (event, ...args) => func(...args)); + ipcRenderer.on(channel, (event, ...args: unknown[]) => func(...args)); }, /** Remove listeners for a single channel from the main process and sent to the renderer process. */ diff --git a/desktop/createDownloadQueue.ts b/desktop/createDownloadQueue.ts new file mode 100644 index 000000000000..132848c5da9e --- /dev/null +++ b/desktop/createDownloadQueue.ts @@ -0,0 +1,116 @@ +import type {BrowserWindow} from 'electron'; +import {app} from 'electron'; +import * as path from 'path'; +import createQueue from '@libs/Queue/Queue'; +import CONST from '@src/CONST'; +import ELECTRON_EVENTS from './ELECTRON_EVENTS'; +import type Options from './electronDownloadManagerType'; + +type DownloadItem = { + // The window where the download will be initiated + win: BrowserWindow; + + // The URL of the file to be downloaded + url: string; + + // The options for the download, such as save path, file name, etc. + options: Options; +}; + +/** + * Returns the filename with extension based on the given name and MIME type. + * @param name - The name of the file. + * @param mime - The MIME type of the file. + * @returns The filename with extension. + */ +const getFilenameFromMime = (name: string, mime: string): string => { + const extensions = mime.split('/').pop(); + return `${name}.${extensions}`; +}; + +const createDownloadQueue = () => { + const downloadItemProcessor = (item: DownloadItem): Promise => + new Promise((resolve, reject) => { + let downloadTimeout: NodeJS.Timeout; + let downloadListener: (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => void; + + const timeoutFunction = () => { + item.win.webContents.session.removeListener('will-download', downloadListener); + resolve(); + }; + + const listenerFunction = (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => { + clearTimeout(downloadTimeout); + + const options = item.options; + const cleanup = () => item.win.webContents.session.removeListener('will-download', listenerFunction); + const errorMessage = `The download of ${electronDownloadItem.getFilename()} was interrupted`; + + if (options.directory && !path.isAbsolute(options.directory)) { + throw new Error('The `directory` option must be an absolute path'); + } + + const directory = options.directory ?? app.getPath('downloads'); + + let filePath: string; + if (options.filename) { + filePath = path.join(directory, options.filename); + } else { + const filename = electronDownloadItem.getFilename(); + const name = path.extname(filename) ? filename : getFilenameFromMime(filename, electronDownloadItem.getMimeType()); + + filePath = options.overwrite ? path.join(directory, name) : path.join(directory, name); + } + + if (options.saveAs) { + electronDownloadItem.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); + } else { + electronDownloadItem.setSavePath(filePath); + } + + electronDownloadItem.on('updated', (_, state) => { + if (state !== 'interrupted') { + return; + } + + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELED, {url: item.url}); + cleanup(); + reject(new Error(errorMessage)); + electronDownloadItem.cancel(); + }); + + electronDownloadItem.on('done', (_, state) => { + cleanup(); + if (state === 'cancelled') { + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELED, {url: item.url}); + resolve(); + } else if (state === 'interrupted') { + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_FAILED, {url: item.url}); + reject(new Error(errorMessage)); + } else if (state === 'completed') { + if (process.platform === 'darwin') { + const savePath = electronDownloadItem.getSavePath(); + app.dock.downloadFinished(savePath); + } + item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, {url: item.url}); + resolve(); + } + }); + }; + + downloadTimeout = setTimeout(timeoutFunction, CONST.DOWNLOADS_TIMEOUT); + downloadListener = listenerFunction; + + item.win.webContents.downloadURL(item.url); + item.win.webContents.session.on('will-download', downloadListener); + }); + + const queue = createQueue(downloadItemProcessor); + + const enqueueDownloadItem = (item: DownloadItem): void => { + queue.enqueue(item); + }; + return {enqueueDownloadItem, dequeueDownloadItem: queue.dequeue}; +}; + +export default createDownloadQueue; diff --git a/desktop/electronDownloadManagerType.ts b/desktop/electronDownloadManagerType.ts new file mode 100644 index 000000000000..755efe173887 --- /dev/null +++ b/desktop/electronDownloadManagerType.ts @@ -0,0 +1,49 @@ +import type {SaveDialogOptions} from 'electron'; + +type Options = { + /** + Show a `Save As…` dialog instead of downloading immediately. + + Note: Only use this option when strictly necessary. Downloading directly without a prompt is a much better user experience. + + @default false + */ + readonly saveAs?: boolean; + + /** + The directory to save the file in. + + Must be an absolute path. + + Default: [User's downloads directory](https://electronjs.org/docs/api/app/#appgetpathname) + */ + readonly directory?: string; + + /** + Name of the saved file. + This option only makes sense for `electronDownloadManager.download()`. + + Default: [`downloadItem.getFilename()`](https://electronjs.org/docs/api/download-item/#downloaditemgetfilename) + */ + readonly filename?: string; + + /** + Allow downloaded files to overwrite files with the same name in the directory they are saved to. + + The default behavior is to append a number to the filename. + + @default false + */ + readonly overwrite?: boolean; + + /** + Customize the save dialog. + + If `defaultPath` is not explicity defined, a default value is assigned based on the file path. + + @default {} + */ + readonly dialogOptions?: SaveDialogOptions; +}; + +export default Options; diff --git a/desktop/main.ts b/desktop/main.ts index b40557464ec1..0f4774d3b73b 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -15,6 +15,8 @@ import type PlatformSpecificUpdater from '@src/setup/platformSetup/types'; import type {Locale} from '@src/types/onyx'; import ELECTRON_EVENTS from './ELECTRON_EVENTS'; +const createDownloadQueue = require('./createDownloadQueue').default; + const port = process.env.PORT ?? 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; @@ -581,7 +583,7 @@ const mainWindow = (): Promise => { app.hide(); } - ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale) => { + ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale: Locale) => { Menu.setApplicationMenu(Menu.buildFromTemplate(localizeMenuItems(initialMenuTemplate, updatedLocale))); disposeContextMenu(); disposeContextMenu = createContextMenu(updatedLocale); @@ -601,7 +603,7 @@ const mainWindow = (): Promise => { // Listen to badge updater event emitted by the render process // and update the app badge count (MacOS only) - ipcMain.on(ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, (event, totalCount) => { + ipcMain.on(ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, (event, totalCount?: number) => { if (totalCount === -1) { // The electron docs say you should be able to update this and pass no parameters to set the badge // to a single red dot, but in practice it resulted in an error "TypeError: Insufficient number of @@ -613,6 +615,15 @@ const mainWindow = (): Promise => { } }); + const downloadQueue = createDownloadQueue(); + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { + const downloadItem = { + ...downloadData, + win: browserWindow, + }; + downloadQueue.enqueueDownloadItem(downloadItem); + }); + // Automatically check for and install the latest version in the background ipcMain.on(ELECTRON_EVENTS.SILENT_UPDATE, () => { if (isSilentUpdating) { diff --git a/docs/articles/expensify-classic/expenses/Referral-Program.md b/docs/articles/expensify-classic/expenses/Referral-Program.md deleted file mode 100644 index 24605dd17d3f..000000000000 --- a/docs/articles/expensify-classic/expenses/Referral-Program.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Earn money with Expensify referrals -description: Get paid with the Expensify referral program! Share your link, earn $250 per successful sign-up, and enjoy unlimited income potential. It’s that easy. -redirect_from: articles/other/Referral-Program/ ---- - - -# Earn money with Expensify referrals - -Picture this: You've found Expensify and it's transformed your approach to expense management and financial organization. You love it so much that you can't help but recommend it to friends, family, and colleagues. Wouldn’t it be nice if you could get rewarded just for spreading the word? - -With Expensify referrals, you can. Every time someone you invite to the platform signs up for a paid annual plan on Expensify, you’ll earn $250. Think of it as a thank-you gift from us to you! - -## How to get paid for Expensify referrals - -Here are a few easy ways to get paid for Expensify friend referrals: - -- Submit an expense report to your boss (even just one receipt!) -- Send an invoice to a client or customer -- Share your referral link with a friend - - To find your referral link, open your Expensify mobile app and go to **Settings > Refer a friend, earn cash! > Share invite link**. - -**If the person you referred commits to an annual subscription with two or more active users and makes two monthly payments, you’ll get $250. Cha-ching!** - -## Who can you refer? - -You can refer anyone who might benefit from Expensify. Seriously. Anybody. - -Know a small business owner? Refer them! An [accountant](https://use.expensify.com/accountants-program)? Refer them! A best friend from childhood who keeps losing paper receipts? Refer them! - -Plus, you can [refer an unlimited amount of new users](https://use.expensify.com/blog/earn-50000-by-referring-your-friends-to-expensify/) with the Expensify referral program, so your earning potential is truly sky-high. - -## Common questions about Expensify benefits - -Still have questions about the Expensify referral program? We’ve got answers. Check out our FAQ below. - -### How will I know if I am the first person to refer someone to Expensify? - -You’ll know if you’re the first person to refer someone to Expensify if we reach out to let you know that they’ve successfully adopted Expensify and have paid for two months of an annual subscription. - -Simply put, we check for the earliest recorded referrer of a member on the workspace, and if that’s you, then we’ll let you know. - -### My referral wasn’t counted! How can I appeal? - -If you think your Expensify friend referral wasn’t counted, please send a message to concierge@expensify.com with the email of the person you referred. Our team will review the referral and get back to you. - -## Share the Expensify love — and get paid in the process - -Who needs a side hustle when you have Expensify? With Expensify benefits, it’s not just about managing your expenses — it's about expanding your income too. Share your Expensify referral link now or send over an invoice to unlock unlimited earning potential. diff --git a/docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md b/docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md new file mode 100644 index 000000000000..81ce761f84f4 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md @@ -0,0 +1,25 @@ +--- +title: Change Expensify Card limit +description: Increase or decrease the limit for an Expensify Card or for a group +--- +
+ +You can set Expensify Card limits for each group in your organization, or you can set the limit per card. + +# Set a limit per card + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Next to the card, click **Edit Limit**. +4. Ensure the Custom Smart Limit toggle is enabled to be able to set a specific card limit. Otherwise, the card limit will be determined by the limit set for the group that the employee is in. +5. In the Limit Amount field, enter the desired limit. If set to $0, the card will be disabled for use until the limit is increased. +6. Click **Save**. + +# Set a limit per group + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Click the **Groups** tab on the left. +4. Click the Expensify Card Smart Limit field for the card and enter the desired limit. + +
diff --git a/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md b/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md new file mode 100644 index 000000000000..d7fa33221834 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md @@ -0,0 +1,28 @@ +--- +title: Deactivate or cancel an Expensify Card +description: Close an Expensify Card +--- +
+ +A cardholder or a Domain Admin can cancel an Expensify Card. You may want to cancel a card: +- To cancel an old Expensify Card after upgrading to the new Expensify Visa® Commercial Card +- After a fraudulent or suspicious charge +- After an employee leaves the company + +# Domain Admins + +To cancel an employee's Expensify Card as a Domain Admin, + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Next to the card, click **Terminate**. + +# Cardholders + +To cancel an Expensify Card assigned to you, + +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Click **Cancel** next to the card. + +
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md index 3ce0d07cb65d..ee116f65a398 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md @@ -276,9 +276,11 @@ To add a Custom Segment to your workspace, you’ll need to locate three fields **To find the Script/Field ID:** -If configuring Custom Segments as Report Fields, use the Field ID on the Transactions tab (under _Custom Segments > Transactions_). +Note that as of 2019.1, any new custom segments that you create automatically use the unified ID, and the Use as Field ID box is not visible. If you are editing a custom segment definition that was created before 2019.1, the Use as Field ID box is available. +To use a unified ID for the entire custom segment definition, check the Use as Field ID box. When the box is checked, no field ID fields or columns are shown on the Application & Sourcing subtabs because one ID is used for all fields. -If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under _Custom Segments > Transaction Columns_). +- If configuring Custom Segments as Report Fields, use the Field ID on the Transactions tab (under _Custom Segments > Transactions_), or if no Field ID is shown, use the unified ID (just called "ID" right below the "Label"). +- If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under _Custom Segments > Transaction Columns_), or if no Field ID is shown, use the unified ID (just called "ID" right below the "Label"). Lastly, head over to Expensify and do the following: 1. Navigate to **Settings > Workspace > Group > _[Workspace Name]_ > Connections > Configure > Coding tab** diff --git a/docs/articles/new-expensify/connections/Coming-Soon.md b/docs/articles/new-expensify/connections/Coming-Soon.md deleted file mode 100644 index 4d32487a14b5..000000000000 --- a/docs/articles/new-expensify/connections/Coming-Soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Coming soon -description: Coming soon ---- - -# Coming soon \ No newline at end of file diff --git a/docs/articles/new-expensify/connections/Set-up-Xero-connection.md b/docs/articles/new-expensify/connections/Set-up-Xero-connection.md new file mode 100644 index 000000000000..73bff6ad5862 --- /dev/null +++ b/docs/articles/new-expensify/connections/Set-up-Xero-connection.md @@ -0,0 +1,102 @@ +--- +title: Set up Xero connection +description: Integrate Xero with Expensify +--- +
+ +{% include info.html %} +To use the Xero connection, you must have a Xero account and an Expensify Collect plan. +{% include end-info.html %} + +To set up your Xero connection, complete the 4 steps below. + +# Step 1: Connect Expensify to Xero + +
    +
  1. Click your profile image or icon in the bottom left menu.
  2. +
  3. Scroll down and click Workspaces in the left menu.
  4. +
  5. Select the workspace you want to connect to Xero.
  6. +
  7. Click More features in the left menu.
  8. +
  9. Scroll down to the Integrate section and enable the Accounting toggle.
  10. +
  11. Click Accounting in the left menu.
  12. +
  13. Click Set up to the right of Xero.
  14. +
  15. Enter your Xero login details to import your settings from Xero to Expensify.
  16. +
+ +# Step 2: Configure import settings + +The following steps help you determine how data will be imported from Xero to Expensify. + +
    +
  1. Under the Accounting settings for your workspace, click Import under the Xero connection.
  2. +
  3. Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
  4. +
      +
    • Xero organization: Select which Xero organization your Expensify workspace is connected to. Each organization can only be connected to one workspace at a time.
    • +
    • Chart of Accounts: Your Xero chart of accounts and any accounts marked as “Show In Expense Claims” will be automatically imported into Expensify as Categories. This cannot be amended.
    • +
    • Tracking Categories: Choose whether to import your Xero categories for cost centers and regions as tags in Expensify.
    • +
    • Re-bill Customers: When enabled, Xero customer contacts are imported into Expensify as tags for expense tracking. After exporting to Xero, tagged billable expenses can be included on a sales invoice to your customer.
    • +
    • Taxes: Choose whether to import tax rates and tax defaults from Xero.
    • +
    +
+ +# Step 3: Configure export settings +The following steps help you determine how data will be exported from Expensify to Xero. + +
    +
  1. Under the Accounting settings for your workspace, click Export under the Xero connection.
  2. +
  3. Review each of the following export settings:
  4. +
      +
    • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
    • +
    +
+{% include info.html %} +- Other Workspace Admins will still be able to export to Xero. +- If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. +{% include end-info.html %} + +
    +
      +
    • Export Out-of-Pocket Expenses as: All out-of-pocket expenses will be exported as purchase bills. This cannot be amended.
    • +
    • Purchase Bill Date: Choose whether to use the date of last expense, export date, or submitted date.
    • +
    • Export invoices as: All invoices exported to Xero will be as a sales invoice. This cannot be amended.
    • +
    • Export company card expenses as: All company card expenses export to Xero as bank transactions. This cannot be amended.
    • +
    • Xero Bank Account: Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported.
    • +
    +
+ +# Step 4: Configure advanced settings + +The following steps help you determine the advanced settings for your connection, like auto-sync. + +
    +
  1. Under the Accounting settings for your workspace, click Advanced under the Xero connection.
  2. +
  3. Select an option for each of the following settings:
  4. +
      +
    • Auto-sync: Choose whether to enable Xero to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period. Once you’ve added a business bank account for ACH reimbursement, any reimbursable expenses will be sent to Xero automatically when the report is reimbursed. For non-reimbursable reports, Expensify automatically queues the report to export to Xero after it has completed the approval workflow in Expensify.
    • +
    • Set Purchase Bill Status: Choose the status of your purchase bills:
    • +
        +
      • Draft
      • +
      • Awaiting Approval
      • +
      • Awaiting Payment
      • +
      +
    • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in Xero will also show in Expensify as Paid. If enabled, you must also select the Xero account that reimbursements are coming out of, and Expensify will automatically create the payment in Xero.
    • +
    • Xero Bill Payment Account: If you enable Sync Reimbursed Reports, you must select the Xero Bill Payment account your reimbursements will come from.
    • +
    • Xero Invoice Collections Account: If you are exporting invoices from Expensify, select the invoice collection account that you want invoices to appear under once they are marked as paid.
    • +
    +
+ +{% include faq-begin.md %} + +**How do I disconnect Xero from Expensify?** + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to disconnect from Xero. +4. Click **Accounting** in the left menu. +5. Click the three dot menu icon to the right of Xero and select **Disconnect**. +6. Click **Disconnect** to confirm. + +You will no longer see the imported options from Xero. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md new file mode 100644 index 000000000000..0cf642c76e4c --- /dev/null +++ b/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md @@ -0,0 +1,72 @@ +--- +title: Approve and Pay Expenses +description: Approve, hold, or pay expenses submitted to you +--- +
+ +When expenses are sent to you for approval, you have the option to: +- Approve and pay the expenses. +- Hold the expenses if payment needs to be delayed or if the expenses require additional information before they can be approved. + +{% include info.html %} +If your workspace does not require expense approvals, or if the expense is sent to you by a friend, you will not need to approve the expense and instead can immediately pay the expense when you are ready. +{% include end-info.html %} + +# Approve expenses + +When someone sends an expense or a group of expenses to you for approval, you’ll receive the expense in Expensify Chat for the related workspace. Chats with new updates appear with a green dot to the right of the chat message. Concierge also sends you an email notification for the new expense. + +To approve an expense, + +1. Open the Expensify Chat thread for the expense. +2. Click the expense or group of expenses. +3. Review the expense details to ensure they are correct. Look at each receipt, the amount, the description, and any additional details. +4. Determine the next steps. + - **Approve**: When you’re satisfied with the expense, click **Approve**. + - **Handle holds**: If any of the expenses are on hold, you can choose to either approve only the expenses that are not on hold or approve the full amount, including any held expenses. + - **Request changes**: You can add a comment to the expense’s chat thread in your Expensify Chat inbox to request changes to the expense details. + +{% include info.html %} +Admins can modify an expense, if needed. +{% include end-info.html %} + +You’re now ready to pay the expense. + +# Hold an expense + +If you need to delay a payment or if you need more information on the expense before it can be approved, you can hold the expense. + +To hold an expense, + +1. Open the Expensify Chat thread for the expense. +2. Click the expense or group of expenses. +3. Click the three dot menu at the top right of the expense and select **Hold**. +4. Enter a reason for the delay. +5. Review the Hold Overview page and click **Got It**. + +When you’re ready, you can choose to: +- **Remove the hold**: Complete the steps above and select **Unhold**. +- **Approve the expense**: Complete the steps above for “Approve expenses.” +Once the expense has been approved, you can now pay the expense. + +{% include info.html %} +Held expenses will not be available for payment until they have been approved. +{% include end-info.html %} + +# Pay expenses + +Once you’ve approved an expense—or if the expense does not require approval—you’ll be able to pay it. + +{% include info.html %} +To pay expenses within Expensify, you’ll need to set up your Expensify Wallet. +{% include end-info.html %} + +To pay an expense, + +1. Open the Expensify Chat thread for the expense. +2. Click the expense or group of expenses. +3. Select a payment option. + - Click **Pay** to pay the full expense within Expensify. If the expenses contain one that has been held, the pay amount will only include the expenses that have not been held. Then you’ll select your payment method. + - Click **Pay Elsewhere** to indicate that a payment has been sent using a method outside of Expensify, such as cash or a check. This will label the expense as Paid. + +
diff --git a/docs/articles/new-expensify/expensify-card/Check-Expensify-Card-limit.md b/docs/articles/new-expensify/expensify-card/Check-Expensify-Card-limit.md new file mode 100644 index 000000000000..803211c873ec --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Check-Expensify-Card-limit.md @@ -0,0 +1,27 @@ +--- +title: Check Expensify Card limit +description: View the available limit for your Expensify Card +--- +
+ +Your Expensify Visa® Commercial Card has a Smart Limit that is automatically updated after you make a purchase with your physical or virtual card. Your base limit is assigned by your admin. + +To check your Smart Limit, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Wallet** in the left menu. +3. Click your Expensify Card to see the available Smart Limit. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Wallet**. +3. Tap your Expensify Card to see the available Smart Limit. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/expensify-card/Coming-Soon.md b/docs/articles/new-expensify/expensify-card/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/expensify-card/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- diff --git a/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md b/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md new file mode 100644 index 000000000000..6c7457641ce6 --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md @@ -0,0 +1,56 @@ +--- +title: Use your Expensify Card +description: Use your physical or virtual Expensify Card +--- +
+ +As soon as you receive your physical Expensify Visa® Commercial Card, you can start using it right away by swiping it like you would with any other card, or you can link your card to your Apple or Google Pay mobile wallet to make in-person, contactless payments. You can also use your virtual Expensify Card for online and in-app purchases. + +A virtual card is a digital card that can be used for online transactions. Virtual cards have the same details as physical cards, but they offer several additional benefits: +- **Flexibility**: Virtual cards can be created or deleted instantly. You can use them for individual transactions with predetermined amounts or recurring payments and subscriptions. +- **Customizable limits**: You can set spending limits for each virtual card. +- **Security**: Admins have the option to issue virtual cards for a single-use (e.g. for one of expenses) or fixed-use (e.g. for recurring expenses). Since you have placed a limit on their usage, it makes them less susceptible to unauthorized transactions. +- **Insights**: You can easily track recurring spend for specific vendors when assigning a virtual card to a team, department, or vendor. + +# View your virtual card details + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Wallet** in the left menu. +3. Click your Expensify Card. +4. Click **Reveal Details** to view your virtual Expensify Card number, expiration date, CVV, and address. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Wallet**. +3. Tap your Expensify Card. +4. Tap **Reveal Details** to view your virtual Expensify Card number, expiration date, CVV, and address. +{% include end-option.html %} + +{% include end-selector.html %} + +{% include faq-begin.md %} + +**Why did my transaction get declined?** + +Here are some reasons why an Expensify Card transaction might be declined: + +- **Insufficient card limit**: If a transaction exceeds your Expensify Card’s available limit, the transaction will be declined. Submitting expenses and getting them approved will free up your limit for more spending. +- **Inactive card**: Your card isn’t active yet or it was disabled by your Domain Admin +- **Incorrect card details**: Your card information was entered incorrectly with the merchant. Entering incorrect card information, such as the CVC, ZIP, or expiration date, will also lead to declines. There was suspicious activity +- **Fraudulent or risky activity**: If Expensify detects unusual or suspicious activity, we may block transactions as a security measure. This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unusual merchants and try again. If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. The merchant is located in a restricted country + +**How do I report my Expensify Card expenses?** + +You can report and submit Expensify Card expenses just like any other expenses, and you’ll want to submit them regularly to ensure you have a sufficient spending amount available on the card. As your expenses are approved, your Smart Limit updates accordingly. + +{% include info.html %} +SmartScanned receipts should automatically attach to the related Expensify Card expense. Expensify also automatically generates an IRS-compliant eReceipt for every transaction as long as the expense isn’t lodging-related. If your organization doesn’t require itemized receipts, you can rely on eReceipts instead. +{% include end-info.html %} + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/workspaces/Coming-Soon.md b/docs/articles/new-expensify/workspaces/Coming-Soon.md deleted file mode 100644 index 266784414761..000000000000 --- a/docs/articles/new-expensify/workspaces/Coming-Soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Coming Soon! -description: More info coming soon! ---- - - diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md index 294dcfc57a23..8f2cf0897ad0 100644 --- a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md +++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md @@ -14,7 +14,7 @@ To require workspace members to add tags and/or categories to their expenses, 3. Select a workspace. 4. Click **Tags** or **Categories** in the left menu. 5. Click **Settings** at the top right of the page. -6. Enable the “Members must tag/categorize all spend” toggle. +6. Enable the “Members must tag/categorize all expenses" toggle. 7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). {% include end-option.html %} @@ -24,7 +24,7 @@ To require workspace members to add tags and/or categories to their expenses, 3. Select a workspace. 4. Tap **Tags** or **Categories**. 5. Tap **Settings** at the top right of the page. -6. Enable the “Members must tag/categorize all spend” toggle. +6. Enable the “Members must tag/categorize all expenses" toggle. 7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). {% include end-option.html %} diff --git a/docs/redirects.csv b/docs/redirects.csv index 74fa9c697f5f..5e4d06619653 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -172,4 +172,8 @@ https://help.expensify.com/articles/new-expensify/getting-started/Free-plan-upgr https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account,https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account https://help.expensify.com/articles/new-expensify/settings/Profile,https://help.expensify.com/new-expensify/hubs/settings/ https://help.expensify.com/articles/new-expensify/expenses/Referral-Program.html,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Referral-Program +https://help.expensify.com/articles/new-expensify/workspaces/The-Free-Plan,https://help.expensify.com/new-expensify/hubs/workspaces/ +https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account +https://help.expensify.com/articles/new-expensify/settings/Security,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security https://help.expensify.com/articles/expensify-classic/workspaces/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency +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 diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 94118eb3bfa4..f86952ca7aca 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -648,6 +648,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FullStory/FullStory.framework/FullStory", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", @@ -659,6 +660,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FullStory.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", @@ -680,6 +682,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FullStory/FullStory.framework/FullStory", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", @@ -691,6 +694,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FullStory.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", @@ -935,7 +939,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -974,7 +978,7 @@ DEVELOPMENT_TEAM = 368M544MTT; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1057,7 +1061,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1142,7 +1146,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1228,7 +1232,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1308,7 +1312,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1386,7 +1390,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1465,7 +1469,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1727,7 +1731,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1872,7 +1876,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2010,7 +2014,7 @@ DEVELOPMENT_TEAM = 368M544MTT; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2145,7 +2149,7 @@ DEVELOPMENT_TEAM = 368M544MTT; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9c9e78ef70b0..2884e1b4318d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.74 + 1.4.76 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.74.4 + 1.4.76.6 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 627c6dbf826c..628d11fb7896 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.74 + 1.4.76 CFBundleSignature ???? CFBundleVersion - 1.4.74.4 + 1.4.76.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 477f83aa6d00..48f43e75badf 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.74 + 1.4.76 CFBundleVersion - 1.4.74.4 + 1.4.76.6 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index 4f00eb2adfdd..d72086d4c07b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -117,3 +117,5 @@ end 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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d0155051fc3b..1ea6b65a58b7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,6 +138,27 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (6.2.1) + - FullStory (1.48.0) + - fullstory_react-native (1.4.2): + - FullStory (~> 1.14) + - 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 - glog (0.3.5) - GoogleAppMeasurement (8.8.0): - GoogleAppMeasurement/AdIdSupport (= 8.8.0) @@ -1731,7 +1752,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNCPicker (2.6.1): + - RNCPicker (2.7.6): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1831,7 +1852,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.69): + - RNLiveMarkdown (0.1.70): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1849,9 +1870,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.69) + - RNLiveMarkdown/common (= 0.1.70) - Yoga - - RNLiveMarkdown/common (0.1.69): + - RNLiveMarkdown/common (0.1.70): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2077,6 +2098,8 @@ 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_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`) - libevent (~> 2.1.12) @@ -2239,6 +2262,10 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" + FullStory: + :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + fullstory_react-native: + :path: "../node_modules/@fullstory/react-native" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: @@ -2429,6 +2456,10 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + FullStory: + :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + SPEC CHECKSUMS: Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 AirshipFrameworkProxy: 7255f4ed9836dc2920f2f1ea5657ced4cee8a35c @@ -2454,6 +2485,8 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + FullStory: 097347c823c21c655ca25fd8d5e6355a9326ec54 + fullstory_react-native: 6cba8a2c054374a24a44dc4310407d9435459cae glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -2545,7 +2578,7 @@ SPEC CHECKSUMS: ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 RNCClipboard: 081418ae3b391b1012c3f41d045e5e39f1beed71 - RNCPicker: a37026a67de0cf1a33ffe8722783527e3b18ea9f + RNCPicker: 106d11a1c159ce937009b2bd52db2bdb1577454f RNDeviceInfo: 449272e9faf2afe94a3fe2896d169e92277fffa8 RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c @@ -2556,7 +2589,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: bfabd5938e5af5afc1e60e4e34286b17f8308184 + RNLiveMarkdown: 23250f3d64c9d5f82ff36c4733c03544af0222d2 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 @@ -2573,8 +2606,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 -PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d +PODFILE CHECKSUM: 66a5c97ae1059e4da1993a4ad95abe5d819f555b COCOAPODS: 1.13.0 diff --git a/jest/setup.ts b/jest/setup.ts index 488e3e36a1d3..174e59a7e493 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -2,9 +2,11 @@ import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import 'setimmediate'; +import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; setupMockImages(); +mockFSLibrary(); // This mock is required as per setup instructions for react-navigation testing // https://reactnavigation.org/docs/testing/#mocking-native-modules diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts new file mode 100644 index 000000000000..9edfccab9441 --- /dev/null +++ b/jest/setupMockFullstoryLib.ts @@ -0,0 +1,24 @@ +type FSPageInterface = { + start: jest.Mock; +}; + +export default function mockFSLibrary() { + jest.mock('@fullstory/react-native', () => { + class Fullstory { + consent = jest.fn(); + + anonymize = jest.fn(); + + identify = jest.fn(); + } + + return { + FSPage(): FSPageInterface { + return { + start: jest.fn(), + }; + }, + default: Fullstory, + }; + }); +} diff --git a/package-lock.json b/package-lock.json index a46198daa88a..fb1d180847c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,28 @@ { "name": "new.expensify", - "version": "1.4.74-4", + "version": "1.4.76-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.74-4", + "version": "1.4.76-6", "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.70", + "@expensify/react-native-live-markdown": "0.1.76", "@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-react-native": "^1.2.1", + "@fullstory/browser": "^2.0.3", + "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -34,7 +37,7 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-google-signin/google-signin": "^10.0.1", - "@react-native-picker/picker": "2.6.1", + "@react-native-picker/picker": "2.7.6", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.29", @@ -56,7 +59,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -100,7 +103,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.32", + "react-native-onyx": "2.0.41", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -109,7 +112,7 @@ "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", - "react-native-reanimated": "^3.7.2", + "react-native-reanimated": "^3.8.0", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", @@ -125,7 +128,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", - "react-pdf": "^7.7.0", + "react-pdf": "^7.7.3", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -201,7 +204,7 @@ "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^29.3.3", + "electron": "^29.4.1", "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -238,7 +241,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "^4.10.2", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", @@ -3569,9 +3572,14 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.70.tgz", - "integrity": "sha512-HyqBtZyvuJFB4gIUECKIMxWCnTPlPj+GPWmw80VzMBRFV9QiFRKUKRWefNEJ1cXV5hl8a6oOWDQla+dCnjCzOQ==", + "version": "0.1.76", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.76.tgz", + "integrity": "sha512-JUXiLg0Y2FJiVOfZKRgoOP1no8ThPaJ6MBc122UsW6SG53OvS12MTHfgfKHjXRH1nIGro/p9ekYb8GAzpp+kdw==", + "workspaces": [ + "parser", + "example", + "WebExample" + ], "engines": { "node": ">= 18.0.0" }, @@ -5589,6 +5597,52 @@ "tslib": "^2.4.0" } }, + "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==" + }, + "node_modules/@fullstory/babel-plugin-react-native": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-react-native/-/babel-plugin-react-native-1.2.1.tgz", + "integrity": "sha512-EMAgoPOo+31eppHxQf05oAGhKKTem7rw8GHDdbNJF0c5dQWzBGNVgF72TPjcxES91UI6hbss2eqoVOhUttLEoQ==", + "dependencies": { + "@babel/parser": "^7.0.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@fullstory/browser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-2.0.3.tgz", + "integrity": "sha512-usjH8FB1O2LiSWoblsuKhFhlYDGpIPuyQVOx4JXtxm9QmQARdKZdNq1vPijxuDvOGjhwtVZa4JmhvByRRuDPnQ==", + "dependencies": { + "@fullstory/snippet": "2.0.3" + } + }, + "node_modules/@fullstory/react-native": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@fullstory/react-native/-/react-native-1.4.2.tgz", + "integrity": "sha512-Ig85ghn5UN+Tc1JWL/y4hY9vleeaVHL3f6qH9W4odDNP4XAv29+G82nIYQhBOQGoVnIQ4oQFQftir/dqAbidSw==", + "dependencies": { + "@fullstory/babel-plugin-annotate-react": "^2.2.0", + "@fullstory/babel-plugin-react-native": "^1.1.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": ">=0.61.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@fullstory/snippet": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@fullstory/snippet/-/snippet-2.0.3.tgz", + "integrity": "sha512-EaCuTQSLv5FvnjHLbTxErn3sS1+nLqf1p6sA/c4PV49stBtkUakA0eLhJJdaw0WLdXyEzZXf86lRNsjEzrgGPw==" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "license": "MIT" @@ -8854,12 +8908,12 @@ } }, "node_modules/@react-native-picker/picker": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.6.1.tgz", - "integrity": "sha512-oJftvmLOj6Y6/bF4kPcK6L83yNBALGmqNYugf94BzP0FQGpHBwimVN2ygqkQ2Sn2ZU3pGUZMs0jV6+Gku2GyYg==", + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.7.6.tgz", + "integrity": "sha512-Cs3PxRmE2vu6TofM9vt9TV8ZYFOtEPSupNxwoorH9lpkKM9HGG8QwK2i29KOEoODpUbtudKHUTtqhMZSuX9pgA==", "peerDependencies": { - "react": ">=16", - "react-native": ">=0.57" + "react": "*", + "react-native": "*" } }, "node_modules/@react-native/assets-registry": { @@ -18568,9 +18622,9 @@ } }, "node_modules/electron": { - "version": "29.3.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.3.3.tgz", - "integrity": "sha512-I/USTe9UsQUKb/iuiYnmt074vHxNHCJZWYiU4Xg6lNPKVBsPadAhZcc+g2gYLqC1rA7KT4AvKTmNsY8n7oEUCw==", + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.1.tgz", + "integrity": "sha512-YQvMAtdmjMF1yGfQFuO/KOmy+04SKot85NalppK/8zxKwOKrrK6dJBp+nJWteqBwRAKiasSrC1lDalF6hZct/w==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -20301,8 +20355,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", - "integrity": "sha512-9BHjM3kZs7/dil0oykEQFkEhXjVD5liTttmO7ZYtPZkl4j6g97mubY2p9lYpWwpkWckUfvU7nGuZQjahw9xSFA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", + "integrity": "sha512-uy1+axUTTuPKwAR06xNG/tGIJ+uaavmSQgKiNU7pQVR94ibNzDD2WESn2E7OEP9/QrHa61lfFlluTjFvvz5I8Q==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -31431,9 +31485,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.32.tgz", - "integrity": "sha512-tB9wqMJGTLOYfrfplRP+9aq5JdD8w/hV/OZsMAVH+ewbE1zLY8OymUsAsIFdF1v+cB8HhehP569JVLZmhm6bsg==", + "version": "2.0.41", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.41.tgz", + "integrity": "sha512-33r0sVBq7MV/GZwRneRt81uxgW8x3YG75VNJvThycB/dkCnGCfbxoVkZADVH3ET3jzfFXy9wnS06sZnZp78zMQ==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -31554,10 +31608,15 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.7.2", - "license": "MIT", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.8.1.tgz", + "integrity": "sha512-EdM0vr3JEaNtqvstqESaPfOBy0gjYBkr1iEolWJ82Ax7io8y9OVUIphgsLKTB36CtR1XtmBw0RZVj7KArc7ZVA==", "dependencies": { - "@babel/plugin-transform-object-assign": "^7.16.7", + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4" @@ -31959,9 +32018,9 @@ } }, "node_modules/react-pdf": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.1.tgz", - "integrity": "sha512-cbbf/PuRtGcPPw+HLhMI1f6NSka8OJgg+j/yPWTe95Owf0fK6gmVY7OXpTxMeh92O3T3K3EzfE0ML0eXPGwR5g==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.3.tgz", + "integrity": "sha512-a2VfDl8hiGjugpqezBTUzJHYLNB7IS7a2t7GD52xMI9xHg8LdVaTMsnM9ZlNmKadnStT/tvX5IfV0yLn+JvYmw==", "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", @@ -36029,9 +36088,10 @@ } }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 1a449f42bece..88b47e803558 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.74-4", + "version": "1.4.76-6", "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.", @@ -65,13 +65,16 @@ "@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.70", + "@expensify/react-native-live-markdown": "0.1.76", "@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-react-native": "^1.2.1", + "@fullstory/browser": "^2.0.3", + "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -86,7 +89,7 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-google-signin/google-signin": "^10.0.1", - "@react-native-picker/picker": "2.6.1", + "@react-native-picker/picker": "2.7.6", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.29", @@ -108,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -152,7 +155,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.32", + "react-native-onyx": "2.0.41", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -161,7 +164,7 @@ "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", - "react-native-reanimated": "^3.7.2", + "react-native-reanimated": "^3.8.0", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", @@ -177,7 +180,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", - "react-pdf": "^7.7.0", + "react-pdf": "^7.7.3", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -253,7 +256,7 @@ "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^29.3.3", + "electron": "^29.4.1", "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -290,7 +293,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "^4.10.2", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", diff --git a/patches/@shopify+flash-list+1.6.3.patch b/patches/@shopify+flash-list+1.6.3.patch index ab347fbb4e9c..e3d690055ff8 100644 --- a/patches/@shopify+flash-list+1.6.3.patch +++ b/patches/@shopify+flash-list+1.6.3.patch @@ -867,7 +867,7 @@ index 023b94a..0000000 -{"program":{"fileNames":["../node_modules/typescript/lib/lib.es5.d.ts","../node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/typescript/lib/lib.dom.d.ts","../node_modules/typescript/lib/lib.dom.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/typescript/lib/lib.esnext.intl.d.ts","../node_modules/tslib/tslib.d.ts","../node_modules/@types/react-native/modules/BatchedBridge.d.ts","../node_modules/@types/react-native/modules/Codegen.d.ts","../node_modules/@types/react-native/modules/Devtools.d.ts","../node_modules/@types/react-native/modules/globals.d.ts","../node_modules/@types/react-native/modules/LaunchScreen.d.ts","../node_modules/@types/react/global.d.ts","../node_modules/csstype/index.d.ts","../node_modules/@types/prop-types/index.d.ts","../node_modules/@types/scheduler/tracing.d.ts","../node_modules/@types/react/index.d.ts","../node_modules/@types/react-native/private/Utilities.d.ts","../node_modules/@types/react-native/public/Insets.d.ts","../node_modules/@types/react-native/Libraries/ReactNative/RendererProxy.d.ts","../node_modules/@types/react-native/public/ReactNativeTypes.d.ts","../node_modules/@types/react-native/Libraries/Types/CoreEventTypes.d.ts","../node_modules/@types/react-native/public/ReactNativeRenderer.d.ts","../node_modules/@types/react-native/Libraries/Components/Touchable/Touchable.d.ts","../node_modules/@types/react-native/Libraries/Components/View/ViewAccessibility.d.ts","../node_modules/@types/react-native/Libraries/Components/View/ViewPropTypes.d.ts","../node_modules/@types/react-native/Libraries/Components/RefreshControl/RefreshControl.d.ts","../node_modules/@types/react-native/Libraries/Components/ScrollView/ScrollView.d.ts","../node_modules/@types/react-native/Libraries/Components/View/View.d.ts","../node_modules/@types/react-native/Libraries/Image/ImageResizeMode.d.ts","../node_modules/@types/react-native/Libraries/Image/ImageSource.d.ts","../node_modules/@types/react-native/Libraries/Image/Image.d.ts","../node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.d.ts","../node_modules/@react-native/virtualized-lists/index.d.ts","../node_modules/@types/react-native/Libraries/Lists/FlatList.d.ts","../node_modules/@types/react-native/Libraries/Lists/SectionList.d.ts","../node_modules/@types/react-native/Libraries/Text/Text.d.ts","../node_modules/@types/react-native/Libraries/Animated/Animated.d.ts","../node_modules/@types/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts","../node_modules/@types/react-native/Libraries/StyleSheet/StyleSheet.d.ts","../node_modules/@types/react-native/Libraries/StyleSheet/processColor.d.ts","../node_modules/@types/react-native/Libraries/ActionSheetIOS/ActionSheetIOS.d.ts","../node_modules/@types/react-native/Libraries/Alert/Alert.d.ts","../node_modules/@types/react-native/Libraries/Animated/Easing.d.ts","../node_modules/@types/react-native/Libraries/Animated/useAnimatedValue.d.ts","../node_modules/@types/react-native/Libraries/vendor/emitter/EventEmitter.d.ts","../node_modules/@types/react-native/Libraries/EventEmitter/RCTDeviceEventEmitter.d.ts","../node_modules/@types/react-native/Libraries/EventEmitter/RCTNativeAppEventEmitter.d.ts","../node_modules/@types/react-native/Libraries/AppState/AppState.d.ts","../node_modules/@types/react-native/Libraries/BatchedBridge/NativeModules.d.ts","../node_modules/@types/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts","../node_modules/@types/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.d.ts","../node_modules/@types/react-native/Libraries/Components/Clipboard/Clipboard.d.ts","../node_modules/@types/react-native/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.d.ts","../node_modules/@types/react-native/Libraries/EventEmitter/NativeEventEmitter.d.ts","../node_modules/@types/react-native/Libraries/Components/Keyboard/Keyboard.d.ts","../node_modules/@types/react-native/private/TimerMixin.d.ts","../node_modules/@types/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.d.ts","../node_modules/@types/react-native/Libraries/Components/Pressable/Pressable.d.ts","../node_modules/@types/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.d.ts","../node_modules/@types/react-native/Libraries/Components/SafeAreaView/SafeAreaView.d.ts","../node_modules/@types/react-native/Libraries/Components/StatusBar/StatusBar.d.ts","../node_modules/@types/react-native/Libraries/Components/Switch/Switch.d.ts","../node_modules/@types/react-native/Libraries/Components/TextInput/InputAccessoryView.d.ts","../node_modules/@types/react-native/Libraries/Components/TextInput/TextInput.d.ts","../node_modules/@types/react-native/Libraries/Components/ToastAndroid/ToastAndroid.d.ts","../node_modules/@types/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.d.ts","../node_modules/@types/react-native/Libraries/Components/Touchable/TouchableHighlight.d.ts","../node_modules/@types/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts","../node_modules/@types/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.d.ts","../node_modules/@types/react-native/Libraries/Components/Button.d.ts","../node_modules/@types/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.d.ts","../node_modules/@types/react-native/Libraries/Interaction/InteractionManager.d.ts","../node_modules/@types/react-native/Libraries/Interaction/PanResponder.d.ts","../node_modules/@types/react-native/Libraries/LayoutAnimation/LayoutAnimation.d.ts","../node_modules/@types/react-native/Libraries/Linking/Linking.d.ts","../node_modules/@types/react-native/Libraries/LogBox/LogBox.d.ts","../node_modules/@types/react-native/Libraries/Modal/Modal.d.ts","../node_modules/@types/react-native/Libraries/Performance/Systrace.d.ts","../node_modules/@types/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.d.ts","../node_modules/@types/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.d.ts","../node_modules/@types/react-native/Libraries/Utilities/IPerformanceLogger.d.ts","../node_modules/@types/react-native/Libraries/ReactNative/AppRegistry.d.ts","../node_modules/@types/react-native/Libraries/ReactNative/I18nManager.d.ts","../node_modules/@types/react-native/Libraries/ReactNative/RootTag.d.ts","../node_modules/@types/react-native/Libraries/ReactNative/UIManager.d.ts","../node_modules/@types/react-native/Libraries/ReactNative/requireNativeComponent.d.ts","../node_modules/@types/react-native/Libraries/Settings/Settings.d.ts","../node_modules/@types/react-native/Libraries/Share/Share.d.ts","../node_modules/@types/react-native/Libraries/StyleSheet/PlatformColorValueTypesIOS.d.ts","../node_modules/@types/react-native/Libraries/StyleSheet/PlatformColorValueTypes.d.ts","../node_modules/@types/react-native/Libraries/TurboModule/RCTExport.d.ts","../node_modules/@types/react-native/Libraries/TurboModule/TurboModuleRegistry.d.ts","../node_modules/@types/react-native/Libraries/Utilities/Appearance.d.ts","../node_modules/@types/react-native/Libraries/Utilities/BackHandler.d.ts","../node_modules/@types/react-native/Libraries/Utilities/DevSettings.d.ts","../node_modules/@types/react-native/Libraries/Utilities/Dimensions.d.ts","../node_modules/@types/react-native/Libraries/Utilities/PixelRatio.d.ts","../node_modules/@types/react-native/Libraries/Utilities/Platform.d.ts","../node_modules/@types/react-native/Libraries/Vibration/Vibration.d.ts","../node_modules/@types/react-native/Libraries/YellowBox/YellowBoxDeprecated.d.ts","../node_modules/@types/react-native/Libraries/vendor/core/ErrorUtils.d.ts","../node_modules/@types/react-native/public/DeprecatedPropertiesAlias.d.ts","../node_modules/@types/react-native/index.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/dependencies/ContextProvider.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/dependencies/DataProvider.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/layoutmanager/LayoutManager.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/dependencies/LayoutProvider.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/dependencies/GridLayoutProvider.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/scrollcomponent/BaseScrollView.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/ViewabilityTracker.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/VirtualRenderer.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/ItemAnimator.d.ts","../node_modules/recyclerlistview/dist/reactnative/utils/ComponentCompat.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/RecyclerListView.d.ts","../node_modules/recyclerlistview/dist/reactnative/utils/AutoScroll.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/layoutmanager/GridLayoutManager.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/ProgressiveListView.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/devutils/debughandlers/resize/ResizeDebugHandler.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/devutils/debughandlers/DebugHandlers.d.ts","../node_modules/recyclerlistview/dist/reactnative/index.d.ts","../node_modules/recyclerlistview/dist/reactnative/core/StickyContainer.d.ts","../node_modules/recyclerlistview/sticky/index.d.ts","../src/native/auto-layout/AutoLayoutViewNativeComponentProps.ts","../src/native/auto-layout/AutoLayoutViewNativeComponent.ts","../src/native/auto-layout/AutoLayoutView.tsx","../src/native/cell-container/CellContainer.tsx","../src/PureComponentWrapper.tsx","../src/viewability/ViewToken.ts","../src/FlashListProps.ts","../src/utils/AverageWindow.ts","../src/utils/ContentContainerUtils.ts","../src/GridLayoutProviderWithProps.ts","../src/errors/CustomError.ts","../src/errors/ExceptionList.ts","../src/errors/Warnings.ts","../src/viewability/ViewabilityHelper.ts","../src/viewability/ViewabilityManager.ts","../node_modules/recyclerlistview/dist/reactnative/platform/reactnative/itemanimators/defaultjsanimator/DefaultJSItemAnimator.d.ts","../src/native/config/PlatformHelper.ts","../src/FlashList.tsx","../src/AnimatedFlashList.ts","../src/MasonryFlashList.tsx","../src/benchmark/AutoScrollHelper.ts","../src/benchmark/roundToDecimalPlaces.ts","../src/benchmark/JSFPSMonitor.ts","../src/benchmark/useBlankAreaTracker.ts","../src/benchmark/useBenchmark.ts","../src/benchmark/useDataMultiplier.ts","../src/benchmark/useFlatListBenchmark.ts","../src/index.ts","../src/__tests__/AverageWindow.test.ts","../src/__tests__/ContentContainerUtils.test.ts","../node_modules/@quilted/react-testing/build/typescript/types.d.ts","../node_modules/@quilted/react-testing/build/typescript/matchers/index.d.ts","../node_modules/@quilted/react-testing/build/typescript/environment.d.ts","../node_modules/@quilted/react-testing/build/typescript/implementations/test-renderer.d.ts","../node_modules/@quilted/react-testing/build/typescript/index.d.ts","../src/__tests__/helpers/mountFlashList.tsx","../src/__tests__/FlashList.test.tsx","../src/__tests__/GridLayoutProviderWithProps.test.ts","../src/__tests__/helpers/mountMasonryFlashList.tsx","../src/__tests__/MasonryFlashList.test.ts","../src/native/config/PlatformHelper.web.ts","../src/__tests__/PlatformHelper.web.test.ts","../src/__tests__/ViewabilityHelper.test.ts","../src/__tests__/useBlankAreaTracker.test.tsx","../src/native/auto-layout/AutoLayoutViewNativeComponent.android.ts","../src/native/auto-layout/AutoLayoutViewNativeComponent.ios.ts","../src/native/cell-container/CellContainer.android.ts","../src/native/cell-container/CellContainer.ios.ts","../src/native/cell-container/CellContainer.web.tsx","../src/native/config/PlatformHelper.android.ts","../src/native/config/PlatformHelper.ios.ts","../node_modules/@babel/types/lib/index.d.ts","../node_modules/@types/babel__generator/index.d.ts","../node_modules/@babel/parser/typings/babel-parser.d.ts","../node_modules/@types/babel__template/index.d.ts","../node_modules/@types/babel__traverse/index.d.ts","../node_modules/@types/babel__core/index.d.ts","../node_modules/@types/node/assert.d.ts","../node_modules/@types/node/assert/strict.d.ts","../node_modules/@types/node/globals.d.ts","../node_modules/@types/node/async_hooks.d.ts","../node_modules/@types/node/buffer.d.ts","../node_modules/@types/node/child_process.d.ts","../node_modules/@types/node/cluster.d.ts","../node_modules/@types/node/console.d.ts","../node_modules/@types/node/constants.d.ts","../node_modules/@types/node/crypto.d.ts","../node_modules/@types/node/dgram.d.ts","../node_modules/@types/node/diagnostics_channel.d.ts","../node_modules/@types/node/dns.d.ts","../node_modules/@types/node/dns/promises.d.ts","../node_modules/@types/node/domain.d.ts","../node_modules/@types/node/events.d.ts","../node_modules/@types/node/fs.d.ts","../node_modules/@types/node/fs/promises.d.ts","../node_modules/@types/node/http.d.ts","../node_modules/@types/node/http2.d.ts","../node_modules/@types/node/https.d.ts","../node_modules/@types/node/inspector.d.ts","../node_modules/@types/node/module.d.ts","../node_modules/@types/node/net.d.ts","../node_modules/@types/node/os.d.ts","../node_modules/@types/node/path.d.ts","../node_modules/@types/node/perf_hooks.d.ts","../node_modules/@types/node/process.d.ts","../node_modules/@types/node/punycode.d.ts","../node_modules/@types/node/querystring.d.ts","../node_modules/@types/node/readline.d.ts","../node_modules/@types/node/repl.d.ts","../node_modules/@types/node/stream.d.ts","../node_modules/@types/node/stream/promises.d.ts","../node_modules/@types/node/stream/consumers.d.ts","../node_modules/@types/node/stream/web.d.ts","../node_modules/@types/node/string_decoder.d.ts","../node_modules/@types/node/timers.d.ts","../node_modules/@types/node/timers/promises.d.ts","../node_modules/@types/node/tls.d.ts","../node_modules/@types/node/trace_events.d.ts","../node_modules/@types/node/tty.d.ts","../node_modules/@types/node/url.d.ts","../node_modules/@types/node/util.d.ts","../node_modules/@types/node/v8.d.ts","../node_modules/@types/node/vm.d.ts","../node_modules/@types/node/wasi.d.ts","../node_modules/@types/node/worker_threads.d.ts","../node_modules/@types/node/zlib.d.ts","../node_modules/@types/node/globals.global.d.ts","../node_modules/@types/node/index.d.ts","../node_modules/@types/graceful-fs/index.d.ts","../node_modules/@types/istanbul-lib-coverage/index.d.ts","../node_modules/@types/istanbul-lib-report/index.d.ts","../node_modules/@types/istanbul-reports/index.d.ts","../node_modules/chalk/index.d.ts","../node_modules/@sinclair/typebox/typebox.d.ts","../node_modules/@jest/schemas/build/index.d.ts","../node_modules/pretty-format/build/index.d.ts","../node_modules/jest-diff/build/index.d.ts","../node_modules/jest-matcher-utils/build/index.d.ts","../node_modules/@types/jest/index.d.ts","../node_modules/@types/json-schema/index.d.ts","../node_modules/@types/json5/index.d.ts","../node_modules/@types/parse-json/index.d.ts","../node_modules/@types/prettier/index.d.ts","../node_modules/@types/react-test-renderer/index.d.ts","../node_modules/@types/scheduler/index.d.ts","../node_modules/@types/stack-utils/index.d.ts","../node_modules/@types/websocket/index.d.ts","../node_modules/@types/yargs-parser/index.d.ts","../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"f5c28122bee592cfaf5c72ed7bcc47f453b79778ffa6e301f45d21a0970719d4","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","e6b724280c694a9f588847f754198fb96c43d805f065c3a5b28bbc9594541c84","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3f149f903dd20dfeb7c80e228b659f0e436532de772469980dbd00702cc05cc1","affectsGlobalScope":true},{"version":"1272277fe7daa738e555eb6cc45ded42cc2d0f76c07294142283145d49e96186","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"43fb1d932e4966a39a41b464a12a81899d9ae5f2c829063f5571b6b87e6d2f9c","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"0d5f52b3174bee6edb81260ebcd792692c32c81fd55499d69531496f3f2b25e7","affectsGlobalScope":true},{"version":"810627a82ac06fb5166da5ada4159c4ec11978dfbb0805fe804c86406dab8357","affectsGlobalScope":true},{"version":"181f1784c6c10b751631b24ce60c7f78b20665db4550b335be179217bacc0d5f","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"75ec0bdd727d887f1b79ed6619412ea72ba3c81d92d0787ccb64bab18d261f14","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"12a310447c5d23c7d0d5ca2af606e3bd08afda69100166730ab92c62999ebb9d","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"cd483c056da900716879771893a3c9772b66c3c88f8943b4205aec738a94b1d0","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"c37f8a49593a0030eecb51bbfa270e709bec9d79a6cc3bb851ef348d4e6b26f8","affectsGlobalScope":true},"14a84fbe4ec531dcbaf5d2594fd95df107258e60ae6c6a076404f13c3f66f28e",{"version":"1c0e04c54479b57b49fec4e93556974b3d071b65d0b750897e07b3b7d2145fc5","affectsGlobalScope":true},"bc1852215dc1488e6747ca43ae0605041de22ab9a6eeef39542d29837919c414","ae6da60c852e7bacc4a49ff14a42dc1a3fdbb44e11bd9b4acb1bf3d58866ee71",{"version":"0dab023e564abb43c817779fff766e125017e606db344f9633fdba330c970532","affectsGlobalScope":true},"4cbd76eafece5844dc0a32807e68047aecbdd8d863edba651f34c050624f18df",{"version":"ecf78e637f710f340ec08d5d92b3f31b134a46a4fcf2e758690d8c46ce62cba6","affectsGlobalScope":true},"ea0aa24a32c073b8639aa1f3130ba0add0f0f2f76b314d9ba988a5cb91d7e3c4","f7b46d22a307739c145e5fddf537818038fdfffd580d79ed717f4d4d37249380","f5a8b384f182b3851cec3596ccc96cb7464f8d3469f48c74bf2befb782a19de5",{"version":"29b8a3a533884705024eab54e56465614ad167f5dd87fdc2567d8e451f747224","affectsGlobalScope":true},"4f2490e3f420ea6345cade9aee5eada76888848e053726956aaf2af8705477ea","b3ac03d0c853c0ac076a10cfef4dc21d810f54dac5899ade2b1c628c35263533","d17a689ac1bd689f37d6f0d3d9a21afac349e60633844044f7a7b7b9d6f7fd83","019650941b03d4978f62d21ae874788a665c02b54e3268ef2029b02d3b4f7561","ae591c8a4d5c7f7fa44b6965016391457d9c1fd763475f68340599a2a2987a24","fbdef0c642b82cc1713b965f07b4da8005bbbb2c026039bfdc15ca2d20769e38","c2c004e7f1a150541d06bc4a408b96e45ac1f08e0b1b35dfd07fc0f678205f95","1f2081eb2cbeb0828f9baa1dd12cf6d207f8104ae0b085ab9975d11adc7f7e6f","cda9069fc4c312ff484c1373455e4297a02d38ae3bd7d0959aad772a2809623c","c028d20108bcaa3b1fdf3514956a8a90ccf680f18672fa3c92ce5acf81d7ab23","1054f6e8774a75aaf17e7cfea4899344f69590b2db1e06da21048ed1e063c693","9533301b8f75664e1b40a8484a4fd9c77efc04aef526409c2447aab7d12ddc63","b78b5b3fdb4e30976c4263c66c0ad38fb81edcc8075a4160a39d99c6dedd35be","032b51d656feaece529823992f5a39fe9e24d44dfa21b3a149982f7787fc7bdf","5bbfdfb694b019cb2a2022fba361a7a857efc1fc2b77a892c92ebc1349b7e984","46bc25e3501d321a70d0878e82a1d47b16ab77bdf017c8fecc76343f50806a0d","42bacb33cddecbcfe3e043ee1117ba848801749e44f947626765b3e0aec74b1c","49dba0d7a37268e6ae2026e84ad4362eac7e776d816756abf649be7fa177dcd5","5f2b5ab209daae571eb9acc1fd2067ccc94e2a13644579a245875bc4f02b562f","f072acf9547f89b814b9fdb3e72f4ebb1649191591cec99db43d35383906f87f","42450dba65ba1307f27c914a8e45e0b602c6f8f78773c052e42b0b87562f081e","f5870d0ca7b0dfb7e2b9ba9abad3a2e2bffe5c711b53dab2e6e76ca2df58302b","aeb20169389e9f508b1a4eb2a30371b64d64bb7c8543120bc39a3c6b78adfcc9","2a3d3acbab8567057a943f9f56113c0144f5fc561623749fbd6bb5c2b33bf738","9cf21fdcd1beb5142a514887133fa59057e06275bb3070713f3b6d51e830ffa0","0ad4f0b67db47064b404df89c50f99552ce12d6c4bb6154255be61eb6beed094","f8a464b9999126fe1095968c266c0d9c6174612cf256379a1ed1993a87bccdc6","49f981ca657ac160b5de5919ee5602d48bc8f8aac0805107c2ce4fd41dc9a2a1","56e4e08d95a3a7886266a2b4f66b67065c340480d9f1beb73ed7578aa83c639a","eb4360d3818dcd879ee965ae2f4b3fdfdc4149db921b6be338cb7dc7c2bd6710","1c1275f325f13af001aa5873418cb497a26b4b8271f9ad20a45e33f61ea3f9d9","b33e8426136c4f9b349b02c940d23310d350179f790899733aa097ed76457061","05aab001669a230a88820be09a54031c45d9af2488b27d53d4a9c8880ce73e8f","d93a066d4b8b33335dfff910fb25abb8979f8814f8ba45ea902a1360907da1f6","41e97e42d182b4d5f0733ebaad69294faaa507d95e595f317168b8f2325da9ca","debc734fc99b6e1684ed565946bad008913c769d4d2e400d8722c0c23d079c06","5a9f7e087aacb01fa0cdbc36b703a60367239f62beed2507a507199e4c417549","c7c23798fbf564983ed69c1ced3371970d986aaed4801a6e0fb41862550dc034","921f5bce372610ae8948ade7d82decbd2cf56d263de578976189585edd0abac0","ac11f8b13beef593e2f097450a7e214b23dca0d428babd570a2f39582f10e9ab","2499beb5d3e2b4c606977bcc2e08b6ef77b2ecda70e78e0622f5af3bed95c9ba","a11057410396907b84051cbdb8b0cd7f7049d72b58d2b6ac1c14ac2608191a52","bb630c26d487cc45ed107f4f2d3c2a95434716f6367f059de734c40d288c31eb","67cbce0ccdfa96b25de478a93cc493266c152e256c3c96b3d16d1f811e3d881f","19905c928bc4c016d05d915625bb08568447266c4661232faf89f7ddc4417ccc","26204eb4c326e8c975f1b789cbf345c6820205bded6d72e57246a83918d3bc84","618f25b2d41a99216e71817a3bc578991eee86c858c3f0f62a9e70707f4d279d","4cd2947878536ec078e4115b7d53cdcd4dcecd3a8288760caa79098db4f8f61f","2129e984399e94c82b77a32b975f3371ca5ee96341ab9f123474f1a5a1a9921f","798120aaa4952d68cd4b43d6625524c62a135c2f5a3eb705caee98de2355230d","6047365397173788c34bd71fea2bf07a9036d981212efd059b33e52d2c405e97","d7e25d7c03ccf8b10972c2a3a57e29a8d9024e6dbc4ac223baf633a6e8c7145c","6c2e2dead2d80007ee44c429b925d0a7b86f8f3d4c237b2197f7db9f39545dc6","38fbc8f9610fbf4bf619854b26e28c4fbbab16dc1944c4317a4af9bf1ac08d8e","1bd0470a72e6869c330e6e978f15ef32ba2c245249aca097b410448152e8a06b","dd05d7970a92b789f7df3b2252574b2e60f1b9a3758e2839e167b498b8f77159","7092be1889127b2f319efd5d9bdcc0b5cf6fe0740e47247ed039446045518898","0a3d5dbf7c2091017e697ebf9af0a727571f5d99cb4c19e6856212a745c6c355","d05f9c767924db6fb89f6075acb64c042cebdb12779bbd1aaca12c850b772d49","d032678e20ff0f4b8ef6f1e4823b6ae37931b776e8381676dc9141999909b3d7","3e4ab0e8e96e968ac84a2484104892c881ded1757acd81b5e969b6229851f54c","d43a36641f5812794a3b4a941e3dfb5fa070f9fff64cfd6daf5291cb962c8b05","32468df81188116040636844517fbe4f67fc37af4fe565c7592353df8e11d2f3","c12b5f9bf412c891cad443ef00a378ad2d3f1301f140943414308665a7d90af8","cf1b65c20036885ed99ce1c18aa0a0ed66f42acd6d415e99b48a8fa4105c23ed","173aec8be1be982c8244df6f94880d77a9b766c8c1ec3eb0af662c8dc6da7f2e","08188020373062e07955835a996fda1aff97a89e57d469edc6b9210bd9c8926f","cad5c2c0085a3e3b74f58aa199944b25ed8d24f93f51c99ebe2463e4f1694785","3e2d93a797c41ab081fbcd80e959b7c30d5d1c358f091c22a6ebe416ef7c5e19","c440df5735a3305e7db118bf821efb597c8318910861f735372846db9f7b506b","d6d8de719a75e5d2ed9dd9d6a99296d1337259e1c96166579db50797edd72ede","32b4c732e183bf5d123f88d526ac21b71a681089c18d2d761be342df31179d94","212d16020e7dce1b5509f3b9813de73612de57c6a3d74536714eb88787b96dc3","1a63d5212341783aa49cf78d667bf2a6cd03208ea09620b2fc3e647ae07f4e0d","84ea58841272970e6e3247cba4dbb326cf22764c2f4bbcb03f1c634315bbbcb5","86f9fbecdd848d02c90f861cc9839d8f3449c518a77e77ea65362f6a4126c63b","ecdaf317a4a1e7e3540e2f1b6aae38acd78dd99d564b52f98eea7358ac74416d","c30430960f1a0552b3cdaf1ef8164fdd4f289c782a8912df5180d57bc9ddfc03","a348081c01502c9f87d39d9e4b5dd58e1111b34c62686d6e569c595a0417bb35","eff69aee13c76502a16b756cde9c451fb4b5c4234052f3b3bee9dbfe92e1b1d5","9943f44400939f4ff008a882ff71162f70ba0c2f735c9743fd4645ef5c925fc4","b7836eba6c5173a1683aee8aa1771ff339e795cb9c21411590edb910274febe4","6fe447aa7e6fabc4f6c536f2997e3b1116b7f73dbe5bf3fc8d958bad434e4a84","15d3908d453d14be4dae760122ed5d74ad789a19f1fec2edd4034e57217436e9","ef00bc701f382da70870ab7721ed8f6552a38e332e60370b93cf340b6470845c","18891a02fa046e57b43a543dddc7212086fcb04ae6c8e8f28f8605dd3ccf57ed",{"version":"5980a888624dce1b0937a0d21c623f97056501bb61a8da29cbe07f1a0be2c9a8","affectsGlobalScope":true},"590a41ccab332c66a6aa62746612b03ceb2e92cc1da58c140e90fb7ff6e8c851","dc1d2996f23fe7f0da0b2c843e05c0ac170a36b51da11e58de089d344de93c3b","78ff01b50e7e9761f239527ec70b96171bccc28a08d909243e193db03b6f6983","ed18472ee2247563a26d754dd4c8bd66383013df13ce7c2927b03cab1a27b7e8","28ac9ac1fa163e5f2321fafa49b9931908c0076216ed3c82646d79abdf79775e","07dd4bed8ddab685f82a2125bf3aa41b42e36f28c16a5aec7357b727649076fb","fc15a2216f29b825747c0c3a54d6989518dd0f4aa0b580520e5526b4a47bec8f","c656d5baf3d4a8f358fc083db04b0fda8cb8503a613a9ba42327ecbd7909773c","397c2c81eaeae1388f7459699d7606feecfc304b212eb9113407c1315746a578","c2d923e9adc26a3efe5186f3a4a72413d24c80f03b306c68c30fa146690fb101","d34782833b7d5f72486a5fb926d3d96198706ed76aeaf1d435c748ebcf9169fc","b093e56054755189dd891ea832dec40d729d110a0a3f432fff5ea5ab1078cdde","98affe620e6230a3888b445c32376e4edbf6b1b376a71f2bf9c07bee11fcdd65","1e05491bef32ff48393d605d557152735899da3d9b111ba3588a1800f2927f4a","1ff7813974b1b9a0524c1e5a99aa52a05e79fc7df7749ada75ded8c53fe1b7e0","cd8c517f54d4ff3475755b290d741c8799df3265ce73d454d8fafe423f8ff749","bf431147b104ae92d61de6b43e9f25d27e8d3eaeaffd612e0c0d3bb8e2423926","f0f21604ae8f880c0ab529f00303806fdeadc943e32a25ca063fc8fea0fa063c","8dc4f45212fba9381e1674e8bd934a588730efbb8a6681b661cad8cd09b081c5",{"version":"52bf774bd30177ebb3e450c808d8d46f67896848a942e6203ae78b65b33d0106","signature":"688c437017a53e69ff66aac2036a0d7f6263082f676a408c9998cbd87ea2ec73"},{"version":"8b6ee36fd764378c62dca37041c5a12fd5a77b9e853c78908b7ed1c90dc149e4","signature":"03846acca031c757d910dbc017d846c87574faf90bde82316fb9b8537896d5ee"},{"version":"0d089d33f31b56697d142aa7395738c0323cf761b4c79fd6bf65a54ab1ddf02f","signature":"027c87e1cb049497d4f185bc9b922ce91cad59832da8faf3411e6b298b9deb78"},{"version":"ec0982b9e7d6c1b6c80e2829c5909eefb9ecee687e60621e0bb937e8ad5d1d43","signature":"8478b617a5be940f1b4b4d19d2fc6149c21ac69c4a7e00c8a7db2c2c21aa2274"},{"version":"84c5fc9d0d22f4566791b88d5fc2c24f56508b50c9ce894ac549ebaa158b1fca","signature":"677ea66c6fa02f1cebf82df19f416a8302c7a7d10e2de265b162760fcd865eef"},{"version":"8455135ea42310a73404fa2513e212d170af1191584061f583ec1e0f6b75dd91","signature":"83e4298f0b6834e955ee6a76569d3e5b3192065d47f1daf4535bb9edb16e88cb"},{"version":"73529962207605bdc5285d5e745919b8d57b776daa0f22a14b75cd8a92d63af9","signature":"422fcd2a7fd87f05efdfaa6eab382ca607d5d54e1f175ba2efccd4aacd5433ef"},{"version":"ebe927d8a9739c9d32ef4df28c1c36cf82daa9abba7cdf3f79e320c5e99e99d8","signature":"2421f9c6b1ecedd50818719090a77e9d2748c2339c33f3d4817beebf7a39d211"},{"version":"165c56632fea46c85e2a62f1b4eae600b846ea0deacd3c137fde9bacb845c30e","signature":"79bf9e3846b43e706d181c00f3c1c50ae8fc60e587c97a16e521adc150317624"},{"version":"866e1d2cf16a41851b056a2cc0cdc5f0f00df0435376cc2c723a8c609f61fbd0","signature":"5f5bbca60f0bfed6ff714163c4e962a5e260e59db754c89ee2063403accd03e3"},{"version":"ecfa1b63e3829b310ac968b2cc1cc7016ba76ffb8532439aebecbcbc57173b99","signature":"2f1dda63ade2bd085704674523b56ede942bc8c2c37fe8ed9b9b0fdfd69b1262"},{"version":"51d2f746d7e599a5549f5a946565934b4556bb9155be1eed2c474e25f1474872","signature":"c15585fe8935ed5cfedec39b7d41ec49990973f40faaba4b3e14278861643d79"},{"version":"b1d1378906c54a2f4d230ad69d212beedd2552afe3f7ad171b7eacb4cecc26d7","signature":"f9e60e8f79a7f606f19e02e2d39a24995719767dbe587f564f970bb24e3ca29d"},{"version":"f5a156e5b3783ea0399ac0326b7ab31a00e8874c5fa9b5e26fac217da8b5adfd","signature":"cfa7179e0306fc04d93f062c96e7ae8bad58d0cc4a7aa0dd4494ff9d262b101c"},{"version":"3c9fefca9303bcfd5712de11a3cbda20b3d6e85f29019bc75cab24690fb0f90d","signature":"306683152ff5a6038cf05b03ddff85a15b1bc8e18ef268aad26b02fd8e0e8b9d"},"a11c3e55d22d6379fe0949793e2638a6b43aa6e9def4f7452c3e352a296ef8da",{"version":"2770956c9437d7d66650084891c559ff6bb94200b7e2820940fd5d5dd0efa489","signature":"2faaf4f254008bf5be0e145be10dba35dccfac7116e9083f9d697a476a8e7076"},{"version":"ceee917fd557b841b93f7e13103dfdad79d38fe9962408f538f27db03dc9368d","signature":"15003ff6ed10d259dca775c7e5f7a64b272a9c370b6085db2d42a2d4a1d81579"},{"version":"a1691ae6d70af82f3e26d9e2e021dc5063021bd9c335bfdb40dc97d3574d1b3f","signature":"cd1c566b611a70ff987a79d0465da67649a8ed7e7668feddfcdf6dceb01c09a8"},{"version":"a105417dd540f1a400f0665c877e5d7e48e2efe08f01c2e5c7272256e644faa5","signature":"b3a6ee392811d6cddb38378ebaa373d4a39aa7dc4ecac73497c6b33627e6430b"},{"version":"581b44cf6122e3ad267d6bda2428c214fef3d38b6d7249df9fa6bc240a880a78","signature":"0ca09d92d6469d906a3d1c7192a6294c7f65b75f4f7eb8072bbd1b68c7f021e1"},{"version":"2e6426c1a1ff8561aa5f01d9398426bf06e55307f688464939de3196f0d4c143","signature":"5357bd09c9816a9765e617f86a9b49f85133d0bc0f9c5e29e834f2f8e6d52acb"},{"version":"508279c48de5627ae6c30a0aee01f4391bf32450335d7f09d5dd82acbc4d13c5","signature":"11d546a505f70f9c5f8092916027d8045c280a817b709fcaf2c4e63fa026c89c"},{"version":"557f2e0a4e5ac8a59b7c3068b2b30162fb963d72d50152482ab8c414e81caf37","signature":"008eaae28119118f1c589a1e29ea7fd17277f2280d2d3bfddeacd71fd1671bb5"},{"version":"f45c172ca54fb28d64b2dd04e495f17038034e20f37bd286a9f3eeb286cf1388","signature":"75a8761564c8fc5581b062dd339ea698921baf60e52eae055c8177dfa89eba90"},{"version":"ea696a0517ad69afea472e47eb1f904aba1667f54d4557eb98b8c766469d56a2","signature":"7e125d9abc19f62d1480f6c04a45d7bb2c89153316245ae8b8e5a0234b078c4e"},{"version":"902937c505f88d8b5b32829b4c14243eb740013fd0e2f58e6485324bbfe197a6","signature":"dc7de7650e5a64fc010387db18e84d48fe8f562dbd9caac01e54f83681ac976b"},{"version":"842accda78bb1b6f494f264aae307b84d933486d607e91f6e1d3a4d2e4851783","signature":"430d9683c8e5aaab71f0e3b271c4240cd5120a91191f953722985499af51d7e6"},{"version":"45b1a895868587c78a2ddff937967669b4e1968ea72c01e1c2b6dd5993f53b36","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"99cab9373415bac71e9d2c84279782c0a361b59551d0ca8dfaee8d4c08ed3247","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"ba1fed463e8a21ffddb67a53df3f0d818b351991723736e167d065e2de1c7183",{"version":"22e311fec88bcc49b2b1fb3c9a7c082cd84b3388c9bcc7b9ef08253f6fa74e26","affectsGlobalScope":true},"c186097fd9b86681981cdeba08c0b6bbfcd8b562ab490c25656d85fef8f10c79","0b0c483e991e81c3f26e5f2da53ff26a15994c98c8b89cda1e4156dfc2428111","3340eb7b30bdee5f0349107d4068fd6f2f4712e11a2ba68e203b2f2489350317",{"version":"2000d60bd5195730ffff0d4ce9389003917928502c455ed2a9e296d3bf1a4b42","signature":"56335d3c9b867cc8654c05e633c508dd8de0038157f9958eb8794b7c123bb90e"},{"version":"dfceb5b9355a4a9002a7c291b1c3315511977c73cb23d9c123a72567783a18c0","signature":"b1802850887a3ea11a06df1fc1c65c6579332eefba1e63b3967a73dc937a2574"},{"version":"384fc0e3fa5966f524c96f1782b9d7a005346ba1621c43d0d1d819bf39077fbc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7fde517b3f03bb21ec3a46ba5f85c6797f8abf27deacb862183126e2f072788e","signature":"8b310edcfec83da25bc4f3adb20a7583bc5dae56d7d06c5b1431b76d390c1b72"},{"version":"894d93831d2afcd26f7362347e4960dd6d53f4153dad08813f3670e1327e387c","signature":"b1802850887a3ea11a06df1fc1c65c6579332eefba1e63b3967a73dc937a2574"},{"version":"8f9eac2c3ae305c25d4ffeff800b9811c8d3ec6a11b142fe96d08a2bc40f6440","signature":"08d6a2d1b004bbcac4249cd5baf6e9c662adc6139939c266b42e0422ef0c68b3"},{"version":"ac8980bdd810c30c444b59cca584c9b61d5ab274fa9474d778970537f3090240","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1c024431c672cf9c6dcdb4d30c5b625435d81a5423b9d45e8de0082e969af8a8","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"eee1b57475023853cd09dd79b8d0d6639b6b82c3baee5863c2f2022b710f4102","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"377ba49d29102653a4b0c72b3870f9c599575df7db3a3fae7a21be5327ff84e2","signature":"c47f5db4df0a5031ed84bc6ee192c412b9e2d4d5e94681af77ccdcc25c851839"},{"version":"377ba49d29102653a4b0c72b3870f9c599575df7db3a3fae7a21be5327ff84e2","signature":"c47f5db4df0a5031ed84bc6ee192c412b9e2d4d5e94681af77ccdcc25c851839"},{"version":"39833acf7547216b2f31b2279dcfec3ed1359dec8adc9d1cb87c695ebf9bff94","signature":"7292d4dc9dac6d815dc30245a4a4a4959845d3a2b84ba0166857e4b23f2d033f"},{"version":"39833acf7547216b2f31b2279dcfec3ed1359dec8adc9d1cb87c695ebf9bff94","signature":"7292d4dc9dac6d815dc30245a4a4a4959845d3a2b84ba0166857e4b23f2d033f"},{"version":"529dd364d169ab3dbbb177ccdc4987c4a6f69187f553f3d36460ab65879ad998","signature":"3919e9d5911da2254732c31942e2cdc0057056ebfc2a16d34041c76a9b58d447"},{"version":"ebea587ca6477b9db29baf75d359924c55ab490fecdc38d7c0f16e589f0d27f9","signature":"0688c25f38e78e052338305d23046c7841074b3da5709a8f9e598ed705b9932b"},{"version":"de411013305dbe5c7a1ac13d2ea16dc36e52e6efd255b4e912fe53862058c649","signature":"2faaf4f254008bf5be0e145be10dba35dccfac7116e9083f9d697a476a8e7076"},"e432b56911b58550616fc4d54c1606f65fe98c74875b81d74601f5f965767c60","cc957354aa3c94c9961ebf46282cfde1e81d107fc5785a61f62c67f1dd3ac2eb","a46a2e69d12afe63876ec1e58d70e5dbee6d3e74132f4468f570c3d69f809f1c","93de1c6dab503f053efe8d304cb522bb3a89feab8c98f307a674a4fae04773e9","3b043cf9a81854a72963fdb57d1884fc4da1cf5be69b5e0a4c5b751e58cb6d88","dd5647a9ccccb2b074dca8a02b00948ac293091ebe73fdf2e6e98f718819f669","0cba3a5d7b81356222594442753cf90dd2892e5ccfe1d262aaca6896ba6c1380","a69c09dbea52352f479d3e7ac949fde3d17b195abe90b045d619f747b38d6d1a",{"version":"c2ab70bbc7a24c42a790890739dd8a0ba9d2e15038b40dff8163a97a5d148c00","affectsGlobalScope":true},"422dbb183fdced59425ca072c8bd09efaa77ce4e2ab928ec0d8a1ce062d2a45a",{"version":"712ba0d43b44d144dfd01593f61af6e2e21cfae83e834d297643e7973e55ed61","affectsGlobalScope":true},"1dab5ab6bcf11de47ab9db295df8c4f1d92ffa750e8f095e88c71ce4c3299628","f71f46ccd5a90566f0a37b25b23bc4684381ab2180bdf6733f4e6624474e1894",{"version":"54e65985a3ee3cec182e6a555e20974ea936fc8b8d1738c14e8ed8a42bd921d4","affectsGlobalScope":true},"82408ed3e959ddc60d3e9904481b5a8dc16469928257af22a3f7d1a3bc7fd8c4","98a3ebfa494b46265634a73459050befba5da8fdc6ca0ef9b7269421780f4ff3","34e5de87d983bc6aefef8b17658556e3157003e8d9555d3cb098c6bef0b5fbc8","cc0b61316c4f37393f1f9595e93b673f4184e9d07f4c127165a490ec4a928668","f27371653aded82b2b160f7a7033fb4a5b1534b6f6081ef7be1468f0f15327d3","c762cd6754b13a461c54b59d0ae0ab7aeef3c292c6cf889873f786ee4d8e75c9","f4ea7d5df644785bd9fbf419930cbaec118f0d8b4160037d2339b8e23c059e79",{"version":"bfea28e6162ed21a0aeed181b623dcf250aa79abf49e24a6b7e012655af36d81","affectsGlobalScope":true},"7a5459efa09ea82088234e6533a203d528c594b01787fb90fba148885a36e8b6","ae97e20f2e10dbeec193d6a2f9cd9a367a1e293e7d6b33b68bacea166afd7792","10d4796a130577d57003a77b95d8723530bbec84718e364aa2129fa8ffba0378","ad41bb744149e92adb06eb953da195115620a3f2ad48e7d3ae04d10762dae197","bf73c576885408d4a176f44a9035d798827cc5020d58284cb18d7573430d9022","7ae078ca42a670445ae0c6a97c029cb83d143d62abd1730efb33f68f0b2c0e82",{"version":"e8b18c6385ff784228a6f369694fcf1a6b475355ba89090a88de13587a9391d5","affectsGlobalScope":true},"5d0a9ea09d990b5788f867f1c79d4878f86f7384cb7dab38eecbf22f9efd063d","12eea70b5e11e924bb0543aea5eadc16ced318aa26001b453b0d561c2fd0bd1e","08777cd9318d294646b121838574e1dd7acbb22c21a03df84e1f2c87b1ad47f2","08a90bcdc717df3d50a2ce178d966a8c353fd23e5c392fd3594a6e39d9bb6304",{"version":"4cd4cff679c9b3d9239fd7bf70293ca4594583767526916af8e5d5a47d0219c7","affectsGlobalScope":true},"2a12d2da5ac4c4979401a3f6eaafa874747a37c365e4bc18aa2b171ae134d21b","002b837927b53f3714308ecd96f72ee8a053b8aeb28213d8ec6de23ed1608b66","1dc9c847473bb47279e398b22c740c83ea37a5c88bf66629666e3cf4c5b9f99c","a9e4a5a24bf2c44de4c98274975a1a705a0abbaad04df3557c2d3cd8b1727949","00fa7ce8bc8acc560dc341bbfdf37840a8c59e6a67c9bfa3fa5f36254df35db2","1b952304137851e45bc009785de89ada562d9376177c97e37702e39e60c2f1ff",{"version":"806ef4cac3b3d9fa4a48d849c8e084d7c72fcd7b16d76e06049a9ed742ff79c0","affectsGlobalScope":true},"44b8b584a338b190a59f4f6929d072431950c7bd92ec2694821c11bce180c8a5","5f0ed51db151c2cdc4fa3bb0f44ce6066912ad001b607a34e65a96c52eb76248",{"version":"3345c276cab0e76dda86c0fb79104ff915a4580ba0f3e440870e183b1baec476","affectsGlobalScope":true},"664d8f2d59164f2e08c543981453893bc7e003e4dfd29651ce09db13e9457980","e383ff72aabf294913f8c346f5da1445ae6ad525836d28efd52cbadc01a361a6","f52fbf64c7e480271a9096763c4882d356b05cab05bf56a64e68a95313cd2ce2","59bdb65f28d7ce52ccfc906e9aaf422f8b8534b2d21c32a27d7819be5ad81df7",{"version":"3a2da34079a2567161c1359316a32e712404b56566c45332ac9dcee015ecce9f","affectsGlobalScope":true},"28a2e7383fd898c386ffdcacedf0ec0845e5d1a86b5a43f25b86bc315f556b79","3aff9c8c36192e46a84afe7b926136d520487155154ab9ba982a8b544ea8fc95","a880cf8d85af2e4189c709b0fea613741649c0e40fffb4360ec70762563d5de0","85bbf436a15bbeda4db888be3062d47f99c66fd05d7c50f0f6473a9151b6a070","9f9c49c95ecd25e0cb2587751925976cf64fd184714cb11e213749c80cf0f927","f0c75c08a71f9212c93a719a25fb0320d53f2e50ca89a812640e08f8ad8c408c",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"9cafe917bf667f1027b2bb62e2de454ecd2119c80873ad76fc41d941089753b8","3ebae8c00411116a66fca65b08228ea0cf0b72724701f9b854442100aab55aba","8b06ac3faeacb8484d84ddb44571d8f410697f98d7bfa86c0fda60373a9f5215","7eb06594824ada538b1d8b48c3925a83e7db792f47a081a62cf3e5c4e23cf0ee","f5638f7c2f12a9a1a57b5c41b3c1ea7db3876c003bab68e6a57afd6bcc169af0","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","7980bf9d2972585cdf76b5a72105f7817be0723ccb2256090f6335f45b462abe","301d7466eb591139c7d456958f732153b3400f3243f68d3321956b43a64769e9","22f13de9e2fe5f0f4724797abd3d34a1cdd6e47ef81fc4933fea3b8bf4ad524b","e3ba509d3dce019b3190ceb2f3fc88e2610ab717122dabd91a9efaa37804040d","cda0cb09b995489b7f4c57f168cd31b83dcbaa7aad49612734fb3c9c73f6e4f2",{"version":"2abad7477cf6761b55c18bea4c21b5a5dcf319748c13696df3736b35f8ac149e","affectsGlobalScope":true},"d38e588a10943bbab1d4ce03d94759bf065ff802a9a72fc57aa75a72f1725b71","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","2b8264b2fefd7367e0f20e2c04eed5d3038831fe00f5efbc110ff0131aab899b","6209c901f30cc321f4b86800d11fad3d67e73a3308f19946b1bc642af0280298","60aaac5fb1858fbd4c4eb40e01706eb227eed9eca5c665564bd146971280dbd3","74b0245c42990ed8a849df955db3f4362c81b13f799ebc981b7bec2d5b414a57","b0d10e46cfe3f6c476b69af02eaa38e4ccc7430221ce3109ae84bb9fb8282298","4266ccd2cf1d6a281efd9c7ddf9efd7daecf76575364148bd233e18919cac3ed","70e9a18da08294f75bf23e46c7d69e67634c0765d355887b9b41f0d959e1426e","105b9a2234dcb06ae922f2cd8297201136d416503ff7d16c72bfc8791e9895c1"],"options":{"composite":true,"declaration":true,"declarationMap":true,"downlevelIteration":true,"esModuleInterop":true,"experimentalDecorators":true,"importHelpers":true,"jsx":2,"noEmitOnError":false,"noImplicitAny":true,"noUnusedLocals":true,"outDir":"./","rootDir":"../src","skipLibCheck":true,"sourceMap":true,"strictNullChecks":true,"target":1,"tsBuildInfoFile":"./tsconfig.tsbuildinfo"},"fileIdsList":[[211,260],[260],[260,273],[53,190,260],[192,194,260],[190,192,193,260],[53,260],[53,140,260],[69,260],[211,212,213,214,215,260],[211,213,260],[233,260,267],[260,269],[260,270],[260,275,277],[217,260],[220,260],[221,226,260],[222,232,233,240,249,259,260],[222,223,232,240,260],[224,260],[225,226,233,241,260],[226,249,256,260],[227,229,232,240,260],[228,260],[229,230,260],[231,232,260],[232,260],[232,233,234,249,259,260],[232,233,234,249,260],[235,240,249,259,260],[232,233,235,236,240,249,256,259,260],[235,237,249,256,259,260],[217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266],[232,238,260],[239,259,260],[229,232,240,249,260],[241,260],[242,260],[220,243,260],[244,258,260,264],[245,260],[246,260],[232,247,260],[247,248,260,262],[232,249,250,251,260],[249,251,260],[249,250,260],[252,260],[253,260],[232,254,255,260],[254,255,260],[226,240,249,256,260],[257,260],[240,258,260],[221,235,246,259,260],[226,260],[249,260,261],[260,262],[260,263],[221,226,232,234,243,249,259,260,262,264],[249,260,265],[76,77,260],[53,58,64,65,68,71,72,73,76,260],[74,260],[84,260],[53,57,82,260],[53,54,57,58,62,75,76,260],[53,76,105,106,260],[53,54,57,58,62,76,260],[82,91,260],[53,54,62,75,76,93,260],[53,55,58,61,62,65,75,76,260],[53,54,57,62,76,260],[53,54,57,62,260],[53,54,55,58,60,62,63,75,76,260],[53,76,260],[53,75,76,260],[53,54,57,58,61,62,75,76,82,93,260],[53,55,58,260],[53,54,57,60,75,76,93,103,260],[53,54,60,76,103,105,260],[53,54,57,60,62,93,103,260],[53,54,55,58,60,61,75,76,93,260],[58,260],[53,55,58,59,60,61,75,76,260],[82,260],[83,260],[53,54,55,57,58,61,66,67,75,76,260],[58,59,260],[53,64,65,70,75,76,260],[53,56,64,70,75,76,260],[53,58,62,260],[53,118,260],[53,57,260],[57,260],[76,260],[75,260],[66,74,76,260],[53,54,57,58,61,75,76,260],[128,260],[53,56,57,260],[91,260],[44,45,46,47,48,55,56,57,58,59,60,61,62,63,64,65,66,67,68,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,260],[140,260],[46,260],[49,50,51,52,260],[232,235,237,240,259,260,267],[260,287],[260,275],[260,272,276],[260,274],[151,260],[53,141,142,143,144,146,147,148,149,150,157,260],[53,140,147,150,151,260],[143,144,260],[142,143,144,147,260],[143,260],[155,260],[157,260],[144,260],[53,144,260],[141,142,143,144,145,146,147,149,150,151,152,153,154,156,260],[149,260],[158,260],[43,140,166,177,260],[43,53,140,157,159,162,163,164,166,168,169,170,171,172,174,176,260],[43,53,140,162,165,260],[43,157,166,167,168,260],[43,53,140,165,166,168,170,171,177,260],[43,53,260],[43,167,260],[43,168,260],[43,53,140,157,162,163,166,172,191,195,260],[43,140,157,177,195,260],[43,53,140,157,177,179,191,198,260],[43,200,260],[43,157,170,171,173,260],[43,53,140,166,177,191,194,260],[43,53,140,166,179,191,194,260],[43,53,177,183,194,195,260],[43,260],[43,181,260],[43,53,177,180,181,182,183,260],[43,53,157,162,177,260],[43,53,140,180,182,184,260],[43,162,163,165,166,177,178,179,180,182,183,184,185,186,260],[43,53,140,160,161,260],[43,140,160,260],[43,140,260],[43,53,140,260],[43,157,260],[43,157,175,260],[43,53,140,157,175,260],[43,140,157,166,260],[43,140,157,170,171,260],[43,140,165,173,177,260],[53,140,166],[53,157,166,169],[53,140,162,165],[157,166],[53,140,166,177],[53],[191],[53,166,177,191,194],[53,166,179,191,194],[53,177,182,183,187],[53,162,177],[140,184],[162,163,165,166,177,178,179,180,182,183,184,185,186],[53,140],[140,160],[140],[157],[53,157],[140,157,166],[140,157],[177]],"referencedMap":[[213,1],[211,2],[274,3],[192,4],[193,5],[194,6],[191,4],[190,7],[69,8],[70,9],[273,2],[216,10],[212,1],[214,11],[215,1],[268,12],[269,2],[270,13],[271,14],[278,15],[279,2],[280,2],[217,16],[218,16],[220,17],[221,18],[222,19],[223,20],[224,21],[225,22],[226,23],[227,24],[228,25],[229,26],[230,26],[231,27],[232,28],[233,29],[234,30],[219,2],[266,2],[235,31],[236,32],[237,33],[267,34],[238,35],[239,36],[240,37],[241,38],[242,39],[243,40],[244,41],[245,42],[246,43],[247,44],[248,45],[249,46],[251,47],[250,48],[252,49],[253,50],[254,51],[255,52],[256,53],[257,54],[258,55],[259,56],[260,57],[261,58],[262,59],[263,60],[264,61],[265,62],[281,2],[282,2],[51,2],[78,63],[79,2],[74,64],[80,2],[81,65],[85,66],[86,2],[87,67],[88,68],[107,69],[89,2],[90,70],[92,71],[94,72],[95,73],[96,74],[63,74],[97,75],[64,76],[98,77],[99,68],[100,78],[101,79],[102,2],[60,80],[104,81],[106,82],[105,83],[103,84],[65,75],[61,85],[62,86],[108,2],[91,87],[83,87],[84,88],[68,89],[66,2],[67,2],[109,87],[110,90],[111,2],[112,71],[71,91],[72,92],[113,2],[114,93],[115,2],[116,2],[117,2],[119,94],[120,2],[56,7],[121,7],[122,95],[123,96],[124,2],[125,97],[127,97],[126,97],[76,98],[75,99],[77,97],[73,100],[128,2],[129,101],[58,102],[130,66],[131,66],[132,103],[133,87],[118,2],[134,2],[135,2],[136,2],[137,7],[138,2],[82,2],[140,104],[44,2],[45,105],[46,106],[48,2],[47,2],[93,2],[54,2],[139,105],[55,2],[59,85],[57,7],[283,7],[49,2],[53,107],[284,2],[52,2],[285,2],[286,108],[287,2],[288,109],[272,2],[50,2],[276,110],[277,111],[275,112],[149,2],[154,113],[151,114],[158,115],[147,116],[148,117],[141,2],[142,2],[145,116],[144,118],[156,119],[155,120],[153,116],[143,121],[146,122],[157,123],[175,124],[152,2],[150,7],[159,125],[43,2],[8,2],[9,2],[11,2],[10,2],[2,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[3,2],[4,2],[23,2],[20,2],[21,2],[22,2],[24,2],[25,2],[26,2],[5,2],[27,2],[28,2],[29,2],[30,2],[6,2],[31,2],[32,2],[33,2],[34,2],[7,2],[35,2],[40,2],[41,2],[36,2],[37,2],[38,2],[39,2],[1,2],[42,2],[178,126],[177,127],[166,128],[169,129],[179,130],[164,131],[188,132],[189,133],[196,134],[197,135],[199,136],[201,137],[202,138],[195,139],[198,140],[203,141],[180,142],[182,143],[181,142],[184,144],[183,145],[185,142],[186,146],[170,142],[171,142],[172,142],[187,147],[162,148],[204,149],[205,149],[161,149],[160,131],[206,150],[207,150],[163,151],[208,131],[209,152],[210,152],[176,153],[200,154],[167,142],[168,155],[165,142],[173,156],[174,157]],"exportedModulesMap":[[213,1],[211,2],[274,3],[192,4],[193,5],[194,6],[191,4],[190,7],[69,8],[70,9],[273,2],[216,10],[212,1],[214,11],[215,1],[268,12],[269,2],[270,13],[271,14],[278,15],[279,2],[280,2],[217,16],[218,16],[220,17],[221,18],[222,19],[223,20],[224,21],[225,22],[226,23],[227,24],[228,25],[229,26],[230,26],[231,27],[232,28],[233,29],[234,30],[219,2],[266,2],[235,31],[236,32],[237,33],[267,34],[238,35],[239,36],[240,37],[241,38],[242,39],[243,40],[244,41],[245,42],[246,43],[247,44],[248,45],[249,46],[251,47],[250,48],[252,49],[253,50],[254,51],[255,52],[256,53],[257,54],[258,55],[259,56],[260,57],[261,58],[262,59],[263,60],[264,61],[265,62],[281,2],[282,2],[51,2],[78,63],[79,2],[74,64],[80,2],[81,65],[85,66],[86,2],[87,67],[88,68],[107,69],[89,2],[90,70],[92,71],[94,72],[95,73],[96,74],[63,74],[97,75],[64,76],[98,77],[99,68],[100,78],[101,79],[102,2],[60,80],[104,81],[106,82],[105,83],[103,84],[65,75],[61,85],[62,86],[108,2],[91,87],[83,87],[84,88],[68,89],[66,2],[67,2],[109,87],[110,90],[111,2],[112,71],[71,91],[72,92],[113,2],[114,93],[115,2],[116,2],[117,2],[119,94],[120,2],[56,7],[121,7],[122,95],[123,96],[124,2],[125,97],[127,97],[126,97],[76,98],[75,99],[77,97],[73,100],[128,2],[129,101],[58,102],[130,66],[131,66],[132,103],[133,87],[118,2],[134,2],[135,2],[136,2],[137,7],[138,2],[82,2],[140,104],[44,2],[45,105],[46,106],[48,2],[47,2],[93,2],[54,2],[139,105],[55,2],[59,85],[57,7],[283,7],[49,2],[53,107],[284,2],[52,2],[285,2],[286,108],[287,2],[288,109],[272,2],[50,2],[276,110],[277,111],[275,112],[149,2],[154,113],[151,114],[158,115],[147,116],[148,117],[141,2],[142,2],[145,116],[144,118],[156,119],[155,120],[153,116],[143,121],[146,122],[157,123],[175,124],[152,2],[150,7],[159,125],[43,2],[8,2],[9,2],[11,2],[10,2],[2,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[3,2],[4,2],[23,2],[20,2],[21,2],[22,2],[24,2],[25,2],[26,2],[5,2],[27,2],[28,2],[29,2],[30,2],[6,2],[31,2],[32,2],[33,2],[34,2],[7,2],[35,2],[40,2],[41,2],[36,2],[37,2],[38,2],[39,2],[1,2],[42,2],[178,158],[177,159],[166,160],[169,161],[179,162],[164,163],[196,164],[199,164],[195,165],[198,166],[184,167],[183,168],[186,169],[187,170],[162,171],[204,172],[205,172],[161,172],[160,163],[206,173],[207,173],[163,171],[208,163],[209,174],[210,174],[176,174],[200,175],[168,176],[173,177],[174,178]],"semanticDiagnosticsPerFile":[213,211,274,192,193,194,191,190,69,70,273,216,212,214,215,268,269,270,271,278,279,280,217,218,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,219,266,235,236,237,267,238,239,240,241,242,243,244,245,246,247,248,249,251,250,252,253,254,255,256,257,258,259,260,261,262,263,264,265,281,282,51,78,79,74,80,81,85,86,87,88,107,89,90,92,94,95,96,63,97,64,98,99,100,101,102,60,104,106,105,103,65,61,62,108,91,83,84,68,66,67,109,110,111,112,71,72,113,114,115,116,117,119,120,56,121,122,123,124,125,127,126,76,75,77,73,128,129,58,130,131,132,133,118,134,135,136,137,138,82,140,44,45,46,48,47,93,54,139,55,59,57,283,49,53,284,52,285,286,287,288,272,50,276,277,275,149,154,151,158,147,148,141,142,145,144,156,155,153,143,146,157,175,152,150,159,43,8,9,11,10,2,12,13,14,15,16,17,18,19,3,4,23,20,21,22,24,25,26,5,27,28,29,30,6,31,32,33,34,7,35,40,41,36,37,38,39,1,42,178,177,166,169,179,164,188,189,196,197,199,201,202,195,198,203,180,182,181,184,183,185,186,170,171,172,187,162,204,205,161,160,206,207,163,208,209,210,176,200,167,168,165,173,174]},"version":"4.7.4"} \ No newline at end of file diff --git a/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutView.swift b/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutView.swift -index f18e92c..71b63dc 100644 +index f18e92c..f166553 100644 --- a/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutView.swift +++ b/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutView.swift @@ -4,31 +4,35 @@ import UIKit @@ -973,7 +973,7 @@ index f18e92c..71b63dc 100644 } else { assertionFailure("CellRendererComponent outer view should always be CellContainer. Learn more here: https://shopify.github.io/flash-list/docs/usage#cellrenderercomponent.") return nil -@@ -106,7 +128,7 @@ import UIKit +@@ -106,12 +128,16 @@ import UIKit /// Checks for overlaps or gaps between adjacent items and then applies a correction. /// Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on the iOS side. @@ -982,7 +982,16 @@ index f18e92c..71b63dc 100644 var maxBound: CGFloat = 0 var minBound: CGFloat = CGFloat(Int.max) var maxBoundNextCell: CGFloat = 0 -@@ -192,7 +214,7 @@ import UIKit + let correctedScrollOffset = scrollOffset - (horizontal ? frame.minX : frame.minY) + lastMaxBoundOverall = 0 ++ if cellContainers.count == 1 { ++ let firstCellContainer = cellContainers[0] ++ lastMaxBoundOverall = horizontal ? firstCellContainer.frame.maxX : firstCellContainer.frame.maxY ++ } + cellContainers.indices.dropLast().forEach { index in + let cellContainer = cellContainers[index] + let cellTop = cellContainer.frame.minY +@@ -192,7 +218,7 @@ import UIKit lastMinBound = minBound } @@ -991,7 +1000,7 @@ index f18e92c..71b63dc 100644 lastMaxBoundOverall = max(lastMaxBoundOverall, horizontal ? currentCell.frame.maxX : currentCell.frame.maxY, horizontal ? nextCell.frame.maxX : nextCell.frame.maxY) } -@@ -217,7 +239,7 @@ import UIKit +@@ -217,7 +243,7 @@ import UIKit /// It's important to avoid correcting views outside the render window. An item that isn't being recycled might still remain in the view tree. If views outside get considered then gaps between unused items will cause algorithm to fail. func isWithinBounds( @@ -1000,21 +1009,18 @@ index f18e92c..71b63dc 100644 scrollOffset: CGFloat, renderAheadOffset: CGFloat, windowSize: CGFloat, -@@ -260,10 +282,10 @@ import UIKit +@@ -260,17 +286,18 @@ import UIKit } private func footerDiff() -> CGFloat { - if subviews.count == 0 { -+ if viewsToLayout.count == 0 { - lastMaxBoundOverall = 0 +- lastMaxBoundOverall = 0 - } else if subviews.count == 1 { - let firstChild = subviews[0] -+ } else if viewsToLayout.count == 1 { -+ let firstChild = viewsToLayout[0] - lastMaxBoundOverall = horizontal ? firstChild.frame.maxX : firstChild.frame.maxY - } +- lastMaxBoundOverall = horizontal ? firstChild.frame.maxX : firstChild.frame.maxY +- } let autoLayoutEnd = horizontal ? frame.width : frame.height -@@ -271,6 +293,13 @@ import UIKit + return lastMaxBoundOverall - autoLayoutEnd } private func footer() -> UIView? { diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch index 5bfb2cc5f0b0..cc9c8531e3a3 100644 --- a/patches/react-native-modal+13.0.1.patch +++ b/patches/react-native-modal+13.0.1.patch @@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644 buildPanResponder: () => void; getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number; diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js -index 80f4e75..3ba8b8c 100644 +index 80f4e75..5a58eae 100644 --- a/node_modules/react-native-modal/dist/modal.js +++ b/node_modules/react-native-modal/dist/modal.js @@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component { @@ -48,7 +48,17 @@ index 80f4e75..3ba8b8c 100644 if (this.didUpdateDimensionsEmitter) { this.didUpdateDimensionsEmitter.remove(); } -@@ -525,7 +540,7 @@ export class ReactNativeModal extends React.Component { +@@ -464,6 +479,9 @@ export class ReactNativeModal extends React.Component { + InteractionManager.clearInteractionHandle(this.interactionHandle); + this.interactionHandle = null; + } ++ if (this.state.isVisible) { ++ this.props.onModalHide(); ++ } + } + componentDidUpdate(prevProps) { + // If the animations have been changed then rebuild them to make sure we're +@@ -525,7 +543,7 @@ export class ReactNativeModal extends React.Component { } return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), this.makeBackdrop(), diff --git a/src/CONST.ts b/src/CONST.ts index 6517ece4276d..601258890e33 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,14 +73,19 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { - MERGED_ACCOUNT_PREFIX: 'MERGED_', + RECENT_WAYPOINTS_NUMBER: 20, DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, - ANIMATED_HIGHLIGHT_DELAY: 500, - ANIMATED_HIGHLIGHT_DURATION: 500, + WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, + ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, + ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, + ANIMATED_HIGHLIGHT_START_DELAY: 10, + ANIMATED_HIGHLIGHT_START_DURATION: 300, + ANIMATED_HIGHLIGHT_END_DELAY: 800, + ANIMATED_HIGHLIGHT_END_DURATION: 2000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, @@ -91,6 +96,7 @@ const CONST = { // Multiplier for gyroscope animation in order to make it a bit more subtle ANIMATION_GYROSCOPE_VALUE: 0.4, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, + SCREEN_TRANSITION_END_TIMEOUT: 1000, ARROW_HIDE_DELAY: 3000, API_ATTACHMENT_VALIDATIONS: { @@ -166,6 +172,9 @@ const CONST = { PULL_REQUEST_NUMBER, + // Regex to get link in href prop inside of component + REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi, + MERCHANT_NAME_MAX_LENGTH: 255, REQUEST_PREVIEW: { @@ -200,27 +209,12 @@ const CONST = { // Sizes needed for report empty state background image handling EMPTY_STATE_BACKGROUND: { ASPECT_RATIO: 3.72, + OVERLAP: 60, SMALL_SCREEN: { IMAGE_HEIGHT: 300, - CONTAINER_MINHEIGHT: 200, - VIEW_HEIGHT: 240, }, WIDE_SCREEN: { IMAGE_HEIGHT: 450, - CONTAINER_MINHEIGHT: 500, - VIEW_HEIGHT: 390, - }, - MONEY_OR_TASK_REPORT: { - SMALL_SCREEN: { - IMAGE_HEIGHT: 300, - CONTAINER_MINHEIGHT: 280, - VIEW_HEIGHT: 240, - }, - WIDE_SCREEN: { - IMAGE_HEIGHT: 450, - CONTAINER_MINHEIGHT: 280, - VIEW_HEIGHT: 390, - }, }, }, @@ -934,6 +928,7 @@ const CONST = { RESIZE_DEBOUNCE_TIME: 100, }, SEARCH_TABLE_COLUMNS: { + RECEIPT: 'receipt', DATE: 'date', MERCHANT: 'merchant', FROM: 'from', @@ -965,7 +960,7 @@ const CONST = { DARK_CONTENT: 'dark-content', }, TRANSACTION: { - DEFAULT_MERCHANT: 'Request', + DEFAULT_MERCHANT: 'Expense', UNKNOWN_MERCHANT: 'Unknown Merchant', PARTIAL_TRANSACTION_MERCHANT: '(none)', TYPE: { @@ -1270,6 +1265,8 @@ const CONST = { EXPENSIFY_EMAIL_DOMAIN: '@expensify.com', }, + CONCIERGE_DISPLAY_NAME: 'Concierge', + INTEGRATION_ENTITY_MAP_TYPES: { DEFAULT: 'DEFAULT', NONE: 'NONE', @@ -1925,7 +1922,7 @@ const CONST = { EMOJI_NAME: /:[\w+-]+:/g, EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, AFTER_FIRST_LINE_BREAK: /\n.*/g, - LINE_BREAK: /\r|\n/g, + LINE_BREAK: /\r\n|\r|\n/g, CODE_2FA: /^\d{6}$/, ATTACHMENT_ID: /chat-attachments\/(\d+)/, HAS_COLON_ONLY_AT_THE_BEGINNING: /^:[^:]+$/, @@ -2059,7 +2056,7 @@ const CONST = { LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, TAG_NAME_LIMIT: 256, - REPORT_NAME_LIMIT: 256, + REPORT_NAME_LIMIT: 100, TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, WORKSPACE_NAME_CHARACTER_LIMIT: 80, @@ -3491,7 +3488,6 @@ const CONST = { NAVIGATION: { TYPE: { - FORCED_UP: 'FORCED_UP', UP: 'UP', }, ACTION_TYPE: { @@ -3673,6 +3669,7 @@ const CONST = { TAX_OUT_OF_POLICY: 'taxOutOfPolicy', TAX_RATE_CHANGED: 'taxRateChanged', TAX_REQUIRED: 'taxRequired', + HOLD: 'hold', }, /** Context menu types */ @@ -3735,6 +3732,7 @@ const CONST = { WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, ONBOARDING_CONCIERGE: { @@ -4754,6 +4752,7 @@ const CONST = { MAX_TAX_RATE_DECIMAL_PLACES: 4, DOWNLOADS_PATH: '/Downloads', + DOWNLOADS_TIMEOUT: 5000, NEW_EXPENSIFY_PATH: '/New Expensify', ENVIRONMENT_SUFFIX: { @@ -4768,10 +4767,10 @@ const CONST = { }, SEARCH_RESULTS_PAGE_SIZE: 50, - SEARCH_BOTTOM_TAB_URL: '/Search_Bottom_Tab', SEARCH_DATA_TYPES: { TRANSACTION: 'transaction', + REPORT: 'report', }, REFERRER: { diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 6205afb9c03c..7a6203a44068 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -1,3 +1,4 @@ +import {Audio} from 'expo-av'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {NativeEventSubscription} from 'react-native'; import {AppState, Linking} from 'react-native'; @@ -108,6 +109,9 @@ function Expensify({ const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]); const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); + const isAuthenticatedRef = useRef(false); + isAuthenticatedRef.current = isAuthenticated; + const contextValue = useMemo( () => ({ isSplashHidden, @@ -194,7 +198,8 @@ function Expensify({ // Open chat report from a deep link (only mobile native) Linking.addEventListener('url', (state) => { - Report.openReportFromDeepLink(state.url); + // We need to pass 'isAuthenticated' to avoid loading a non-existing profile page twice + Report.openReportFromDeepLink(state.url, !isAuthenticatedRef.current); }); return () => { @@ -206,6 +211,11 @@ function Expensify({ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again }, []); + // This is being done since we want to play sound even when iOS device is on silent mode, to align with other platforms. + useEffect(() => { + Audio.setAudioModeAsync({playsInSilentModeIOS: true}); + }, []); + // Display a blank page until the onyx migration completes if (!isOnyxMigrated) { return null; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ddf37fba2354..a027a8493b41 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -121,6 +121,9 @@ const ONYXKEYS = { /** Contains the users's block expiration (if they have one) */ NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge', + /** Whether the user is blocked from chat */ + NVP_BLOCKED_FROM_CHAT: 'nvp_private_blockedFromChat', + /** A unique identifier that each user has that's used to send notifications */ NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'nvp_private_pushNotificationID', @@ -188,6 +191,9 @@ const ONYXKEYS = { /** User's Expensify Wallet */ USER_WALLET: 'userWallet', + /** User's metadata that will be used to segmentation */ + USER_METADATA: 'userMetadata', + /** Object containing Onfido SDK Token + applicantID */ WALLET_ONFIDO: 'walletOnfido', @@ -610,10 +616,12 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.SESSION]: OnyxTypes.Session; + [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; + [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; diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 9ad4643e834a..296ecce7d092 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -4,7 +4,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type {MaybePhraseKey} from '@libs/Localize'; -import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -149,8 +148,6 @@ function AddressForm({ label={translate('common.addressLine', {lineNumber: 1})} onValueChange={(data: unknown, key: unknown) => { onAddressChanged(data, key); - // This enforces the country selector to use the country from address instead of the country from URL - Navigation.setParams({country: undefined}); }} defaultValue={street1} renamedInputKeys={{ diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 31ccfc954513..d1dc42bb4678 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -23,7 +23,25 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import type {AddressSearchProps} from './types'; +import listViewOverflow from './listViewOverflow'; +import type {AddressSearchProps, PredefinedPlace} from './types'; + +/** + * Check if the place matches the search by the place name or description. + * @param search The search string for a place + * @param place The place to check for a match on the search + * @returns true if search is related to place, otherwise it returns false. + */ +function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean { + if (!search) { + return true; + } + if (!place) { + return false; + } + const fullSearchSentence = `${place.name ?? ''} ${place.description}`; + return search.split(' ').every((searchTerm) => !searchTerm || (searchTerm && fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()))); +} // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -41,6 +59,7 @@ function AddressSearch( isLimitedToUSA = false, label, maxInputLength, + onFocus, onBlur, onInputChange, onPress, @@ -263,7 +282,7 @@ function AddressSearch( } setIsFetchingCurrentLocation(false); - setLocationErrorCode(errorData.code); + setLocationErrorCode(errorData?.code ?? null); }, { maximumAge: 0, // No cache, always get fresh location info @@ -298,10 +317,16 @@ function AddressSearch( }; }, []); + const filteredPredefinedPlaces = useMemo(() => { + if (!isOffline || !searchValue) { + return predefinedPlaces ?? []; + } + return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; + }, [isOffline, predefinedPlaces, searchValue]); + const listEmptyComponent = useCallback( - () => - !!isOffline || !isTyping ? null : {translate('common.noResultsFound')}, - [isOffline, isTyping, styles, translate], + () => (!isTyping ? null : {translate('common.noResultsFound')}), + [isTyping, styles, translate], ); const listLoader = useCallback( @@ -338,11 +363,10 @@ function AddressSearch( ref={containerRef} > - {!!title && {title}} + {!!title && {title}} {subtitle} ); @@ -385,6 +409,7 @@ function AddressSearch( shouldSaveDraft, onFocus: () => { setIsFocused(true); + onFocus?.(); }, onBlur: (event) => { if (!isCurrentTargetInsideContainer(event, containerRef)) { @@ -414,10 +439,18 @@ function AddressSearch( }} styles={{ textInputContainer: [styles.flexColumn], - listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}], + listView: [ + StyleUtils.getGoogleListViewStyle(displayListViewBorder), + listViewOverflow, + styles.borderLeft, + styles.borderRight, + styles.flexGrow0, + !isFocused && styles.h0, + ], row: [styles.pv4, styles.ph3, styles.overflowAuto], description: [styles.googleSearchText], separator: [styles.googleSearchSeparator], + container: [styles.mh100], }} numberOfLines={2} isRowScrollable={false} @@ -441,11 +474,13 @@ function AddressSearch( ) } placeholder="" - /> - setLocationErrorCode(null)} - locationErrorCode={locationErrorCode} - /> + listViewDisplayed + > + setLocationErrorCode(null)} + locationErrorCode={locationErrorCode} + /> + {isFetchingCurrentLocation && } diff --git a/src/components/AddressSearch/listViewOverflow/index.native.ts b/src/components/AddressSearch/listViewOverflow/index.native.ts new file mode 100644 index 000000000000..36b9f4005376 --- /dev/null +++ b/src/components/AddressSearch/listViewOverflow/index.native.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-restricted-imports +import {defaultStyles} from '@styles/index'; + +export default defaultStyles.overflowHidden; diff --git a/src/components/AddressSearch/listViewOverflow/index.ts b/src/components/AddressSearch/listViewOverflow/index.ts new file mode 100644 index 000000000000..ae8bf35cc80c --- /dev/null +++ b/src/components/AddressSearch/listViewOverflow/index.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line no-restricted-imports +import {defaultStyles} from '@styles/index'; + +export default defaultStyles.overflowAuto; diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index bc7acf3f7e40..22cc3834b7a9 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -24,6 +24,10 @@ type StreetValue = { street: string; }; +type PredefinedPlace = Place & { + name?: string; +}; + type AddressSearchProps = { /** The ID used to uniquely identify the input in a Form */ inputID?: string; @@ -31,6 +35,9 @@ type AddressSearchProps = { /** Saves a draft of the input value when used in a form */ shouldSaveDraft?: boolean; + /** Callback that is called when the text input is focused */ + onFocus?: () => void; + /** Callback that is called when the text input is blurred */ onBlur?: () => void; @@ -65,7 +72,7 @@ type AddressSearchProps = { canUseCurrentLocation?: boolean; /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces?: Place[] | null; + predefinedPlaces?: PredefinedPlace[] | null; /** A map of inputID key names */ renamedInputKeys?: Address; @@ -85,4 +92,4 @@ type AddressSearchProps = { type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; -export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue}; +export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue, PredefinedPlace}; diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 0c8af3dfc826..595e28acd3bc 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -7,6 +7,7 @@ import {ShowContextMenuContext, showContextMenuForReport} from '@components/Show import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import * as Browser from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import * as ReportUtils from '@libs/ReportUtils'; import * as Download from '@userActions/Download'; @@ -48,7 +49,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow return; } Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, displayName).then(() => Download.setDownload(sourceID, false)); + fileDownload(sourceURLWithAuth, displayName, '', Browser.isMobileSafari()).then(() => Download.setDownload(sourceID, false)); }} onPressIn={onPressIn} onPressOut={onPressOut} diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index fb6a8e911e87..de98ba79d23c 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -607,7 +607,6 @@ export default withOnyx({ const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage.IOUTransactionID ?? '0' : '0'; return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, - initWithStoredValues: false, }, })(memo(AttachmentModal)); diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index ad4cf023c096..218a255d34d3 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -171,7 +171,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s */ const showDocumentPicker = useCallback( (): Promise => - RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error) => { + RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error: Error) => { if (RNDocumentPicker.isCancel(error)) { return; } @@ -247,7 +247,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s .then((result) => { completeAttachmentSelection.current(result); }) - .catch((error) => { + .catch((error: Error) => { showGeneralAlert(error.message); throw error; }); diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx deleted file mode 100644 index 839e05c419df..000000000000 --- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import {PixelRatio, View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; - -type AttachmentCarouselCellRendererProps = { - /** Cell Container styles */ - style?: StyleProp; -}; - -function AttachmentCarouselCellRenderer(props: AttachmentCarouselCellRendererProps) { - const styles = useThemeStyles(); - const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); - const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); - const style = [props.style, styles.h100, {width: PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}]; - - return ( - - ); -} - -AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer'; - -export default React.memo(AttachmentCarouselCellRenderer); diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 3a7e0f19c4cd..23b285faf10e 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,8 +1,10 @@ import isEqual from 'lodash/isEqual'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; -import {FlatList, Keyboard, PixelRatio, View} from 'react-native'; +import {Keyboard, PixelRatio, View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; +import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -10,12 +12,12 @@ import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenCo import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import AttachmentCarouselCellRenderer from './AttachmentCarouselCellRenderer'; import CarouselActions from './CarouselActions'; import CarouselButtons from './CarouselButtons'; import CarouselItem from './CarouselItem'; @@ -29,16 +31,23 @@ const viewabilityConfig = { itemVisiblePercentThreshold: 95, }; +const MIN_FLING_VELOCITY = 500; + function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); + const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const {isFullScreenRef} = useFullScreenContext(); - const scrollRef = useRef(null); + const scrollRef = useAnimatedRef>>(); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - const [containerWidth, setContainerWidth] = useState(0); + const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); + const cellWidth = useMemo( + () => PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2), + [modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth], + ); const [page, setPage] = useState(0); const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); @@ -51,6 +60,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions ?? undefined); if (isEqual(attachments, attachmentsFromReport)) { + if (attachments.length === 0) { + setPage(-1); + setDownloadButtonVisibility?.(false); + } return; } @@ -75,6 +88,17 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate]); + // Scroll position is affected when window width is resized, so we readjust it on width changes + useEffect(() => { + if (attachments.length === 0 || scrollRef.current == null) { + return; + } + + scrollRef.current.scrollToIndex({index: page, animated: false}); + // The hook is not supposed to run on page change, so we keep the page out of the dependencies + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cellWidth]); + /** Updates the page state when the user navigates between attachments */ const updatePage = useCallback( ({viewableItems}: UpdatePageProps) => { @@ -92,13 +116,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, return; } + const item: Attachment = entry.item; if (entry.index !== null) { setPage(entry.index); - setActiveSource(entry.item.source); + setActiveSource(item.source); } if (onNavigate) { - onNavigate(entry.item); + onNavigate(item); } }, [isFullScreenRef, onNavigate], @@ -120,7 +145,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen}); }, - [attachments, canUseTouchScreen, isFullScreenRef, page], + [attachments, canUseTouchScreen, isFullScreenRef, page, scrollRef], ); const extractItemKey = useCallback( @@ -132,35 +157,55 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /** Calculate items layout information to optimize scrolling performance */ const getItemLayout = useCallback( (data: ArrayLike | null | undefined, index: number) => ({ - length: containerWidth, - offset: containerWidth * index, + length: cellWidth, + offset: cellWidth * index, index, }), - [containerWidth], + [cellWidth], ); /** Defines how a single attachment should be rendered */ const renderItem = useCallback( ({item}: ListRenderItemInfo) => ( - setShouldShowArrows((oldState: boolean) => !oldState) : undefined} - isModalHovered={shouldShowArrows} - /> + + setShouldShowArrows((oldState) => !oldState) : undefined} + isModalHovered={shouldShowArrows} + /> + ), - [activeSource, canUseTouchScreen, setShouldShowArrows, shouldShowArrows], + [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + ); + /** Pan gesture handing swiping through attachments on touch screen devices */ + const pan = useMemo( + () => + Gesture.Pan() + .enabled(canUseTouchScreen) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) + .onEnd(({translationX, velocityX}) => { + let newIndex; + if (velocityX > MIN_FLING_VELOCITY) { + // User flung to the right + newIndex = Math.max(0, page - 1); + } else if (velocityX < -MIN_FLING_VELOCITY) { + // User flung to the left + newIndex = Math.min(attachments.length - 1, page + 1); + } else { + // snap scroll position to the nearest cell (making sure it's within the bounds of the list) + const delta = Math.round(-translationX / cellWidth); + newIndex = Math.min(attachments.length - 1, Math.max(0, page + delta)); + } + + scrollTo(scrollRef, newIndex * cellWidth, 0, true); + }), + [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef], ); return ( { - if (isFullScreenRef.current) { - return; - } - setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width)); - }} onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)} onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)} > @@ -184,36 +229,26 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, cancelAutoHideArrow={cancelAutoHideArrows} /> - {containerWidth > 0 && ( - + - )} + diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 32ac018acb04..4c11f1f0e35c 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,16 +1,12 @@ -import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -// We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another -import {ScrollView} from 'react-native-gesture-handler'; +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 ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; @@ -45,11 +41,10 @@ function BaseAutoCompleteSuggestions({ isSuggestionPickerLarge, keyExtractor, }: AutoCompleteSuggestionsProps) { - const {windowWidth, isLargeScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); - const scrollRef = useRef>(null); + const scrollRef = useRef>(null); /** * Render a suggestion menu item component. */ @@ -72,13 +67,7 @@ function BaseAutoCompleteSuggestions({ const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); - const estimatedListSize = useMemo( - () => ({ - height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, - width: (isLargeScreenWidth ? windowWidth - variables.sideBarWidth : windowWidth) - CONST.CHAT_FOOTER_HORIZONTAL_PADDING, - }), - [isLargeScreenWidth, suggestions.length, windowWidth], - ); + useEffect(() => { rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { duration: 100, @@ -105,14 +94,11 @@ function BaseAutoCompleteSuggestions({ }} > - rowHeight.value} diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index f633869ccd30..c0d89f4acf1e 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -80,18 +80,19 @@ function Avatar({ }, [originalSource]); const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; - const iconSize = StyleUtils.getAvatarSize(size); - const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; - const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; - - // We pass the color styles down to the SVG for the workspace and fallback avatar. - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, Number(avatarID)); + // If it's user avatar then accountID will be a number + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, avatarID as number); const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; const avatarSource = useFallBackAvatar ? fallbackAvatar : source; + // We pass the color styles down to the SVG for the workspace and fallback avatar. + const iconSize = StyleUtils.getAvatarSize(size); + const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; + const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; + let iconColors; if (isWorkspace) { iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(avatarID?.toString() ?? ''); diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 8942bf97a7dd..156c27abbc9d 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -12,7 +12,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; @@ -25,7 +25,7 @@ type AvatarWithDisplayNamePropsWithOnyx = { parentReportActions: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 5fa17e726c6f..0d5a825b79b5 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -49,6 +49,9 @@ type AvatarWithImagePickerProps = { /** Avatar source to display */ source?: AvatarSource; + /** Account id of user for which avatar is displayed */ + avatarID?: number | string; + /** Additional style props */ style?: StyleProp; @@ -123,6 +126,9 @@ type AvatarWithImagePickerProps = { /** Optionally override the default "Edit" icon */ editIcon?: IconAsset; + + /** Determines if a style utility function should be used for calculating the PopoverMenu anchor position. */ + shouldUseStyleUtilityForAnchorPosition?: boolean; }; function AvatarWithImagePicker({ @@ -136,6 +142,7 @@ function AvatarWithImagePicker({ errorRowStyles, onErrorClose = () => {}, source = '', + avatarID, fallbackIcon = Expensicons.FallbackAvatar, size = CONST.AVATAR_SIZE.DEFAULT, type = CONST.ICON_TYPE_AVATAR, @@ -152,6 +159,7 @@ function AvatarWithImagePicker({ enablePreview = false, shouldDisableViewPhoto = false, editIcon = Expensicons.Pencil, + shouldUseStyleUtilityForAnchorPosition = false, }: AvatarWithImagePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -298,7 +306,6 @@ function AvatarWithImagePicker({ - + {source ? ( )} - + {!disabled && ( diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 72dc53cceb39..4e5c238fd692 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -82,7 +82,7 @@ function Banner({ containerStyles, ]} > - + {shouldShowIcon && icon && ( ) : ( diff --git a/src/components/BlockedReportFooter.tsx b/src/components/BlockedReportFooter.tsx new file mode 100644 index 000000000000..89290545166c --- /dev/null +++ b/src/components/BlockedReportFooter.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Banner from './Banner'; + +function BlockedReportFooter() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const text = translate('youHaveBeenBanned'); + + return ( + + ); +} + +BlockedReportFooter.displayName = 'ArchivedReportFooter'; + +export default BlockedReportFooter; diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index ac7eb95191ee..4d135cdd88e2 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -27,7 +27,6 @@ function Composer( // On Android the selection prop is required on the TextInput but this prop has issues on IOS selection, value, - isGroupPolicyReport = false, ...props }: ComposerProps, ref: ForwardedRef, @@ -35,7 +34,7 @@ function Composer( const textInput = useRef(null); const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const theme = useTheme(); - const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); + const markdownStyle = useMarkdownStyle(value); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index f7bf277050a2..f8bff00f3689 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -70,14 +70,13 @@ function Composer( isReportActionCompose = false, isComposerFullSize = false, shouldContainScroll = false, - isGroupPolicyReport = false, ...props }: ComposerProps, ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); - const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); + const markdownStyle = useMarkdownStyle(value); const StyleUtils = useStyleUtils(); const textRef = useRef(null); const textInput = useRef(null); @@ -123,7 +122,7 @@ function Composer( if (shouldCalculateCaretPosition && isRendered) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { - setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); + setValueBeforeCaret((webEvent.target as HTMLInputElement).value.slice(0, webEvent.nativeEvent.selection.start)); setCaretContent(getNextChars(value ?? '', webEvent.nativeEvent.selection.start)); }); const selectionValue = { diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 0ff91111bd07..531bcd03f8bf 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -70,9 +70,6 @@ type ComposerProps = TextInputProps & { /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; - - /** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */ - isGroupPolicyReport?: boolean; }; export type {TextSelection, ComposerProps}; diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 361d82140326..02ed11afa7db 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -48,6 +48,7 @@ function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, textInputValue={searchValue} onChangeText={setSearchValue} onSelectRow={onSelect} + shouldDebounceRowSelect headerMessage={headerMessage} initiallyFocusedOptionKey={initiallySelectedCurrencyCode} showScrollIndicator diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 63889f76e67c..f4216dcc9f8a 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -21,7 +21,7 @@ type EReceiptThumbnailOnyxProps = { transaction: OnyxEntry; }; -type IconSize = 'small' | 'medium' | 'large'; +type IconSize = 'x-small' | 'small' | 'medium' | 'large'; type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { /** TransactionID of the transaction this EReceipt corresponds to. It's used by withOnyx HOC */ @@ -40,7 +40,7 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { /** Center the eReceipt Icon vertically */ centerIconV?: boolean; - /** Size of the eReceipt icon. Possible values 'small', 'medium' or 'large' */ + /** Size of the eReceipt icon. Possible values 'x-small', 'small', 'medium' or 'large' */ iconSize?: IconSize; }; @@ -72,8 +72,16 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; let labelFontSize: number = variables.fontSizeNormal; let labelLineHeight: number = variables.lineHeightLarge; + let backgroundImageMinWidth: number = variables.eReceiptBackgroundImageMinWidth; - if (iconSize === 'small') { + if (iconSize === 'x-small') { + receiptIconWidth = variables.eReceiptIconWidthXSmall; + receiptIconHeight = variables.eReceiptIconHeightXSmall; + receiptMCCSize = variables.iconSizeXSmall; + labelFontSize = variables.fontSizeExtraSmall; + labelLineHeight = variables.lineHeightXSmall; + backgroundImageMinWidth = variables.w80; + } else if (iconSize === 'small') { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; @@ -100,7 +108,7 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT > diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 3d20f910dca0..b6699c0cf3f4 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -74,6 +74,9 @@ type FormProviderProps = FormProvider /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; + + /** Whether the form container should grow or adapt to the viewable available space */ + shouldContainerGrow?: boolean; }; function FormProvider( diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 5c74fd466a15..e7c9dba5c77a 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useMemo, useRef} from 'react'; import type {RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {ScrollView as RNScrollView, StyleProp, View, ViewStyle} from 'react-native'; -import {Keyboard} from 'react-native'; +import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native'; +import {Keyboard, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -32,6 +32,9 @@ type FormWrapperProps = ChildrenProps & /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; + /** Whether the form container should grow or adapt to the viewable available space */ + shouldContainerGrow?: boolean; + /** Server side errors keyed by microtime */ errors: FormInputErrors; @@ -60,6 +63,7 @@ function FormWrapper({ scrollContextEnabled = false, shouldHideFixErrorsAlert = false, disablePressOnEnter = true, + shouldContainerGrow = true, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -104,7 +108,7 @@ function FormWrapper({ ref={formContentRef} style={[style, safeAreaPaddingBottomStyle.paddingBottom ? safeAreaPaddingBottomStyle : styles.pb5]} > - {children} + {children} {isSubmitButtonVisible && ( @@ -164,7 +168,7 @@ function FormWrapper({ ) : ( diff --git a/src/components/FullstoryNative.tsx b/src/components/FullstoryNative.tsx new file mode 100644 index 000000000000..63e6217a464e --- /dev/null +++ b/src/components/FullstoryNative.tsx @@ -0,0 +1,3 @@ +import FS from '@fullstory/react-native'; + +export default FS; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index 6802a4518ba1..095dcb294857 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -18,7 +18,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { const attrHref = htmlAttribs.href || htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] || ''; const sourceURL = tryResolveUrlFromApiRoot(attrHref); const fileName = FileUtils.getFileName(`${sourceURL}`); - const thumbnailUrl = htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_URL_ATTRIBUTE]; + const thumbnailUrl = tryResolveUrlFromApiRoot(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_URL_ATTRIBUTE]); const width = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE]); const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]); const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]); diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index c147c3735e96..cadf0f01e225 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -127,6 +127,7 @@ import Printer from '@assets/images/printer.svg'; import Profile from '@assets/images/profile.svg'; import QrCode from '@assets/images/qrcode.svg'; import QuestionMark from '@assets/images/question-mark-circle.svg'; +import ReceiptPlus from '@assets/images/receipt-plus.svg'; import ReceiptScan from '@assets/images/receipt-scan.svg'; import ReceiptSearch from '@assets/images/receipt-search.svg'; import Receipt from '@assets/images/receipt.svg'; @@ -297,6 +298,7 @@ export { QrCode, QuestionMark, Receipt, + ReceiptPlus, ReceiptScan, RemoveMembers, ReceiptSearch, diff --git a/src/components/Icon/__mocks__/Expensicons.ts b/src/components/Icon/__mocks__/Expensicons.ts index 78ad86210bf0..e268b109f0a5 100644 --- a/src/components/Icon/__mocks__/Expensicons.ts +++ b/src/components/Icon/__mocks__/Expensicons.ts @@ -1,4 +1,7 @@ -const Expensicons = jest.requireActual('../Expensicons'); +// eslint-disable-next-line import/no-import-module-exports +import type {SvgProps} from 'react-native-svg'; + +const Expensicons = jest.requireActual>>('../Expensicons'); module.exports = Object.keys(Expensicons).reduce((prev, curr) => { // We set the name of the anonymous mock function here so we can dynamically build the list of mocks and access the diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx index a3df93844ca9..710f045ede4e 100644 --- a/src/components/InitialURLContextProvider.tsx +++ b/src/components/InitialURLContextProvider.tsx @@ -1,6 +1,5 @@ -import React, {createContext, useEffect, useState} from 'react'; +import React, {createContext} from 'react'; import type {ReactNode} from 'react'; -import {Linking} from 'react-native'; import type {Route} from '@src/ROUTES'; /** Initial url that will be opened when NewDot is embedded into Hybrid App. */ @@ -15,16 +14,7 @@ type InitialURLContextProviderProps = { }; function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) { - const [initialURL, setInitialURL] = useState(url); - useEffect(() => { - if (initialURL) { - return; - } - Linking.getInitialURL().then((initURL) => { - setInitialURL(initURL as Route); - }); - }, [initialURL]); - return {children}; + return {children}; } InitialURLContextProvider.displayName = 'InitialURLContextProvider'; diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index d37e00727fa6..8c4a131284c8 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -11,7 +11,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PaymentUtils from '@libs/PaymentUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; -import * as Policy from '@userActions/Policy'; +import * as Policy from '@userActions/Policy/Policy'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/components/KeyboardAvoidingView/index.ios.tsx b/src/components/KeyboardAvoidingView/index.ios.tsx index a7cd767377ef..171210eab7ac 100644 --- a/src/components/KeyboardAvoidingView/index.ios.tsx +++ b/src/components/KeyboardAvoidingView/index.ios.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; -import type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx index 09ec21e5b219..c0882ae1e9cc 100644 --- a/src/components/KeyboardAvoidingView/index.tsx +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {View} from 'react-native'; -import type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; diff --git a/src/components/KeyboardAvoidingView/types.ts b/src/components/KeyboardAvoidingView/types.ts index 48d354e8b53f..2c1ef64ced8f 100644 --- a/src/components/KeyboardAvoidingView/types.ts +++ b/src/components/KeyboardAvoidingView/types.ts @@ -1,3 +1,4 @@ -import {KeyboardAvoidingViewProps} from 'react-native'; +import type {KeyboardAvoidingViewProps} from 'react-native'; -export default KeyboardAvoidingViewProps; +// eslint-disable-next-line import/prefer-default-export +export type {KeyboardAvoidingViewProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 6bb900ecb489..b0418c9fdb28 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -58,13 +58,24 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); - if (!optionItem) { + if (!optionItem && !isFocused) { // rendering null as a render item causes the FlashList to render all - // its children and consume signficant memory. We can avoid this by - // rendering a placeholder view instead. + // its children and consume signficant memory on the first render. We can avoid this by + // rendering a placeholder view instead. This behaviour is only observed when we + // first sign in to the App. + // We can fix this by checking if the optionItem is null and the component is not focused. + // Which means that the currentReportID is not the same as the reportID. The currentReportID + // in this case is empty and hence the component is not focused. return ; } + if (!optionItem) { + // This is the case when the component is focused and the optionItem is null. + // For example, when you submit an expense in offline mode and click on the + // generated expense report, we would only see the Report Details but no item in LHN. + return null; + } + const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index b1021350435c..5174db6fd32f 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -5,9 +5,11 @@ import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, u import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import setUserLocation from '@libs/actions/UserLocation'; +import * as UserLocation from '@libs/actions/UserLocation'; import compose from '@libs/compose'; import getCurrentPosition from '@libs/getCurrentPosition'; +import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; +import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; import CONST from '@src/CONST'; import useLocalize from '@src/hooks/useLocalize'; import useNetwork from '@src/hooks/useNetwork'; @@ -38,13 +40,16 @@ const MapView = forwardRef( // if there are one or more waypoints present. const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); - const setCurrentPositionToInitialState = useCallback(() => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (cachedUserLocation || !initialState) { - return; - } - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); - }, [initialState, cachedUserLocation]); + const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback( + (error) => { + if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialState) { + return; + } + UserLocation.clearUserLocation(); + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, + [initialState], + ); useFocusEffect( useCallback(() => { @@ -66,7 +71,7 @@ const MapView = forwardRef( getCurrentPosition((params) => { const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; setCurrentPosition(currentCoords); - setUserLocation(currentCoords); + UserLocation.setUserLocation(currentCoords); }, setCurrentPositionToInitialState); }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), ); diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index e16289ee948d..e75d47305d90 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -13,7 +13,9 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import setUserLocation from '@userActions/UserLocation'; +import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; +import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; +import * as UserLocation from '@userActions/UserLocation'; import CONST from '@src/CONST'; import useLocalize from '@src/hooks/useLocalize'; import useNetwork from '@src/hooks/useNetwork'; @@ -62,13 +64,16 @@ const MapView = forwardRef( // if there are one or more waypoints present. const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); - const setCurrentPositionToInitialState = useCallback(() => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (cachedUserLocation || !initialState) { - return; - } - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); - }, [initialState, cachedUserLocation]); + const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback( + (error) => { + if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialState) { + return; + } + UserLocation.clearUserLocation(); + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, + [initialState], + ); useFocusEffect( useCallback(() => { @@ -90,7 +95,7 @@ const MapView = forwardRef( getCurrentPosition((params) => { const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; setCurrentPosition(currentCoords); - setUserLocation(currentCoords); + UserLocation.setUserLocation(currentCoords); }, setCurrentPositionToInitialState); }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), ); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index ad06f9fcb78c..1d4c57c167ff 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -527,7 +527,7 @@ function MenuItem( diff --git a/src/components/MenuItemGroup.tsx b/src/components/MenuItemGroup.tsx index 8dc8586028d8..ed34778dd941 100644 --- a/src/components/MenuItemGroup.tsx +++ b/src/components/MenuItemGroup.tsx @@ -5,7 +5,7 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; type MenuItemGroupContextProps = { isExecuting: boolean; - singleExecution: (action?: Action | undefined) => (...params: T) => void; + singleExecution: (action: Action) => (...params: T) => void; waitForNavigate: ReturnType; }; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4eb3be871a8d..aad801a7c259 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -13,6 +13,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; +import * as TransactionActions from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -29,21 +30,7 @@ import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import SettlementButton from './SettlementButton'; -type MoneyReportHeaderOnyxProps = { - /** The chat report this report is linked to */ - chatReport: OnyxEntry; - - /** The next step for the report */ - nextStep: OnyxEntry; - - /** Session info for the currently logged in user. */ - session: OnyxEntry; - - /** The transaction thread report associated with the current report, if any */ - transactionThreadReport: OnyxEntry; -}; - -type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { +type MoneyReportHeaderProps = { /** The report currently being looked at */ report: OnyxTypes.Report; @@ -64,17 +51,12 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { onBackButtonPress: () => void; }; -function MoneyReportHeader({ - session, - policy, - chatReport, - nextStep, - report: moneyRequestReport, - transactionThreadReport, - reportActions, - shouldUseNarrowLayout = false, - onBackButtonPress, -}: MoneyReportHeaderProps) { +function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyReportHeaderProps) { + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport.chatReportID}`); + const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport.reportID}`); + const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); + const [session] = useOnyx(ONYXKEYS.SESSION); + const styles = useThemeStyles(); const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); @@ -104,6 +86,7 @@ function MoneyReportHeader({ const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID); const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs); @@ -125,14 +108,16 @@ function MoneyReportHeader({ const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !allHavePendingRTERViolation; - const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; + const shouldShowNextStep = + !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !allHavePendingRTERViolation && !hasScanningReceipt; + const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; - const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && shouldUseNarrowLayout); + const isMoreContentShown = shouldShowNextStep || hasScanningReceipt || (shouldShowAnyButton && shouldUseNarrowLayout); const confirmPayment = (type?: PaymentMethodType | undefined) => { if (!type) { @@ -169,6 +154,16 @@ function MoneyReportHeader({ setIsDeleteRequestModalVisible(false); }, [moneyRequestReport?.reportID, requestParentReportAction, setIsDeleteRequestModalVisible]); + const markAsCash = useCallback(() => { + if (!requestParentReportAction) { + return; + } + const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + const reportID = transactionThreadReport?.reportID ?? ''; + + TransactionActions.markAsCash(iouTransactionID, reportID); + }, [requestParentReportAction, transactionThreadReport?.reportID]); + // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled, @@ -212,7 +207,7 @@ function MoneyReportHeader({ shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={onBackButtonPress} // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout) && !allHavePendingRTERViolation} + shouldShowBorderBottom={!isMoreContentShown && !allHavePendingRTERViolation} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -249,20 +244,44 @@ function MoneyReportHeader({ /> )} + {shouldShowMarkAsCashButton && !shouldUseNarrowLayout && ( + +