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/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 353a898a941f..bb24a1be5e5c 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -352,6 +352,16 @@ jobs: env: CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} + # Build a version of iOS and Android HybridApp if we are deploying to staging + hybridApp: + runs-on: ubuntu-latest + needs: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} + steps: + - name: 'Deploy HybridApp' + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: Expensify/Mobile-Deploy/.github/workflows/deploy.yml@main + postSlackMessageOnFailure: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest 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/.storybook/public/favicon.svg b/.storybook/public/favicon.svg index 6bc34f89282e..726791b58cfb 100644 --- a/.storybook/public/favicon.svg +++ b/.storybook/public/favicon.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/README.md b/README.md index 29a9e9b8ffdc..6544e0e95486 100644 --- a/README.md +++ b/README.md @@ -663,7 +663,38 @@ Sometimes it might be beneficial to generate a local production version instead In order to generate a production web build, run `npm run build`, this will generate a production javascript build in the `dist/` folder. #### Local production build of the MacOS desktop app -In order to compile a production desktop build, run `npm run desktop-build`, this will generate a production app in the `dist/Mac` folder named `Chat.app`. +The commands used to compile a production or staging desktop build are `npm run desktop-build` and `npm run desktop-build-staging`, respectively. These will product an app in the `dist/Mac` folder named NewExpensify.dmg that you can install like a normal app. + +HOWEVER, by default those commands will try to notarize the build (signing it as Expensify) and publish it to the S3 bucket where it's hosted for users. In most cases you won't actually need or want to do that for your local testing. To get around that and disable those behaviors for your local build, apply the following diff: + +```diff +diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js +index e4ed685f65..4c7c1b3667 100644 +--- a/config/electronBuilder.config.js ++++ b/config/electronBuilder.config.js +@@ -42,9 +42,6 @@ module.exports = { + entitlements: 'desktop/entitlements.mac.plist', + entitlementsInherit: 'desktop/entitlements.mac.plist', + type: 'distribution', +- notarize: { +- teamId: '368M544MTT', +- }, + }, + dmg: { + title: 'New Expensify', +diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh +index 791f59d733..526306eec1 100755 +--- a/scripts/build-desktop.sh ++++ b/scripts/build-desktop.sh +@@ -35,4 +35,4 @@ npx webpack --config config/webpack/webpack.desktop.ts --env file=$ENV_FILE + title "Building Desktop App Archive Using Electron" + info "" + shift 1 +-npx electron-builder --config config/electronBuilder.config.js --publish always "$@" ++npx electron-builder --config config/electronBuilder.config.js --publish never "$@" +``` + +There may be some cases where you need to test a signed and published build, such as when testing the update flows. Instructions on setting that up can be found in [Testing Electron Auto-Update](https://github.com/Expensify/App/blob/main/desktop/README.md#testing-electron-auto-update). Good luck 🙃 #### Local production build the iOS app In order to compile a production iOS build, run `npm run ios-build`, this will generate a `Chat.ipa` in the root directory of this project. diff --git a/__mocks__/react-native-permissions.ts b/__mocks__/react-native-permissions.ts index 67b7db830d94..d98b7f32a611 100644 --- a/__mocks__/react-native-permissions.ts +++ b/__mocks__/react-native-permissions.ts @@ -35,30 +35,30 @@ const requestNotifications: jest.Mock = jest.fn((options: Record notificationOptions.includes(option)) - .reduce((acc: NotificationSettings, option: string) => ({...acc, [option]: true}), { - lockScreen: true, - notificationCenter: true, - }), + .reduce( + (acc: NotificationSettings, option: string) => { + acc[option] = true; + return acc; + }, + { + lockScreen: true, + notificationCenter: true, + }, + ), })); const checkMultiple: jest.Mock = jest.fn((permissions: string[]) => - permissions.reduce( - (acc: ResultsCollection, permission: string) => ({ - ...acc, - [permission]: RESULTS.GRANTED, - }), - {}, - ), + permissions.reduce((acc: ResultsCollection, permission: string) => { + acc[permission] = RESULTS.GRANTED; + return acc; + }, {}), ); const requestMultiple: jest.Mock = jest.fn((permissions: string[]) => - permissions.reduce( - (acc: ResultsCollection, permission: string) => ({ - ...acc, - [permission]: RESULTS.GRANTED, - }), - {}, - ), + permissions.reduce((acc: ResultsCollection, permission: string) => { + acc[permission] = RESULTS.GRANTED; + return acc; + }, {}), ); export { diff --git a/android/app/build.gradle b/android/app/build.gradle index 9e5a237afd1e..f1429500b76e 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 1001047804 + versionName "1.4.78-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" @@ -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/all.svg b/assets/images/all.svg index d1a833d280ce..f6d9f46fc92e 100644 --- a/assets/images/all.svg +++ b/assets/images/all.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/arrow-down-long.svg b/assets/images/arrow-down-long.svg new file mode 100644 index 000000000000..cbf6e7e5ad2f --- /dev/null +++ b/assets/images/arrow-down-long.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/arrow-right.svg b/assets/images/arrow-right.svg index 8d2ded92e791..649582544847 100644 --- a/assets/images/arrow-right.svg +++ b/assets/images/arrow-right.svg @@ -1,10 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/arrow-up-long.svg b/assets/images/arrow-up-long.svg new file mode 100644 index 000000000000..13d7a0c2d67e --- /dev/null +++ b/assets/images/arrow-up-long.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/avatars/fallback-avatar.svg b/assets/images/avatars/fallback-avatar.svg index 69293d72aed9..4a7fecf967db 100644 --- a/assets/images/avatars/fallback-avatar.svg +++ b/assets/images/avatars/fallback-avatar.svg @@ -1,10 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_1.svg b/assets/images/avatars/group/default-avatar_1.svg index 5d97c5bf855b..1edcaa33a8aa 100644 --- a/assets/images/avatars/group/default-avatar_1.svg +++ b/assets/images/avatars/group/default-avatar_1.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_10.svg b/assets/images/avatars/group/default-avatar_10.svg index 12c9dd76ae31..62e818cb3e45 100644 --- a/assets/images/avatars/group/default-avatar_10.svg +++ b/assets/images/avatars/group/default-avatar_10.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_11.svg b/assets/images/avatars/group/default-avatar_11.svg index 97f17f30f3a7..2f976b05519d 100644 --- a/assets/images/avatars/group/default-avatar_11.svg +++ b/assets/images/avatars/group/default-avatar_11.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_12.svg b/assets/images/avatars/group/default-avatar_12.svg index f917fb136582..c29992aa1793 100644 --- a/assets/images/avatars/group/default-avatar_12.svg +++ b/assets/images/avatars/group/default-avatar_12.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_13.svg b/assets/images/avatars/group/default-avatar_13.svg index 9e59fb9123a5..5f6b69f01fe3 100644 --- a/assets/images/avatars/group/default-avatar_13.svg +++ b/assets/images/avatars/group/default-avatar_13.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_14.svg b/assets/images/avatars/group/default-avatar_14.svg index ca071e488416..27096ffd77d7 100644 --- a/assets/images/avatars/group/default-avatar_14.svg +++ b/assets/images/avatars/group/default-avatar_14.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_15.svg b/assets/images/avatars/group/default-avatar_15.svg index f227cc0717be..7cae7b1e6562 100644 --- a/assets/images/avatars/group/default-avatar_15.svg +++ b/assets/images/avatars/group/default-avatar_15.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_16.svg b/assets/images/avatars/group/default-avatar_16.svg index efbb85f0b13d..1c02725ba669 100644 --- a/assets/images/avatars/group/default-avatar_16.svg +++ b/assets/images/avatars/group/default-avatar_16.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_17.svg b/assets/images/avatars/group/default-avatar_17.svg index 25c015c595ca..58a5014fae68 100644 --- a/assets/images/avatars/group/default-avatar_17.svg +++ b/assets/images/avatars/group/default-avatar_17.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_18.svg b/assets/images/avatars/group/default-avatar_18.svg index a58ee6e66eff..43eeffb3db8d 100644 --- a/assets/images/avatars/group/default-avatar_18.svg +++ b/assets/images/avatars/group/default-avatar_18.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_2.svg b/assets/images/avatars/group/default-avatar_2.svg index ff1cc3e6dd2d..f67a49d28cd2 100644 --- a/assets/images/avatars/group/default-avatar_2.svg +++ b/assets/images/avatars/group/default-avatar_2.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_3.svg b/assets/images/avatars/group/default-avatar_3.svg index dde31b5d02a0..471d3a348b4a 100644 --- a/assets/images/avatars/group/default-avatar_3.svg +++ b/assets/images/avatars/group/default-avatar_3.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_4.svg b/assets/images/avatars/group/default-avatar_4.svg index f6d02801bc6b..46e22d28b6df 100644 --- a/assets/images/avatars/group/default-avatar_4.svg +++ b/assets/images/avatars/group/default-avatar_4.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_5.svg b/assets/images/avatars/group/default-avatar_5.svg index fdabd36e2058..a81471170e23 100644 --- a/assets/images/avatars/group/default-avatar_5.svg +++ b/assets/images/avatars/group/default-avatar_5.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_6.svg b/assets/images/avatars/group/default-avatar_6.svg index 6f1c6b80eda6..71da5e5631f3 100644 --- a/assets/images/avatars/group/default-avatar_6.svg +++ b/assets/images/avatars/group/default-avatar_6.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_7.svg b/assets/images/avatars/group/default-avatar_7.svg index 62d9a8b76bb8..080426ca0454 100644 --- a/assets/images/avatars/group/default-avatar_7.svg +++ b/assets/images/avatars/group/default-avatar_7.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_8.svg b/assets/images/avatars/group/default-avatar_8.svg index 206b10c2322b..b6b2d98579eb 100644 --- a/assets/images/avatars/group/default-avatar_8.svg +++ b/assets/images/avatars/group/default-avatar_8.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/avatars/group/default-avatar_9.svg b/assets/images/avatars/group/default-avatar_9.svg index ffbe02ce57e8..14885d4c401c 100644 --- a/assets/images/avatars/group/default-avatar_9.svg +++ b/assets/images/avatars/group/default-avatar_9.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/back-left.svg b/assets/images/back-left.svg index 2ddd554e9720..2c709401916f 100644 --- a/assets/images/back-left.svg +++ b/assets/images/back-left.svg @@ -1,10 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/check-circle.svg b/assets/images/check-circle.svg new file mode 100644 index 000000000000..c13b83cbf281 --- /dev/null +++ b/assets/images/check-circle.svg @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/assets/images/coins.svg b/assets/images/coins.svg index aa3c68e72ea8..164fa84388f5 100644 --- a/assets/images/coins.svg +++ b/assets/images/coins.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/comment-bubbles.svg b/assets/images/comment-bubbles.svg new file mode 100644 index 000000000000..1277b8958c94 --- /dev/null +++ b/assets/images/comment-bubbles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/connection-complete.svg b/assets/images/connection-complete.svg index fbfb2b041358..d864d9a33626 100644 --- a/assets/images/connection-complete.svg +++ b/assets/images/connection-complete.svg @@ -1,330 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/credit-card-hourglass.svg b/assets/images/credit-card-hourglass.svg index 2acd013fbe59..28ffe766b597 100644 --- a/assets/images/credit-card-hourglass.svg +++ b/assets/images/credit-card-hourglass.svg @@ -1,19 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/crosshair.svg b/assets/images/crosshair.svg new file mode 100644 index 000000000000..357faab49178 --- /dev/null +++ b/assets/images/crosshair.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg index cce2e3027cea..729bc98d4f8a 100644 --- a/assets/images/document-plus.svg +++ b/assets/images/document-plus.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/assets/images/document-slash.svg b/assets/images/document-slash.svg index ebb183142e40..e8a0ff20702e 100644 --- a/assets/images/document-slash.svg +++ b/assets/images/document-slash.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/integrationicons/qbo-icon-square.svg b/assets/images/integrationicons/qbo-icon-square.svg index a8ce3468ffbf..e297b597f980 100644 --- a/assets/images/integrationicons/qbo-icon-square.svg +++ b/assets/images/integrationicons/qbo-icon-square.svg @@ -1,14 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/integrationicons/xero-icon-square.svg b/assets/images/integrationicons/xero-icon-square.svg index 94b79bb3533d..43774919c92c 100644 --- a/assets/images/integrationicons/xero-icon-square.svg +++ b/assets/images/integrationicons/xero-icon-square.svg @@ -1,32 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/invoice-generic.svg b/assets/images/invoice-generic.svg index d0e2662c4084..251918c4cff4 100644 --- a/assets/images/invoice-generic.svg +++ b/assets/images/invoice-generic.svg @@ -1,15 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/money-waving.svg b/assets/images/money-waving.svg index 5242e31092a0..e68744d595be 100644 --- a/assets/images/money-waving.svg +++ b/assets/images/money-waving.svg @@ -1,81 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify-adhoc.svg b/assets/images/new-expensify-adhoc.svg index b3dd92fbbaae..8da6331c8c94 100644 --- a/assets/images/new-expensify-adhoc.svg +++ b/assets/images/new-expensify-adhoc.svg @@ -1,31 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify-dev.svg b/assets/images/new-expensify-dev.svg index 316da6b5aa4d..fcb371f586b6 100644 --- a/assets/images/new-expensify-dev.svg +++ b/assets/images/new-expensify-dev.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify-stg.svg b/assets/images/new-expensify-stg.svg index 1a1994c7a9fd..d536257fc880 100644 --- a/assets/images/new-expensify-stg.svg +++ b/assets/images/new-expensify-stg.svg @@ -1,35 +1 @@ - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/play.svg b/assets/images/play.svg index 5f7e14969529..98a8c00520fc 100644 --- a/assets/images/play.svg +++ b/assets/images/play.svg @@ -1,6 +1 @@ - - - - - + \ No newline at end of file diff --git a/assets/images/qrcode.svg b/assets/images/qrcode.svg index 42c49c958246..47d61d7dd47c 100644 --- a/assets/images/qrcode.svg +++ b/assets/images/qrcode.svg @@ -1,14 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/receipt-plus.svg b/assets/images/receipt-plus.svg new file mode 100644 index 000000000000..ca4d96b3dfa5 --- /dev/null +++ b/assets/images/receipt-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg index c93986de3c9b..f7c164c948c8 100644 --- a/assets/images/receipt-scan.svg +++ b/assets/images/receipt-scan.svg @@ -1,14 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__abacus.svg b/assets/images/simple-illustrations/simple-illustration__abacus.svg index df94ab653982..6dac0e9009b1 100644 --- a/assets/images/simple-illustrations/simple-illustration__abacus.svg +++ b/assets/images/simple-illustrations/simple-illustration__abacus.svg @@ -1,43 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__accounting.svg b/assets/images/simple-illustrations/simple-illustration__accounting.svg index f7634141e966..3213b4f93856 100644 --- a/assets/images/simple-illustrations/simple-illustration__accounting.svg +++ b/assets/images/simple-illustrations/simple-illustration__accounting.svg @@ -1,32 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__alert.svg b/assets/images/simple-illustrations/simple-illustration__alert.svg index 2e7bca02f5e3..cbf70b7655a7 100644 --- a/assets/images/simple-illustrations/simple-illustration__alert.svg +++ b/assets/images/simple-illustrations/simple-illustration__alert.svg @@ -1,15 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__binoculars.svg b/assets/images/simple-illustrations/simple-illustration__binoculars.svg index 381be8988873..5abacd359464 100644 --- a/assets/images/simple-illustrations/simple-illustration__binoculars.svg +++ b/assets/images/simple-illustrations/simple-illustration__binoculars.svg @@ -1,50 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__car-ice.svg b/assets/images/simple-illustrations/simple-illustration__car-ice.svg index ba2b79bca6aa..9da1b844c101 100644 --- a/assets/images/simple-illustrations/simple-illustration__car-ice.svg +++ b/assets/images/simple-illustrations/simple-illustration__car-ice.svg @@ -1,53 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__car.svg b/assets/images/simple-illustrations/simple-illustration__car.svg index 2d420be6c3a9..9da1b844c101 100644 --- a/assets/images/simple-illustrations/simple-illustration__car.svg +++ b/assets/images/simple-illustrations/simple-illustration__car.svg @@ -1,25 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg new file mode 100644 index 000000000000..a96a7e5dc0af --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__coins.svg b/assets/images/simple-illustrations/simple-illustration__coins.svg index 5350886402c6..5caa1c0635d5 100644 --- a/assets/images/simple-illustrations/simple-illustration__coins.svg +++ b/assets/images/simple-illustrations/simple-illustration__coins.svg @@ -1,26 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__company-card.svg b/assets/images/simple-illustrations/simple-illustration__company-card.svg index 4121bbeeb205..1f4e43dbc047 100644 --- a/assets/images/simple-illustrations/simple-illustration__company-card.svg +++ b/assets/images/simple-illustrations/simple-illustration__company-card.svg @@ -1,38 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__lightbulb.svg b/assets/images/simple-illustrations/simple-illustration__lightbulb.svg index 1dc359764147..62a9cb0c3b76 100644 --- a/assets/images/simple-illustrations/simple-illustration__lightbulb.svg +++ b/assets/images/simple-illustrations/simple-illustration__lightbulb.svg @@ -1,33 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__pencil.svg b/assets/images/simple-illustrations/simple-illustration__pencil.svg index 8d9f06991612..d3eaf8771021 100644 --- a/assets/images/simple-illustrations/simple-illustration__pencil.svg +++ b/assets/images/simple-illustrations/simple-illustration__pencil.svg @@ -1,20 +1 @@ - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__piggybank.svg b/assets/images/simple-illustrations/simple-illustration__piggybank.svg index be87ff34752a..ab1f73113f18 100644 --- a/assets/images/simple-illustrations/simple-illustration__piggybank.svg +++ b/assets/images/simple-illustrations/simple-illustration__piggybank.svg @@ -1,50 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__receiptupload.svg b/assets/images/simple-illustrations/simple-illustration__receiptupload.svg index b8fe5101715f..efff624f481f 100644 --- a/assets/images/simple-illustrations/simple-illustration__receiptupload.svg +++ b/assets/images/simple-illustrations/simple-illustration__receiptupload.svg @@ -1,22 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__sendmoney.svg b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg new file mode 100644 index 000000000000..80393e3c30cf --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__splitbill.svg b/assets/images/simple-illustrations/simple-illustration__splitbill.svg index dfed7535ee90..1390a7cf9205 100644 --- a/assets/images/simple-illustrations/simple-illustration__splitbill.svg +++ b/assets/images/simple-illustrations/simple-illustration__splitbill.svg @@ -1,55 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__tag.svg b/assets/images/simple-illustrations/simple-illustration__tag.svg index 0cac51679a5e..0a93014d11b3 100644 --- a/assets/images/simple-illustrations/simple-illustration__tag.svg +++ b/assets/images/simple-illustrations/simple-illustration__tag.svg @@ -1,33 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg b/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg index b4edd9513722..27ce709889dd 100644 --- a/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg +++ b/assets/images/simple-illustrations/simple-illustration__teachers-unite.svg @@ -1,49 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg index b684c58126f7..c11d2663997f 100644 --- a/assets/images/simple-illustrations/simple-illustration__workflows.svg +++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg @@ -1,153 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/stopwatch.svg b/assets/images/stopwatch.svg index 0f26af219e04..b8ca46fd1fa1 100644 --- a/assets/images/stopwatch.svg +++ b/assets/images/stopwatch.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/suitcase.svg b/assets/images/suitcase.svg index 97036db6b5ac..452c44f73e22 100644 --- a/assets/images/suitcase.svg +++ b/assets/images/suitcase.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/tag.svg b/assets/images/tag.svg index f5e13b8135cb..f25bcbe47f71 100644 --- a/assets/images/tag.svg +++ b/assets/images/tag.svg @@ -1,12 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/thread.svg b/assets/images/thread.svg index 3b8f334fafdd..9f01ce7b2c06 100644 --- a/assets/images/thread.svg +++ b/assets/images/thread.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/x-circle.svg b/assets/images/x-circle.svg index c186e41c4244..5fa5f3741567 100644 --- a/assets/images/x-circle.svg +++ b/assets/images/x-circle.svg @@ -1,12 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 9f8b7a711d78..060bc0313950 100644 --- a/babel.config.js +++ b/babel.config.js @@ -10,6 +10,9 @@ const defaultPlugins = [ '@babel/transform-runtime', '@babel/plugin-proposal-class-properties', + // This will serve to map the classes correctly in FullStory + '@fullstory/babel-plugin-annotate-react', + // We use `transform-class-properties` for transforming ReactNative libraries and do not use it for our own // source code transformation as we do not use class property assignment. 'transform-class-properties', @@ -35,6 +38,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/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index 08a444a6b8e4..cc3e256be399 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -96,6 +96,79 @@ These steps are covered in more detail in the "testing" section below. Due to some technical constraints, Apple and Google sign-in may require additional configuration to be able to work in the development environment as expected. This document describes any additional steps for each platform. +## Show Apple / Google SSO buttons development environment + +The Apple/Google Sign In button renders differently in development mode. To prevent confusion +for developers about a possible regression, we decided to not render third party buttons in +development mode. + +To re-enable the SSO buttons in development mode, remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components: + +```diff +diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx +index 4286a26033..850f8944ca 100644 +--- a/src/pages/signin/LoginForm/BaseLoginForm.tsx ++++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx +@@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false + // for developers about possible regressions, we won't render buttons in development mode. + // For more information about these differences and how to test in development mode, + // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md` +- CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && ( ++ ( + + `Swift Default Apps` => `URI Schemes` => `new-expensify` and select `New Expensify.app` +4. Note that a dev build of the desktop app will not work. You'll create and install a local staging build: + 1. Update `build-desktop.sh` replacing `--publish always` with `--publish never`. + 2. Run `npm run desktop-build-staging` and install the locally-generated desktop app to test. +5. (Google only) apply the following diff: + + ```diff + diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx + index 765fbab038..4318528b4c 100644 + --- a/src/components/DeeplinkWrapper/index.website.tsx + +++ b/src/components/DeeplinkWrapper/index.website.tsx + @@ -63,14 +63,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra + const isUnsupportedDeeplinkRoute = routeRegex.test(window.location.pathname); + + // Making a few checks to exit early before checking authentication status + - if ( + - !isMacOSWeb() || + - isUnsupportedDeeplinkRoute || + - hasShownPrompt || + - CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV || + - autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || + - Session.isAnonymousUser() + - ) { + + if (!isMacOSWeb() || isUnsupportedDeeplinkRoute || hasShownPrompt || autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || Session.isAnonymousUser()) { + return; + } + // We want to show the prompt immediately if the user is already authenticated. + diff --git a/src/libs/Navigation/linkingConfig/prefixes.ts b/src/libs/Navigation/linkingConfig/prefixes.ts + index ca2da6f56b..2c191598f0 100644 + --- a/src/libs/Navigation/linkingConfig/prefixes.ts + +++ b/src/libs/Navigation/linkingConfig/prefixes.ts + @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [ + 'https://www.expensify.cash', + 'https://staging.expensify.cash', + 'https://dev.new.expensify.com', + + 'http://localhost', + CONST.NEW_EXPENSIFY_URL, + CONST.STAGING_NEW_EXPENSIFY_URL, + ]; + ``` + +6. Run `npm run web` + ## Apple #### Port requirements @@ -193,57 +266,11 @@ This is required because the desktop app needs to know the address of the web ap Note that changing this value to a domain that isn't configured for use with Expensify will cause Android to break, as it is still using the real client ID, but now has an incorrect value for `redirectURI`. -#### Set Environment to something other than "Development" - -The `DeepLinkWrapper` component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development". - -Within the `.env` file, set `envName` to something other than "Development", for example: - -``` -envName=Staging -``` - -Alternatively, within the `DeepLinkWrapper/index.website.js` file, you can set the `CONFIG.ENVIRONMENT` to something other than "Development". +## Google -#### Handle deep links in dev on MacOS +Unlike with Apple, to test Google Sign-In we don't need to set up any http/ssh tunnels. We can just use `localhost`. But we need to set up the web and desktop environments to use `localhost` instead of `dev.new.expensify.com` -If developing on MacOS, the development desktop app can't handle deeplinks correctly. To be able to test deeplinking back to the app, follow these steps: - -1. Create a "real" build of the desktop app, which can handle deep links, open the build folder, and install the dmg there: - -```shell -npm run desktop-build -open desktop-build -# Then double-click "NewExpensify.dmg" in Finder window -``` - -2. Even with this build, the deep link may not be handled by the correct app, as the development Electron config seems to intercept it sometimes. To manage this, install [SwiftDefaultApps](https://github.com/Lord-Kamina/SwiftDefaultApps), which adds a preference pane that can be used to configure which app should handle deep links. - -### Test the Apple / Google SSO buttons in development environment - -The Apple/Google Sign In button renders differently in development mode. To prevent confusion -for developers about a possible regression, we decided to not render third party buttons in -development mode. - -Here's how you can re-enable the SSO buttons in development mode: - -- Remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components - ```diff - diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx - index 4286a26033..850f8944ca 100644 - --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx - +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx - @@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false - // for developers about possible regressions, we won't render buttons in development mode. - // For more information about these differences and how to test in development mode, - // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md` - - CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && ( - + ( - - { + @@ -246,7 +246,7 @@ const mainWindow = (): Promise => { + let deeplinkUrl: string; + let browserWindow: BrowserWindow; + + - const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); + + const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`http://localhost:${port}`) : serve({directory: `${__dirname}/www`}); + + // Prod and staging set the icon in the electron-builder config, so only update it here for dev + if (__DEV__) { + ``` -The DeepLinkWrapper component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development". 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/Hidden/Instructions b/docs/Hidden/Instructions new file mode 100644 index 000000000000..940c7ab60d10 --- /dev/null +++ b/docs/Hidden/Instructions @@ -0,0 +1 @@ +This folder is used to house articles that should not be live articles on the helpsite. diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index ae19775d75df..eb59388159bf 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -941,8 +941,8 @@ button { } #platform-tabs > .active { - color: var(--color-button-text); - background-color: var(--color-button-success-background); + color: var(--color-text); + background-color: var(--color-button-background); } .hidden { 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/chat/Expensify-Chat-For-Admins.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md deleted file mode 100644 index 5128484adc9d..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Expensify Chat for Admins -description: Best Practices for Admins settings up Expensify Chat ---- - -# Overview -Expensify Chat is an incredible way to build a community and foster long-term relationships between event producers and attendees, or attendees with each other. Admins are a huge factor in the success of the connections built in Expensify Chat during the events, as they are generally the drivers of the conference schedule, and help ensure safety and respect is upheld by all attendees both on and offline. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your session attendees: -- [How to get set up and start using Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-use-chat-in-expensify) -- [How to format text in Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text) -- [How to flag content and/or users for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) - -# Admin Best Practices -In order to get the most out of Expensify Chat, we created a list of best practices for admins to review in order to use the tool to its fullest capabilities. - -**During the conference:** -- At a minimum, send 3 announcements throughout the day to create awareness of any sessions, activations, contests, or parties you want to promote. -- Communicate with the Expensify Team in the #admins room if you see anything you have questions about or are unsure of to make sure we’re resolving issues together ASAP. -- As an admin, It’s up to you to help keep your conference community safe and respectful. [Flag any content for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) that does not fit your culture and values to keep chatrooms a positive experience for everyone involved. - -**After the conference:** -- The rooms will all stay open after the conference ends, so encourage speakers to keep engaging as long as the conversation is going in their session room. -- Continue sharing photos and videos from the event or anything fun in #social as part of a wrap up for everyone. -- Use the #announce room to give attendees a sneak preview of your next event. -- \ No newline at end of file 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/Dispute-Expensify-Card-transaction.md b/docs/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction.md new file mode 100644 index 000000000000..5bd23cd53730 --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction.md @@ -0,0 +1,73 @@ +--- +title: Dispute Expensify Card transaction +description: Dispute an unrecognized, unauthorized, or fraudulent charge +--- +
+ +When using your Expensify Visa® Commercial Card, you may come across transactions that you want to dispute, including: + +- Unrecognized, unauthorized, or fraudulent charges + - Charges made with your card after it was lost or stolen + - Unauthorized charges while your card is still in your possession + - Continued charges for a canceled recurring subscription +- Service disputes + - Damaged or defective merchandise + - Charges for merchandise that was never received + - Duplicate charges for a single transaction + - Transactions of an incorrect amount + - Refund not received after a return + +# Dispute a transaction + +If you spot a transaction error on your Expensify Card, + +1. Contact the merchant. They can often address the issue promptly. +2. If you are unable to resolve the issue with the merchant, contact us immediately by opening your chat with Expensify Concierge in your Expensify Chat inbox, or by emailing concierge@expensify.com to start the dispute process. Provide the following information: + - Details about the disputed charge, including why you’re disputing it, what occurred, and any steps you’ve taken to address the issue + - Supporting documentation like receipts or cancellation confirmations +3. If you suspect fraud on your Expensify Card, immediately deactivate your card: + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +
    +
  1. Click your profile image or icon in the bottom left menu.
  2. +
  3. Click Wallet in the left menu.
  4. +
  5. Click your Expensify Card.
  6. +
  7. Click Report card fraud.
  8. +
  9. Follow the prompts to deactivate your card and request a new one.
  10. +
+{% include end-option.html %} + +{% include option.html value="mobile" %} +
    +
  1. Tap your profile image or icon in the bottom menu.
  2. +
  3. Tap Wallet.
  4. +
  5. Tap your Expensify Card.
  6. +
  7. Tap Report card fraud.
  8. +
  9. Follow the prompts to deactivate your card and request a new one.
  10. +
+{% include end-option.html %} + +{% include end-selector.html %} + +{:start="4"} +4. [Enable Two-Factor Authentication (2FA)](https://help.expensify.com/articles/new-expensify/settings/Enable-Two-Factor-Authentication) to add an additional layer of security to your account. + +{% include faq-begin.md %} + +**How am I protected from fraud using the Expensify Card?** + +Expensify leverages sophisticated algorithms to detect and/or block unusual card activity. You can also enable real-time notifications to receive alerts each time your card is charged. + +**How long does the dispute process take?** + +The dispute process can take up to 90 days. + +**Can I cancel a dispute?** + +You can cancel a filed dispute by using your Expensify Chat thread with Concierge or by emailing concierge@expensify.com. + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expensify-card/Update-your-Expensify-Card-mailing-address.md b/docs/articles/new-expensify/expensify-card/Update-your-Expensify-Card-mailing-address.md new file mode 100644 index 000000000000..6ce53b6a359a --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Update-your-Expensify-Card-mailing-address.md @@ -0,0 +1,29 @@ +--- +title: Update your Expensify Card mailing address +description: Change your mailing address for your Expensify Card +--- +
+ +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Click **Request a New Card** on your physical card pending activation. +4. Select **I lost my card**. + +{% include info.html %} +If you’re updating your address to receive your new Expensify Visa® Commercial Card, you’ll still select **I lost my card** even though you have not lost a card. +{% include end-info.html %} + +{:start="5"} +5. Confirm your details and click **Continue**. +6. Update your address and click **Continue**. + +{% include info.html %} +If you’re updating your address to receive your new Expensify Visa® Commercial Card, you can click the X in the right corner to end the process here if the new card has not been shipped out to you yet. However, if the new card has already been shipped out to an incorrect address, proceed to the next step to resend the card to the newly updated address. +{% include end-info.html %} + +{:start="7"} +7. Proceed with the card replacement. + +Your new card will arrive in 2-3 business days. + +
diff --git a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md index 24f178db9f12..56e456eb1256 100644 --- a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md +++ b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md @@ -31,7 +31,7 @@ Before completing this process, you’ll want to: New cards will have the same limit as the existing cards. Each cardholder’s current physical and virtual cards will remain active until a Domain Admin or the cardholder deactivates it. {% include info.html %} -Cards won’t be issued to any employees who don’t currently have them. In this case, you’ll need to issue a new card. +Cards won’t be issued to any employees who don’t currently have them. In this case, you’ll need to [issue a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa%C2%AE-Commercial-Card-for-your-Company) {% include end-info.html %} {% include faq-begin.md %} 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/assets/images/AdminissuedVirtualCards.png b/docs/assets/images/AdminissuedVirtualCards.png index 88df9b2f3fec..9c44763f5840 100644 Binary files a/docs/assets/images/AdminissuedVirtualCards.png and b/docs/assets/images/AdminissuedVirtualCards.png differ diff --git a/docs/assets/images/domains.svg b/docs/assets/images/domains.svg index 3a3c95604b79..12c0a0a0792b 100644 --- a/docs/assets/images/domains.svg +++ b/docs/assets/images/domains.svg @@ -1,44 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/assets/images/plane.svg b/docs/assets/images/plane.svg index 0295aa3c66c0..bd7fceba3607 100644 --- a/docs/assets/images/plane.svg +++ b/docs/assets/images/plane.svg @@ -1,34 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js index 6b3390148ff0..4d819804ed44 100644 --- a/docs/assets/js/main.js +++ b/docs/assets/js/main.js @@ -196,6 +196,30 @@ const tocbotOptions = { scrollContainer: 'content-area', }; +function selectNewExpensify(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent) { + newExpensifyTab.classList.add('active'); + newExpensifyContent.classList.remove('hidden'); + + expensifyClassicTab.classList.remove('active'); + expensifyClassicContent.classList.add('hidden'); + window.tocbot.refresh({ + ...tocbotOptions, + contentSelector: '#new-expensify', + }); +} + +function selectExpensifyClassic(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent) { + expensifyClassicTab.classList.add('active'); + expensifyClassicContent.classList.remove('hidden'); + + newExpensifyTab.classList.remove('active'); + newExpensifyContent.classList.add('hidden'); + window.tocbot.refresh({ + ...tocbotOptions, + contentSelector: '#expensify-classic', + }); +} + window.addEventListener('DOMContentLoaded', () => { injectFooterCopywrite(); @@ -219,8 +243,10 @@ window.addEventListener('DOMContentLoaded', () => { let contentSelector = '.article-toc-content'; if (expensifyClassicContent) { contentSelector = '#expensify-classic'; + selectExpensifyClassic(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); } else if (newExpensifyContent) { contentSelector = '#new-expensify'; + selectNewExpensify(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); } if (window.tocbot) { @@ -232,28 +258,12 @@ window.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line es/no-optional-chaining expensifyClassicTab?.addEventListener('click', () => { - expensifyClassicTab.classList.add('active'); - expensifyClassicContent.classList.remove('hidden'); - - newExpensifyTab.classList.remove('active'); - newExpensifyContent.classList.add('hidden'); - window.tocbot.refresh({ - ...tocbotOptions, - contentSelector: '#expensify-classic', - }); + selectExpensifyClassic(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); }); // eslint-disable-next-line es/no-optional-chaining newExpensifyTab?.addEventListener('click', () => { - newExpensifyTab.classList.add('active'); - newExpensifyContent.classList.remove('hidden'); - - expensifyClassicTab.classList.remove('active'); - expensifyClassicContent.classList.add('hidden'); - window.tocbot.refresh({ - ...tocbotOptions, - contentSelector: '#new-expensify', - }); + selectNewExpensify(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); }); document.getElementById('header-button').addEventListener('click', toggleHeaderMenu); diff --git a/docs/redirects.csv b/docs/redirects.csv index 74fa9c697f5f..3042dc79085c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -26,10 +26,31 @@ https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vac https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/expenses/Distance-Tracking https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings -https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://community.expensify.com/discussion/3498/how-do-i-invite-users-in-my-company,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/6015/tutorial,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/2596/setting-up-accounts,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/7665/how-do-i-add-another-person-to-my-account-to-keep-track-of-there-expenses,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/7456/how-do-i-submit-an-expense-for-reimbursement,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/1460/schedule-a-demo,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/835/what-is-the-difference-between-a-category-and-a-tag,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/7703/getting-started-video,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/1845/how-to-set-up-account-and-add-users,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/8629/employee-training-e-learning-program,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/1607/on-demand-webinars,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5444/admin-onboarding-webinar-faqs,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5417/employee-training-webinar-faqs,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5885/overview-the-employee-training-webinar,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5854/overview-the-expensify-admin-onboarding-webinar,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/4699/how-to-download-the-mobile-app,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/4524/how-to-set-up-the-uber-integration,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5212/how-to-connect-your-policy-to-netsuite-token-based-authentication,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Add-profile-photo +https://community.expensify.com/discussion/5922/deep-dive-day-1-with-expensify-for-submitters,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5934/day-1-with-expensify-admins-and-accountants,https://help.expensify.com/expensify-classic/hubs/getting-started +https://community.expensify.com/discussion/5694/deep-dive-admin-training-and-setup-resources,https://help.expensify.com/expensify-classic/hubs/getting-started https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Perks.html,https://use.expensify.com/company-credit-card https://help.expensify.com/articles/expensify-classic/expensify-partner-program/How-to-Join-the-ExpensifyApproved!-Partner-Program.html,https://use.expensify.com/accountants-program https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners, https://use.expensify.com/blog/maximizing-rewards-expensifyapproved-accounting-partners-now-earn-0-5-revenue-share @@ -172,4 +193,9 @@ 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/chat/Expensify-Chat-For-Admins,https://help.expensify.com/new-expensify/hubs/chat/ +https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account 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..6ee40003244f 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.74 + 1.4.78 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.74.4 + 1.4.78.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 627c6dbf826c..e4e80edd56bc 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.74 + 1.4.78 CFBundleSignature ???? CFBundleVersion - 1.4.74.4 + 1.4.78.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 477f83aa6d00..d13d39a4dcc5 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.74 + 1.4.78 CFBundleVersion - 1.4.74.4 + 1.4.78.4 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..8ceeacdbc086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.74-4", + "version": "1.4.78-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.74-4", + "version": "1.4.78-4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -20,6 +20,9 @@ "@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", @@ -70,7 +73,6 @@ "mapbox-gl": "^2.15.0", "onfido-sdk-ui": "14.15.0", "process": "^0.11.10", - "prop-types": "^15.7.2", "pusher-js": "8.3.0", "react": "18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -109,7 +111,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", @@ -121,11 +123,11 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", "react-native-vision-camera": "^4.0.0-beta.13", - "react-native-web": "^0.19.9", + "react-native-web": "^0.19.12", "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,11 +203,11 @@ "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", - "eslint-config-expensify": "^2.0.47", + "eslint-config-expensify": "^2.0.49", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -1852,19 +1854,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-assign": { - "version": "7.18.6", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", @@ -5589,6 +5578,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 +8889,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 +18603,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": { @@ -19330,9 +19365,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.48.tgz", - "integrity": "sha512-PFegJ9Wfsiu5tgevhjA1toCxsZ8Etfk6pIjtXAnwpmVj7q4CtB3QDRusJoUDyJ3HrZr8AsFKViz7CU/CBTfwOw==", + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.49.tgz", + "integrity": "sha512-3yGQuOsjvtWh/jYSJKIJgmwULhrVMCiYkWGzLOKpm/wCzdiP4l0T/gJMWOkvGhTtyqxsP7ZUTwPODgcE3extxA==", "dev": true, "dependencies": { "@lwc/eslint-plugin-lwc": "^1.7.2", @@ -20301,8 +20336,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", @@ -31554,10 +31589,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" @@ -31742,11 +31782,12 @@ } }, "node_modules/react-native-web": { - "version": "0.19.9", - "license": "MIT", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.12.tgz", + "integrity": "sha512-o2T0oztoVDQjztt4YksO9S1XRjoH/AqcSvifgWLrPJgGVbMWsfhILgl6lfUdEamVZzZSVV/2gqDVMAk/qq7mZw==", "dependencies": { "@babel/runtime": "^7.18.6", - "@react-native/normalize-color": "^2.1.0", + "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^6.0.1", "memoize-one": "^6.0.0", @@ -31776,6 +31817,11 @@ "react-native-web": "*" } }, + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { + "version": "0.74.81", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.81.tgz", + "integrity": "sha512-g3YvkLO7UsSWiDfYAU+gLhRHtEpUyz732lZB+N8IlLXc5MnfXHC8GKneDGY3Mh52I3gBrs20o37D5viQX9E1CA==" + }, "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", "license": "MIT" @@ -31959,9 +32005,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", diff --git a/package.json b/package.json index 1a449f42bece..a8e16bb7baa6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.74-4", + "version": "1.4.78-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -72,6 +72,9 @@ "@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", @@ -122,7 +125,6 @@ "mapbox-gl": "^2.15.0", "onfido-sdk-ui": "14.15.0", "process": "^0.11.10", - "prop-types": "^15.7.2", "pusher-js": "8.3.0", "react": "18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -161,7 +163,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", @@ -173,11 +175,11 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", "react-native-vision-camera": "^4.0.0-beta.13", - "react-native-web": "^0.19.9", + "react-native-web": "^0.19.12", "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,11 +255,11 @@ "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", - "eslint-config-expensify": "^2.0.47", + "eslint-config-expensify": "^2.0.49", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", diff --git a/patches/@react-navigation+native+6.1.12.patch b/patches/@react-navigation+native+6.1.12.patch index d451d89d687c..d53f8677d225 100644 --- a/patches/@react-navigation+native+6.1.12.patch +++ b/patches/@react-navigation+native+6.1.12.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js b/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js -index 16fdbef..bc2c96a 100644 +index 16fdbef..e660dd6 100644 --- a/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js +++ b/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js @@ -1,8 +1,23 @@ @@ -63,6 +63,15 @@ index 16fdbef..bc2c96a 100644 replace(_ref3) { var _window$history$state2; let { +@@ -80,7 +101,7 @@ export default function createMemoryHistory() { + + // Need to keep the hash part of the path if there was no previous history entry + // or the previous history entry had the same path +- let pathWithHash = path; ++ let pathWithHash = path.replace(/(\/{2,})|(\/$)/g, (match, p1) => (p1 ? '/' : '')); + if (!items.length || items.findIndex(item => item.id === id) < 0) { + // There are two scenarios for creating an array with only one history record: + // - When loaded id not found in the items array, this function by default will replace @@ -108,7 +129,9 @@ export default function createMemoryHistory() { window.history.replaceState({ id 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/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.12+001+initial.patch similarity index 98% rename from patches/react-native-web+0.19.9+001+initial.patch rename to patches/react-native-web+0.19.12+001+initial.patch index 91ba6bfd59c0..c77cfc7829ed 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.12+001+initial.patch @@ -1,9 +1,9 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..0c9dfcb 100644 +index e137def..c3e5054 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js @@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. constructor(_props) { - var _this$props$updateCel; @@ -243,7 +243,7 @@ index c879838..0c9dfcb 100644 }); this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1317,8 +1370,12 @@ class VirtualizedList extends StateSafePureComponent { onStartReached = _this$props8.onStartReached, onStartReachedThreshold = _this$props8.onStartReachedThreshold, onEndReached = _this$props8.onEndReached, @@ -258,7 +258,7 @@ index c879838..0c9dfcb 100644 var _this$_scrollMetrics2 = this._scrollMetrics, contentLength = _this$_scrollMetrics2.contentLength, visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1358,16 +1415,10 @@ class VirtualizedList extends StateSafePureComponent { // and call onStartReached only once for a given content length, // and only if onEndReached is not being executed else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { @@ -279,9 +279,9 @@ index c879838..0c9dfcb 100644 } // If the user scrolls away from the start or end and back again, -@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { - } - } +@@ -1433,6 +1484,11 @@ class VirtualizedList extends StateSafePureComponent { + */ + _updateViewableItems(props, cellsAroundViewport) { + // If we have any pending scroll updates it means that the scroll metrics + // are out of date and we should not call any of the visibility callbacks. @@ -292,7 +292,7 @@ index c879838..0c9dfcb 100644 tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); }); diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..43f9653 100644 +index c7d68bb..459f017 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js @@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.12+002+fixLastSpacer.patch similarity index 100% rename from patches/react-native-web+0.19.9+004+fixLastSpacer.patch rename to patches/react-native-web+0.19.12+002+fixLastSpacer.patch diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.12+003+image-header-support.patch similarity index 95% rename from patches/react-native-web+0.19.9+005+image-header-support.patch rename to patches/react-native-web+0.19.12+003+image-header-support.patch index 4652e22662f0..6652f0345cc4 100644 --- a/patches/react-native-web+0.19.9+005+image-header-support.patch +++ b/patches/react-native-web+0.19.12+003+image-header-support.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 95355d5..19109fc 100644 +index 9649d27..3281cc8 100644 --- a/node_modules/react-native-web/dist/exports/Image/index.js +++ b/node_modules/react-native-web/dist/exports/Image/index.js @@ -135,7 +135,22 @@ function resolveAssetUri(source) { @@ -23,10 +23,10 @@ index 95355d5..19109fc 100644 + return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers); +} +var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => { - var ariaLabel = props['aria-label'], + var _ariaLabel = props['aria-label'], + accessibilityLabel = props.accessibilityLabel, blurRadius = props.blurRadius, - defaultSource = props.defaultSource, -@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -238,16 +253,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { } }, function error() { updateState(ERRORED); @@ -47,7 +47,7 @@ index 95355d5..19109fc 100644 }); } function abortPendingRequest() { -@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -279,10 +288,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { suppressHydrationWarning: true }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); }); @@ -129,7 +129,7 @@ index 95355d5..19109fc 100644 ImageLoader.getSize(uri, success, failure); }; diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -index bc06a87..e309394 100644 +index bc06a87..5a22819 100644 --- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js +++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js @@ -76,7 +76,7 @@ var ImageLoader = { diff --git a/patches/react-native-web+0.19.9+006+fixPointerEventDown.patch b/patches/react-native-web+0.19.12+004+fixPointerEventDown.patch similarity index 100% rename from patches/react-native-web+0.19.9+006+fixPointerEventDown.patch rename to patches/react-native-web+0.19.12+004+fixPointerEventDown.patch diff --git a/patches/react-native-web+0.19.9+007+osr-improvement.patch b/patches/react-native-web+0.19.12+005+osr-improvement.patch similarity index 100% rename from patches/react-native-web+0.19.9+007+osr-improvement.patch rename to patches/react-native-web+0.19.12+005+osr-improvement.patch diff --git a/patches/react-native-web+0.19.9+002+measureInWindow.patch b/patches/react-native-web+0.19.9+002+measureInWindow.patch deleted file mode 100644 index f41b4b3b48cb..000000000000 --- a/patches/react-native-web+0.19.9+002+measureInWindow.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/UIManager/index.js b/node_modules/react-native-web/dist/exports/UIManager/index.js -index 15b71d5..46b9e01 100644 ---- a/node_modules/react-native-web/dist/exports/UIManager/index.js -+++ b/node_modules/react-native-web/dist/exports/UIManager/index.js -@@ -77,7 +77,7 @@ var UIManager = { - measureInWindow(node, callback) { - if (node) { - setTimeout(() => { -- var _getRect2 = getRect(node), -+ var _getRect2 = node.getBoundingClientRect(), - height = _getRect2.height, - left = _getRect2.left, - top = _getRect2.top, diff --git a/src/CONST.ts b/src/CONST.ts index 2eca497498c1..32c500962d8a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,14 +73,18 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { - MERGED_ACCOUNT_PREFIX: 'MERGED_', 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 +95,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: { @@ -146,6 +151,7 @@ const CONST = { DISPLAY_NAME: { MAX_LENGTH: 50, RESERVED_NAMES: ['Expensify', 'Concierge'], + EXPENSIFY_CONCIERGE: 'Expensify Concierge', }, GPS: { @@ -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, - }, }, }, @@ -668,9 +662,9 @@ const CONST = { DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action DISMISSED_VIOLATION: 'DISMISSEDVIOLATION', DONATION: 'DONATION', // OldDot Action - EXPORTED_TO_CSV: 'EXPORTEDTOCSV', // OldDot Action - EXPORTED_TO_INTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action - EXPORTED_TO_QUICK_BOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action + EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action + EXPORTED_TO_INTEGRATION: 'EXPORTINTEGRATION', // OldDot Action + EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // OldDot Action FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', HOLD_COMMENT: 'HOLDCOMMENT', @@ -692,6 +686,7 @@ const CONST = { REIMBURSEMENT_DEQUEUED: 'REIMBURSEMENTDEQUEUED', REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // OldDot Action + REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // OldDot Action RENAMED: 'RENAMED', REPORT_PREVIEW: 'REPORTPREVIEW', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action @@ -935,8 +930,10 @@ const CONST = { RESIZE_DEBOUNCE_TIME: 100, }, SEARCH_TABLE_COLUMNS: { + RECEIPT: 'receipt', DATE: 'date', MERCHANT: 'merchant', + DESCRIPTION: 'description', FROM: 'from', TO: 'to', CATEGORY: 'category', @@ -966,7 +963,7 @@ const CONST = { DARK_CONTENT: 'dark-content', }, TRANSACTION: { - DEFAULT_MERCHANT: 'Request', + DEFAULT_MERCHANT: 'Expense', UNKNOWN_MERCHANT: 'Unknown Merchant', PARTIAL_TRANSACTION_MERCHANT: '(none)', TYPE: { @@ -1190,6 +1187,10 @@ const CONST = { WEBP: 'image/webp', JPEG: 'image/jpeg', }, + ATTACHMENT_TYPE: { + REPORT: 'r', + NOTE: 'n', + }, IMAGE_OBJECT_POSITION: { TOP: 'top', @@ -1271,6 +1272,8 @@ const CONST = { EXPENSIFY_EMAIL_DOMAIN: '@expensify.com', }, + CONCIERGE_DISPLAY_NAME: 'Concierge', + INTEGRATION_ENTITY_MAP_TYPES: { DEFAULT: 'DEFAULT', NONE: 'NONE', @@ -1307,12 +1310,13 @@ const CONST = { SYNC: 'sync', ENABLE_NEW_CATEGORIES: 'enableNewCategories', EXPORT: 'export', + TENANT_ID: 'tenantID', IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', INVOICE_STATUS: { - AWAITING_PAYMENT: 'AWT_PAYMENT', DRAFT: 'DRAFT', AWAITING_APPROVAL: 'AWT_APPROVAL', + AWAITING_PAYMENT: 'AWT_PAYMENT', }, IMPORT_TRACKING_CATEGORIES: 'importTrackingCategories', MAPPINGS: 'mappings', @@ -1597,6 +1601,9 @@ const CONST = { ACCOUNTANT: 'accountant', }, }, + ACCESS_VARIANTS: { + CREATE: 'create', + }, }, GROWL: { @@ -1777,7 +1784,8 @@ const CONST = { XERO: 'xero', }, SYNC_STAGE_NAME: { - STARTING_IMPORT: 'startingImport', + STARTING_IMPORT_QBO: 'startingImportQBO', + STARTING_IMPORT_XERO: 'startingImportXero', QBO_IMPORT_MAIN: 'quickbooksOnlineImportMain', QBO_IMPORT_CUSTOMERS: 'quickbooksOnlineImportCustomers', QBO_IMPORT_EMPLOYEES: 'quickbooksOnlineImportEmployees', @@ -1929,10 +1937,10 @@ const CONST = { // Extract attachment's source from the data's html string ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g, - EMOJI_NAME: /:[\w+-]+:/g, - EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, + EMOJI_NAME: /:[\p{L}0-9_+-]+:/gu, + EMOJI_SUGGESTIONS: /:[\p{L}0-9_+-]{1,40}$/u, 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: /^:[^:]+$/, @@ -2066,7 +2074,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, @@ -2093,7 +2101,6 @@ const CONST = { INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { - SHARE_CODE: 'shareCode', MEMBERS: 'member', INVITE: 'invite', SETTINGS: 'settings', @@ -3296,6 +3303,7 @@ const CONST = { }, CONCIERGE_TRAVEL_URL: 'https://community.expensify.com/discussion/7066/introducing-concierge-travel', + BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', SCREEN_READER_STATES: { ALL: 'all', ACTIVE: 'active', @@ -3486,6 +3494,9 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', + SHARED: 'shared', + DRAFTS: 'drafts', + FINISHED: 'finished', }, STATUS_TEXT_MAX_LENGTH: 100, @@ -3498,7 +3509,6 @@ const CONST = { NAVIGATION: { TYPE: { - FORCED_UP: 'FORCED_UP', UP: 'UP', }, ACTION_TYPE: { @@ -3519,10 +3529,12 @@ const CONST = { COLON: ':', MAPBOX: { PADDING: 50, - DEFAULT_ZOOM: 10, + DEFAULT_ZOOM: 15, SINGLE_MARKER_ZOOM: 15, DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', + ANIMATION_DURATION_ON_CENTER_ME: 1000, + CENTER_BUTTON_FADE_DURATION: 300, }, ONYX_UPDATE_TYPES: { HTTPS: 'https', @@ -3680,6 +3692,7 @@ const CONST = { TAX_OUT_OF_POLICY: 'taxOutOfPolicy', TAX_RATE_CHANGED: 'taxRateChanged', TAX_REQUIRED: 'taxRequired', + HOLD: 'hold', }, /** Context menu types */ @@ -3742,8 +3755,9 @@ const CONST = { WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, - + ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?', ONBOARDING_CONCIERGE: { [onboardingChoices.EMPLOYER]: '# Expensify is the fastest way to get paid back!\n' + @@ -4761,6 +4775,7 @@ const CONST = { MAX_TAX_RATE_DECIMAL_PLACES: 4, DOWNLOADS_PATH: '/Downloads', + DOWNLOADS_TIMEOUT: 5000, NEW_EXPENSIFY_PATH: '/New Expensify', ENVIRONMENT_SUFFIX: { @@ -4775,15 +4790,22 @@ const CONST = { }, SEARCH_RESULTS_PAGE_SIZE: 50, - SEARCH_BOTTOM_TAB_URL: '/Search_Bottom_Tab', SEARCH_DATA_TYPES: { TRANSACTION: 'transaction', + REPORT: 'report', }, REFERRER: { NOTIFICATION: 'notification', }, + + SORT_ORDER: { + ASC: 'asc', + DESC: 'desc', + }, + + SUBSCRIPTION_SIZE_LIMIT: 20000, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 6205afb9c03c..ddc4b5f88a69 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; @@ -259,6 +269,8 @@ function Expensify({ ); } +Expensify.displayName = 'Expensify'; + export default withOnyx({ isCheckingPublicRoom: { key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index aba56fe380da..f8600ed9c9ee 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', @@ -130,9 +133,6 @@ const ONYXKEYS = { /** This NVP holds to most recent waypoints that a person has used when creating a distance expense */ NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', - /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */ - NVP_HAS_DISMISSED_IDLE_PANEL: 'nvp_hasDismissedIdlePanel', - /** This NVP contains the choice that the user made on the engagement modal */ NVP_INTRO_SELECTED: 'nvp_introSelected', @@ -191,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', @@ -479,6 +482,8 @@ const ONYXKEYS = { WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft', NEW_CHAT_NAME_FORM: 'newChatNameForm', NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', + SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', + SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', }, } as const; @@ -536,6 +541,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; + [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; }; type OnyxFormDraftValuesMapping = { @@ -586,7 +592,10 @@ type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; + + // NVP_ONBOARDING is an array for old users. [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; + [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; @@ -613,17 +622,18 @@ 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; [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; - [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 47e96f0a501a..61034382fefd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; +import type {CentralPaneNavigatorParamList} from './libs/Navigation/types'; +import type {SearchQuery} from './types/onyx/SearchResults'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; // This is a file containing constants for all the routes we want to be able to go to @@ -14,10 +16,20 @@ function getUrlWithBackToParam(url: TUrl, backTo?: string): return `${url}${backToParam}` as const; } -const ROUTES = { +const PUBLIC_SCREENS_ROUTES = { // If the user opens this route, we'll redirect them to the path saved in the last visited path or to the home page if the last visited path is empty. ROOT: '', + TRANSITION_BETWEEN_APPS: 'transition', + CONNECTION_COMPLETE: 'connection-complete', + VALIDATE_LOGIN: 'v/:accountID/:validateCode', + UNLINK_LOGIN: 'u/:accountID/:validateCode', + APPLE_SIGN_IN: 'sign-in-with-apple', + GOOGLE_SIGN_IN: 'sign-in-with-google', + SAML_SIGN_IN: 'sign-in-with-saml', +} as const; +const ROUTES = { + ...PUBLIC_SCREENS_ROUTES, // This route renders the list of reports. HOME: 'home', @@ -25,7 +37,15 @@ const ROUTES = { SEARCH: { route: '/search/:query', - getRoute: (query: string) => `search/${query}` as const, + getRoute: (searchQuery: SearchQuery, queryParams?: CentralPaneNavigatorParamList['Search_Central_Pane']) => { + const {sortBy, sortOrder} = queryParams ?? {}; + + if (!sortBy && !sortOrder) { + return `search/${searchQuery}` as const; + } + + return `search/${searchQuery}?sortBy=${sortBy}&sortOrder=${sortOrder}` as const; + }, }, SEARCH_REPORT: { @@ -53,18 +73,11 @@ const ROUTES = { getRoute: (accountID: string | number) => `a/${accountID}/avatar` as const, }, - TRANSITION_BETWEEN_APPS: 'transition', - VALIDATE_LOGIN: 'v/:accountID/:validateCode', - CONNECTION_COMPLETE: 'connection-complete', GET_ASSISTANCE: { route: 'get-assistance/:taskID', getRoute: (taskID: string, backTo: string) => getUrlWithBackToParam(`get-assistance/${taskID}`, backTo), }, - UNLINK_LOGIN: 'u/:accountID/:validateCode', - APPLE_SIGN_IN: 'sign-in-with-apple', - GOOGLE_SIGN_IN: 'sign-in-with-google', DESKTOP_SIGN_IN_REDIRECT: 'desktop-signin-redirect', - SAML_SIGN_IN: 'sign-in-with-saml', // This is a special validation URL that will take the user to /workspace/new after validation. This is used // when linking users from e.com in order to share a session in this app. @@ -89,6 +102,7 @@ const ROUTES = { SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_SUBSCRIPTION: 'settings/subscription', + SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', SETTINGS_THEME: 'settings/preferences/theme', @@ -183,7 +197,10 @@ const ROUTES = { SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', SETTINGS_TROUBLESHOOT: 'settings/troubleshoot', - SETTINGS_CONSOLE: 'settings/troubleshoot/console', + SETTINGS_CONSOLE: { + route: 'settings/troubleshoot/console', + getRoute: (backTo?: string) => getUrlWithBackToParam(`settings/troubleshoot/console`, backTo), + }, SETTINGS_SHARE_LOG: { route: 'settings/troubleshoot/console/share-log', getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const, @@ -235,9 +252,10 @@ const ROUTES = { route: 'r/:reportID/details/shareCode', getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const, }, - REPORT_ATTACHMENTS: { - route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const, + ATTACHMENTS: { + route: 'attachment', + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number) => + `attachment?source=${encodeURIComponent(url)}&type=${type}${reportID ? `&reportID=${reportID}` : ''}${accountID ? `&accountID=${accountID}` : ''}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -263,13 +281,9 @@ const ROUTES = { route: 'r/:reportID/settings', getRoute: (reportID: string) => `r/${reportID}/settings` as const, }, - REPORT_SETTINGS_ROOM_NAME: { - route: 'r/:reportID/settings/room-name', - getRoute: (reportID: string) => `r/${reportID}/settings/room-name` as const, - }, - REPORT_SETTINGS_GROUP_NAME: { - route: 'r/:reportID/settings/group-name', - getRoute: (reportID: string) => `r/${reportID}/settings/group-name` as const, + REPORT_SETTINGS_NAME: { + route: 'r/:reportID/settings/name', + getRoute: (reportID: string) => `r/${reportID}/settings/name` as const, }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', @@ -354,6 +368,27 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, + SETTINGS_CATEGORIES_ROOT: { + route: 'settings/:policyID/categories', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories`, backTo), + }, + SETTINGS_CATEGORY_SETTINGS: { + route: 'settings/:policyID/categories/:categoryName', + getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/${encodeURIComponent(categoryName)}`, backTo), + }, + SETTINGS_CATEGORIES_SETTINGS: { + route: 'settings/:policyID/categories/settings', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/settings`, backTo), + }, + SETTINGS_CATEGORY_CREATE: { + route: 'settings/:policyID/categories/new', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/new`, backTo), + }, + SETTINGS_CATEGORY_EDIT: { + route: 'settings/:policyID/categories/:categoryName/edit', + getRoute: (policyID: string, categoryName: string, backTo = '') => + getUrlWithBackToParam(`settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit`, backTo), + }, MONEY_REQUEST_STEP_CURRENCY: { route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '') => @@ -644,10 +679,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, }, - WORKSPACE_MORE_FEATURES: { - route: 'settings/workspaces/:policyID/more-features', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, - }, WORKSPACE_CATEGORY_CREATE: { route: 'settings/workspaces/:policyID/categories/new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const, @@ -656,6 +687,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName/edit', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const, }, + WORKSPACE_MORE_FEATURES: { + route: 'settings/workspaces/:policyID/more-features', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, + }, WORKSPACE_TAGS: { route: 'settings/workspaces/:policyID/tags', getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const, @@ -673,12 +708,12 @@ const ROUTES = { getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const, }, WORKSPACE_TAG_EDIT: { - route: 'settings/workspace/:policyID/tag/:tagName/edit', - getRoute: (policyID: string, tagName: string) => `settings/workspace/${policyID}/tag/${encodeURIComponent(tagName)}/edit` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/edit', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/edit` as const, }, WORKSPACE_TAG_SETTINGS: { - route: 'settings/workspaces/:policyID/tag/:tagName', - getRoute: (policyID: string, tagName: string) => `settings/workspaces/${policyID}/tag/${encodeURIComponent(tagName)}` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', @@ -796,17 +831,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories` as const, }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/cost-centers', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/cost-centers` as const, - }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/region` as const, + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP: { + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/mapping/:categoryId/:categoryName', + getRoute: (policyID: string, categoryId: string, categoryName: string) => + `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/mapping/${categoryId}/${encodeURIComponent(categoryName)}` as const, }, POLICY_ACCOUNTING_XERO_CUSTOMER: { - route: '/settings/workspaces/:policyID/accounting/xero/import/customers', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/accounting/xero/import/customers` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/customers', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/customers` as const, }, POLICY_ACCOUNTING_XERO_TAXES: { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', @@ -817,8 +849,8 @@ const ROUTES = { getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, }, POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT: { - route: '/settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, + route: 'settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, }, POLICY_ACCOUNTING_XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: { route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-date-select', @@ -832,6 +864,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/advanced', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, }, + POLICY_ACCOUNTING_XERO_BILL_STATUS_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-status-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export/purchase-bill-status-selector` as const, + }, POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR: { route: 'settings/workspaces/:policyID/accounting/xero/advanced/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/invoice-account-selector` as const, @@ -876,7 +912,7 @@ const HYBRID_APP_ROUTES = { MONEY_REQUEST_SUBMIT_CREATE: '/submit/new/scan', } as const; -export {HYBRID_APP_ROUTES, getUrlWithBackToParam}; +export {HYBRID_APP_ROUTES, getUrlWithBackToParam, PUBLIC_SCREENS_ROUTES}; export default ROUTES; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4e7243d0eb2c..6f32f980d6c2 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -7,7 +7,7 @@ import type DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', CONCIERGE: 'Concierge', - REPORT_ATTACHMENTS: 'ReportAttachments', + ATTACHMENTS: 'Attachments', } as const; const SCREENS = { @@ -106,6 +106,7 @@ const SCREENS = { SUBSCRIPTION: { ROOT: 'Settings_Subscription', + SIZE: 'Settings_Subscription_Size', }, }, SAVE_THE_WORLD: { @@ -142,6 +143,7 @@ const SCREENS = { PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', TRAVEL: 'Travel', SEARCH_REPORT: 'SearchReport', + SETTINGS_CATEGORIES: 'SettingsCategories', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -186,10 +188,17 @@ const SCREENS = { ENABLE_PAYMENTS: 'IOU_Send_Enable_Payments', }, + SETTINGS_CATEGORIES: { + SETTINGS_CATEGORY_SETTINGS: 'Settings_Category_Settings', + SETTINGS_CATEGORIES_SETTINGS: 'Settings_Categories_Settings', + SETTINGS_CATEGORY_CREATE: 'Settings_Category_Create', + SETTINGS_CATEGORY_EDIT: 'Settings_Category_Edit', + SETTINGS_CATEGORIES_ROOT: 'Settings_Categories', + }, + REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', - ROOM_NAME: 'Report_Settings_Room_Name', - GROUP_NAME: 'Report_Settings_Group_Name', + NAME: 'Report_Settings_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', VISIBILITY: 'Report_Settings_Visibility', @@ -248,11 +257,11 @@ const SCREENS = { XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', XERO_TRACKING_CATEGORIES: 'Policy_Accounting_Xero_Tracking_Categories', - XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', - XERO_MAP_REGION: 'Policy_Accounting_Xero_Map_Region', + XERO_MAP_TRACKING_CATEGORY: 'Policy_Accounting_Xero_Map_Tracking_Category', XERO_EXPORT: 'Policy_Accounting_Xero_Export', XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: 'Policy_Accounting_Xero_Export_Purchase_Bill_Date_Select', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', + XERO_BILL_STATUS_SELECTOR: 'Policy_Accounting_Xero_Export_Bill_Status_Selector', XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector', XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 31ccfc954513..17a2f6212447 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -263,7 +263,7 @@ function AddressSearch( } setIsFetchingCurrentLocation(false); - setLocationErrorCode(errorData.code); + setLocationErrorCode(errorData?.code ?? null); }, { maximumAge: 0, // No cache, always get fresh location info 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/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 99a0ee3bf683..2212e7460a2a 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -6,9 +6,9 @@ import {StyleSheet} from 'react-native'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -30,7 +30,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', [], ); - const {isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const linkProps: LinkProps = {}; if (onPress) { @@ -38,7 +38,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', } else { linkProps.href = href; } - const defaultTextStyle = DeviceCapabilities.canUseTouchScreen() || isSmallScreenWidth ? {} : {...styles.userSelectText, ...styles.cursorPointer}; + const defaultTextStyle = DeviceCapabilities.canUseTouchScreen() || shouldUseNarrowLayout ? {} : {...styles.userSelectText, ...styles.cursorPointer}; const isEmail = Str.isValidEmail(href.replace(/mailto:/i, '')); return ( diff --git a/src/components/AttachmentContext.ts b/src/components/AttachmentContext.ts new file mode 100644 index 000000000000..4ed6bdc9084f --- /dev/null +++ b/src/components/AttachmentContext.ts @@ -0,0 +1,22 @@ +import {createContext} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type AttachmentContextProps = { + type?: ValueOf; + reportID?: string; + accountID?: number; +}; + +const AttachmentContext = createContext({ + type: undefined, + reportID: undefined, + accountID: undefined, +}); + +AttachmentContext.displayName = 'AttachmentContext'; + +export { + // eslint-disable-next-line import/prefer-default-export + AttachmentContext, +}; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index fb6a8e911e87..d1c027378563 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -5,8 +5,10 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {useSharedValue} from 'react-native-reanimated'; +import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -100,6 +102,12 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { /** The report that has this attachment */ report?: OnyxEntry | EmptyObject; + /** The type of the attachment */ + type?: ValueOf; + + /** If the attachment originates from a note, the accountID will represent the author of that note. */ + accountID?: number; + /** Optional callback to fire when we want to do something after modal show. */ onModalShow?: () => void; @@ -155,6 +163,8 @@ function AttachmentModal({ onModalClose = () => {}, isLoading = false, shouldShowNotFoundPage = false, + type = undefined, + accountID = undefined, }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -170,8 +180,9 @@ function AttachmentModal({ const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); + const {windowWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const nope = useSharedValue(false); - const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); @@ -451,7 +462,7 @@ function AttachmentModal({ let shouldShowThreeDotsButton = false; if (!isEmptyObject(report)) { headerTitleNew = translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !isReceiptAttachment && !isOffline; + shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isReceiptAttachment && !isOffline; shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen && threeDotsMenuItems.length !== 0; } const context = useMemo( @@ -486,14 +497,14 @@ function AttachmentModal({ propagateSwipe > - {isSmallScreenWidth && } + {shouldUseNarrowLayout && } downloadAttachment()} - shouldShowCloseButton={!isSmallScreenWidth} - shouldShowBackButton={isSmallScreenWidth} + shouldShowCloseButton={!shouldUseNarrowLayout} + shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={closeModal} onCloseButtonPress={closeModal} shouldShowThreeDotsButton={shouldShowThreeDotsButton} @@ -515,35 +526,37 @@ function AttachmentModal({ onLinkPress={() => Navigation.dismissModal()} /> )} - {!isEmptyObject(report) && !isReceiptAttachment ? ( - - ) : ( - !!sourceForAttachmentView && - shouldLoadAttachment && - !isLoading && - !shouldShowNotFoundPage && ( - - - - ) - )} + {!shouldShowNotFoundPage && + (!isEmptyObject(report) && !isReceiptAttachment ? ( + + ) : ( + !!sourceForAttachmentView && + shouldLoadAttachment && + !isLoading && ( + + + + ) + ))}
{/* If we have an onConfirm method show a confirmation button */} {!!onConfirm && ( @@ -553,7 +566,7 @@ function AttachmentModal({ - )} - {/** + + {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} + + + )} + {/** These are the actionable buttons that appear at the bottom of a Concierge message for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} -
- ) : ( - - )} + {actionableItemButtons.length > 0 && ( + + )} + + ) : ( + + )} + ); } @@ -783,20 +790,18 @@ function ReportActionItem({ message = 'parentReportAction.deletedExpense'; } return ( - + - - - - ${translate(message)}`} /> - - - - + + + ${translate(message)}`} /> + + + ); } @@ -815,27 +820,25 @@ function ReportActionItem({ if (ReportUtils.isTaskReport(report)) { if (ReportUtils.isCanceledTaskReport(report, parentReportAction)) { return ( - + - - - - ${translate('parentReportAction.deletedTask')}`} /> - - - - + + + ${translate('parentReportAction.deletedTask')}`} /> + + + ); } return ( - + - + {renderThreadDivider} diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index f446c38fb1e9..0a7f9007c49b 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -7,7 +7,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ReportUtils from '@libs/ReportUtils'; @@ -38,7 +37,6 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions(); @@ -62,27 +60,29 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { onClose={() => navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing > - + - ReportUtils.navigateToDetailsPage(props.report)} - style={[styles.mh5, styles.mb3, styles.alignSelfStart]} - accessibilityLabel={translate('common.details')} - role={CONST.ROLE.BUTTON} - disabled={shouldDisableDetailPage} - > - - + + ReportUtils.navigateToDetailsPage(props.report)} + style={[styles.mh5, styles.mb3, styles.alignSelfStart]} + accessibilityLabel={translate('common.details')} + role={CONST.ROLE.BUTTON} + disabled={shouldDisableDetailPage} + > + + + ([]); const {isOffline} = useNetwork(); @@ -100,9 +96,8 @@ function ReportActionItemParentAction({ }, []); return ( - + - {allAncestors.map((ancestor) => ( action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? {}); const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); - let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); + let avatarSource = avatar; + let avatarId: number | string | undefined = actorAccountID; if (isWorkspaceActor) { displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); + avatarId = report.policyID; } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. @@ -100,16 +102,17 @@ function ReportActionItemSingle({ const delegateDisplayName = delegateDetails?.displayName; actorHint = `${delegateDisplayName} (${translate('reportAction.asCopilot')} ${displayName})`; displayName = actorHint; - avatarSource = UserUtils.getAvatar(delegateDetails?.avatar ?? '', Number(action.delegateAccountID)); + avatarSource = delegateDetails?.avatar; + avatarId = action.delegateAccountID; } // If this is a report preview, display names and avatars of both people involved let secondaryAvatar: Icon; const primaryDisplayName = displayName; if (displayAllActors) { - // The ownerAccountID and actorAccountID can be the same if the a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice + // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport ? iouReport?.managerID : iouReport?.ownerAccountID; - const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; + const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); if (!isInvoiceReport) { @@ -117,7 +120,7 @@ function ReportActionItemSingle({ } secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), + source: secondaryUserAvatar, type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName ?? '', id: secondaryAccountId, @@ -132,10 +135,10 @@ function ReportActionItemSingle({ secondaryAvatar = {name: '', source: '', type: 'avatar'}; } const icon = { - source: avatarSource, + source: avatarSource ?? FallbackAvatar, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName ?? '', - id: isWorkspaceActor ? report.policyID : actorAccountID, + id: avatarId, }; // Since the display name for a report action message is delivered with the report history as an array of fragments diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index d8aed24e3b13..c700fea4fb85 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -209,9 +209,13 @@ function ReportActionsList({ ), [sortedReportActions, isOffline], ); + + // whisper action doesn't affect lastVisibleActionCreated, so we should not take it into account while checking if there is the newest report action + const newestVisibleReportAction = useMemo(() => sortedVisibleReportActions.find((item) => !ReportActionsUtils.isWhisperAction(item)) ?? null, [sortedVisibleReportActions]); + const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); - const hasNewestReportAction = sortedVisibleReportActions?.[0]?.created === report.lastVisibleActionCreated; + const hasNewestReportAction = newestVisibleReportAction?.created === report.lastVisibleActionCreated; const hasNewestReportActionRef = useRef(hasNewestReportAction); hasNewestReportActionRef.current = hasNewestReportAction; const previousLastIndex = useRef(lastActionIndex); @@ -303,12 +307,12 @@ function ReportActionsList({ setCurrentUnreadMarker(null); }; - const unreadActionSubscription = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime) => { + const unreadActionSubscription = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime: string) => { resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(new Date().getTime()); }); - const readNewestActionSubscription = DeviceEventEmitter.addListener(`readNewestAction_${report.reportID}`, (newLastReadTime) => { + const readNewestActionSubscription = DeviceEventEmitter.addListener(`readNewestAction_${report.reportID}`, (newLastReadTime: string) => { resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(0); }); @@ -596,7 +600,7 @@ function ReportActionsList({ const extraData = useMemo(() => [isSmallScreenWidth ? currentUnreadMarker : undefined, ReportUtils.isArchivedRoom(report)], [currentUnreadMarker, isSmallScreenWidth, report]); const hideComposer = !ReportUtils.canUserPerformWriteAction(report); const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; - const canShowHeader = !isOffline && !hasHeaderRendered.current && scrollingVerticalOffset.current > VERTICAL_OFFSET_THRESHOLD; + const canShowHeader = isOffline || hasHeaderRendered.current; const contentContainerStyle: StyleProp = useMemo( () => [styles.chatContentScrollView, isLoadingNewerReportActions && canShowHeader ? styles.chatContentScrollViewWithHeaderLoader : {}], diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index d1e45f3998cd..5e18378ff949 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -12,7 +12,6 @@ import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; -import Log from '@libs/Log'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as NumberUtils from '@libs/NumberUtils'; import {generateNewRandomInt} from '@libs/NumberUtils'; @@ -105,6 +104,7 @@ function ReportActionsView({ const didLayout = useRef(false); const didLoadOlderChats = useRef(false); const didLoadNewerChats = useRef(false); + const {isOffline} = useNetwork(); // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest,we don't maintain their position and instead trigger a recalculation of their positioning in the list. // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned @@ -129,6 +129,14 @@ function ReportActionsView({ Report.openReport(reportID, reportActionID); }; + useEffect(() => { + // When we linked to message - we do not need to wait for initial actions - they already exists + if (!reportActionID || !isOffline) { + return; + } + Report.updateLoadingInitialReportAction(report.reportID); + }, [isOffline, report.reportID, reportActionID]); + useLayoutEffect(() => { setCurrentReportActionID(''); }, [route]); @@ -200,7 +208,7 @@ function ReportActionsView({ */ const fetchNewerAction = useCallback( (newestReportAction: OnyxTypes.ReportAction) => { - if (isLoadingNewerReportActions || isLoadingInitialReportActions) { + if (isLoadingNewerReportActions || isLoadingInitialReportActions || (reportActionID && isOffline)) { return; } @@ -217,7 +225,7 @@ function ReportActionsView({ Report.getNewerActions(reportID, newestReportAction.reportActionID); } }, - [isLoadingNewerReportActions, isLoadingInitialReportActions, reportID, transactionThreadReport, reportActionIDMap], + [isLoadingNewerReportActions, isLoadingInitialReportActions, reportActionID, isOffline, transactionThreadReport, reportActionIDMap, reportID], ); const hasMoreCached = reportActions.length < combinedReportActions.length; @@ -289,17 +297,6 @@ function ReportActionsView({ */ const loadOlderChats = useCallback( (force = false) => { - Log.info( - `[ReportActionsView] loadOlderChats ${JSON.stringify({ - isOffline: network.isOffline, - isLoadingOlderReportActions, - isLoadingInitialReportActions, - oldestReportActionID: oldestReportAction?.reportActionID, - hasCreatedAction, - isTransactionThread: !isEmptyObject(transactionThreadReport), - })}`, - ); - // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if ( !force && @@ -351,18 +348,6 @@ function ReportActionsView({ // and there are fewer than 23 items, indicating we've reached the oldest message. const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; - Log.info( - `[ReportActionsView] loadNewerChats ${JSON.stringify({ - isOffline: network.isOffline, - isLoadingNewerReportActions, - isLoadingInitialReportActions, - newestReportAction: newestReportAction.pendingAction, - firstReportActionID: newestReportAction?.reportActionID, - isLoadingOlderReportsFirstNeeded, - reportActionID, - })}`, - ); - if ( !force && (!reportActionID || @@ -372,7 +357,6 @@ function ReportActionsView({ // If there was an error only try again once on initial mount. We should also still load // more in case we have cached messages. (!hasMoreCached && didLoadNewerChats.current && hasLoadingNewerReportActionsError) || - network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) ) { return; @@ -391,7 +375,6 @@ function ReportActionsView({ reportActionID, indexOfLinkedAction, handleReportActionPagination, - network.isOffline, reportActions.length, newestReportAction, isFocused, @@ -630,6 +613,7 @@ export default Performance.withRenderTrace({id: ' rendering'} }, transactionThreadReport: { key: ({transactionThreadReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, + initialValue: {} as OnyxTypes.Report, }, })(MemoizedReportActionsView), ); diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 82b49d1e260c..c537fedfe994 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -1,33 +1,40 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import type {Attachment} from '@components/Attachments/types'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type ReportAttachmentsProps = StackScreenProps; +type ReportAttachmentsProps = StackScreenProps; function ReportAttachments({route}: ReportAttachmentsProps) { const reportID = route.params.reportID; + const type = route.params.type; + const accountID = route.params.accountID; const report = ReportUtils.getReport(reportID); + const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); // In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource const source = Number(route.params.source) || route.params.source; const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, String(attachment.source)); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID)); Navigation.navigate(routeToNavigate); }, - [reportID], + [reportID, accountID, type], ); return ( ); } diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index b3bddb9ba499..ac56fe916bc9 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import AnonymousReportFooter from '@components/AnonymousReportFooter'; import ArchivedReportFooter from '@components/ArchivedReportFooter'; +import BlockedReportFooter from '@components/BlockedReportFooter'; import OfflineIndicator from '@components/OfflineIndicator'; import {usePersonalDetails} from '@components/OnyxProvider'; import SwipeableView from '@components/SwipeableView'; @@ -30,6 +31,9 @@ type ReportFooterOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; + + /** Whether user is blocked from chat. */ + blockedFromChat: OnyxEntry; }; type ReportFooterProps = ReportFooterOnyxProps & { @@ -74,6 +78,7 @@ function ReportFooter({ isReportReadyForDisplay = true, listHeight = 0, isComposerFullSize = false, + blockedFromChat, onComposerBlur, onComposerFocus, }: ReportFooterProps) { @@ -85,7 +90,7 @@ function ReportFooter({ const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; - const hideComposer = !ReportUtils.canUserPerformWriteAction(report, reportNameValuePairs); + const hideComposer = !ReportUtils.canUserPerformWriteAction(report, reportNameValuePairs) || blockedFromChat; const canWriteInReport = ReportUtils.canWriteInReport(report); const isSystemChat = ReportUtils.isSystemChat(report); @@ -153,6 +158,7 @@ function ReportFooter({ /> )} {isArchivedRoom && } + {!isArchivedRoom && blockedFromChat && } {!isAnonymousUser && !canWriteInReport && isSystemChat && } {!isSmallScreenWidth && {hideComposer && }} @@ -190,6 +196,9 @@ export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, + blockedFromChat: { + key: ONYXKEYS.NVP_BLOCKED_FROM_CHAT, + }, })( memo( ReportFooter, diff --git a/src/pages/home/report/reportActionFragmentPropTypes.js b/src/pages/home/report/reportActionFragmentPropTypes.js deleted file mode 100644 index 5d2e3b951a1d..000000000000 --- a/src/pages/home/report/reportActionFragmentPropTypes.js +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** The type of the action item fragment. Used to render a corresponding component */ - type: PropTypes.string.isRequired, - - /** The text content of the fragment. */ - text: PropTypes.string.isRequired, - - /** Used to apply additional styling. Style refers to a predetermined constant and not a class name. e.g. 'normal' - * or 'strong' - */ - style: PropTypes.string, - - /** ID of a report */ - reportID: PropTypes.string, - - /** ID of a policy */ - policyID: PropTypes.string, - - /** The target of a link fragment e.g. '_blank' */ - target: PropTypes.string, - - /** The destination of a link fragment e.g. 'https://www.expensify.com' */ - href: PropTypes.string, - - /** An additional avatar url - not the main avatar url but used within a message. */ - iconUrl: PropTypes.string, - - /** Fragment edited flag */ - isEdited: PropTypes.bool, -}); diff --git a/src/pages/home/report/reportActionPropTypes.js b/src/pages/home/report/reportActionPropTypes.js deleted file mode 100644 index 5f9643571e54..000000000000 --- a/src/pages/home/report/reportActionPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; -import reportActionFragmentPropTypes from './reportActionFragmentPropTypes'; - -export default { - /** The ID of the reportAction. It is the string representation of the a 64-bit integer. */ - reportActionID: PropTypes.string, - - /** Name of the action e.g. ADD_COMMENT */ - actionName: PropTypes.string, - - /** Person who created the action */ - person: PropTypes.arrayOf(reportActionFragmentPropTypes), - - /** ISO-formatted datetime */ - created: PropTypes.string, - - /** report action message */ - message: PropTypes.arrayOf(reportActionFragmentPropTypes), - - /** Original message associated with this action */ - originalMessage: PropTypes.shape({ - // The ID of the iou transaction - IOUTransactionID: PropTypes.string, - - /** accountIDs of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ - whisperedTo: PropTypes.arrayOf(PropTypes.number), - }), - - /** Error message that's come back from the server. */ - error: PropTypes.string, -}; diff --git a/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx b/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx index e7726fb89537..b0287efb8990 100644 --- a/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx +++ b/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx @@ -5,7 +5,6 @@ import AvatarWithIndicator from '@components/AvatarWithIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; type ProfileAvatarWithIndicatorProps = { @@ -23,7 +22,8 @@ function ProfileAvatarWithIndicator({isSelected = false}: ProfileAvatarWithIndic diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 308d4b1fd294..3852cb0d12e7 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -13,7 +13,7 @@ import type {PolicySelector} from '@hooks/useReportIDs'; import {policySelector, useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as Policy from '@userActions/Policy'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SidebarLinks from './SidebarLinks'; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 76e756f6a7b0..2e55405d3cac 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -24,7 +24,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; -import * as Policy from '@userActions/Policy'; +import * as Policy from '@userActions/Policy/Policy'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx index 51b432d7e860..ff8970a3a0a7 100644 --- a/src/pages/iou/SplitBillDetailsPage.tsx +++ b/src/pages/iou/SplitBillDetailsPage.tsx @@ -5,10 +5,13 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -16,6 +19,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; import type {WithReportAndReportActionOrNotFoundProps} from '@pages/home/report/withReportAndReportActionOrNotFound'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,6 +61,7 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr const styles = useThemeStyles(); const reportID = report?.reportID ?? ''; const {translate} = useLocalize(); + const theme = useTheme(); const reportAction = useMemo(() => reportActions?.[route.params.reportActionID] ?? ({} as ReportAction), [reportActions, route.params.reportActionID]); const participantAccountIDs = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? reportAction?.originalMessage.participantAccountIDs ?? [] : []; @@ -100,9 +105,17 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr {isScanning && ( + } + description={translate('iou.receiptScanInProgressDescription')} shouldShowBorderBottom + shouldStyleFlexGrow={false} /> )} {!!participants.length && ( diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 1bbf0d02a941..c1d55516b433 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -13,13 +12,12 @@ import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as IOUUtils from '@libs/IOUUtils'; import * as KeyDownPressListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as IOU from '@userActions/IOU'; import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -105,9 +103,6 @@ function IOURequestStartPage({ const isExpenseReport = ReportUtils.isExpenseReport(report); const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT; - // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense - const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); - const navigateBack = () => { Navigation.closeRHPFlow(); }; @@ -126,15 +121,21 @@ function IOURequestStartPage({ } return ( - - {({safeAreaPaddingBottomStyle}) => ( - + + {({safeAreaPaddingBottomStyle}) => ( - - )} - + )} + + ); } diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index b525a2c1e3dd..1d9aec1ea60d 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -22,7 +22,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import type {MaybePhraseKey} from '@libs/Localize'; import type {Options} from '@libs/OptionsListUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as Policy from '@userActions/Policy'; +import * as Policy from '@userActions/Policy/Policy'; import * as Report from '@userActions/Report'; import type {IOUAction, IOURequestType, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; @@ -358,6 +358,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic onChangeText={setSearchTerm} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} onSelectRow={(item) => (isIOUSplit ? addParticipantToSelection(item) : addSingleParticipant(item))} + shouldDebounceRowSelect footerContent={footerContent} headerMessage={headerMessage} showLoadingPlaceholder={!areOptionsInitialized || !didScreenTransitionEnd} diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index d4919a4172aa..fd5c60537c38 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -1,19 +1,26 @@ import lodashIsEmpty from 'lodash/isEmpty'; import React, {useEffect} from 'react'; +import {ActivityIndicator, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import Button from '@components/Button'; import CategoryPicker from '@components/CategoryPicker'; +import FixedFooter from '@components/FixedFooter'; +import * as Illustrations from '@components/Icon/Illustrations'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; 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 PolicyActions from '@userActions/Policy'; +import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -75,6 +82,7 @@ function IOURequestStepCategory({ const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); const isEditing = action === CONST.IOU.ACTION.EDIT; const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; @@ -92,7 +100,7 @@ function IOURequestStepCategory({ const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = !shouldShowCategory || (isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction))); + const shouldShowNotFoundPage = isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction)); const fetchData = () => { if (policy && policyCategories) { @@ -101,8 +109,9 @@ function IOURequestStepCategory({ PolicyActions.openDraftWorkspaceRequest(report?.policyID ?? ''); }; - - useNetwork({onReconnect: fetchData}); + const {isOffline} = useNetwork({onReconnect: fetchData}); + const isLoading = !isOffline && policyCategories === undefined; + const shouldShowEmptyState = !isLoading && !shouldShowCategory; useEffect(() => { fetchData(); @@ -150,14 +159,53 @@ function IOURequestStepCategory({ shouldShowWrapper shouldShowNotFoundPage={shouldShowNotFoundPage} testID={IOURequestStepCategory.displayName} - includeSafeAreaPaddingBottom={false} > - {translate('iou.categorySelection')} - + {isLoading && ( + + )} + {shouldShowEmptyState && ( + + + +