diff --git a/.eslintrc.js b/.eslintrc.js index 4df9493b2e8c..c8a842fa4650 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -78,7 +78,7 @@ const restrictedImportPatterns = [ module.exports = { extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], - plugins: ['react-native-a11y'], + plugins: ['react-native-a11y', 'testing-library'], parser: 'babel-eslint', ignorePatterns: ['!.*', 'src/vendor', '.github/actions/**/index.js', 'desktop/dist/*.js', 'dist/*.js', 'node_modules/.bin/**', 'node_modules/.cache/**', '.git/**'], env: { @@ -122,6 +122,7 @@ module.exports = { }, }, ], + 'rulesdir/avoid-anonymous-functions': 'off', }, }, // This helps disable the `prefer-alias` rule to be enabled for specific directories @@ -129,6 +130,20 @@ module.exports = { files: ['tests/**/*.js', 'tests/**/*.ts', 'tests/**/*.jsx', 'assets/**/*.js', '.storybook/**/*.js'], rules: {'@dword-design/import-alias/prefer-alias': ['off']}, }, + { + files: ['tests/**/*.js', 'tests/**/*.ts', 'tests/**/*.jsx', 'tests/**/*.tsx'], + extends: ['plugin:testing-library/react'], + rules: { + 'testing-library/await-async-queries': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-debugging-utils': 'error', + 'testing-library/no-manual-cleanup': 'error', + 'testing-library/no-unnecessary-act': 'error', + 'testing-library/prefer-find-by': 'error', + 'testing-library/prefer-presence-queries': 'error', + 'testing-library/prefer-screen-queries': 'error', + }, + }, { files: ['*.js', '*.jsx'], settings: { @@ -276,5 +291,11 @@ module.exports = { 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], }, }, + { + files: ['en.ts', 'es.ts'], + rules: { + 'rulesdir/use-periods-for-error-messages': 'error', + }, + }, ], }; diff --git a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts index 7de78e257dc4..26947193cd80 100644 --- a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts +++ b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts @@ -8,6 +8,7 @@ import {promiseDoWhile} from '@github/libs/promiseWhile'; type CurrentStagingDeploys = Awaited>['data']['workflow_runs']; function run() { + console.info('[awaitStagingDeploys] POLL RATE', CONST.POLL_RATE); console.info('[awaitStagingDeploys] run()'); console.info('[awaitStagingDeploys] getStringInput', getStringInput); console.info('[awaitStagingDeploys] GitHubUtils', GitHubUtils); diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index d84c6df1a0d3..c91313520673 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12131,6 +12131,7 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); const promiseWhile_1 = __nccwpck_require__(9438); function run() { + console.info('[awaitStagingDeploys] POLL RATE', CONST_1.default.POLL_RATE); console.info('[awaitStagingDeploys] run()'); console.info('[awaitStagingDeploys] getStringInput', ActionUtils_1.getStringInput); console.info('[awaitStagingDeploys] GitHubUtils', GithubUtils_1.default); @@ -12742,7 +12743,12 @@ function promiseWhile(condition, action) { resolve(); return; } - Promise.resolve(actionResult).then(loop).catch(reject); + Promise.resolve(actionResult) + .then(() => { + // Set a timeout to delay the next loop iteration + setTimeout(loop, 1000); // 1000 ms delay + }) + .catch(reject); } }; loop(); diff --git a/.github/libs/promiseWhile.ts b/.github/libs/promiseWhile.ts index 01c061096d64..401b6ee2e18a 100644 --- a/.github/libs/promiseWhile.ts +++ b/.github/libs/promiseWhile.ts @@ -19,7 +19,12 @@ function promiseWhile(condition: () => boolean, action: (() => Promise) | return; } - Promise.resolve(actionResult).then(loop).catch(reject); + Promise.resolve(actionResult) + .then(() => { + // Set a timeout to delay the next loop iteration + setTimeout(loop, 1000); // 1000 ms delay + }) + .catch(reject); } }; loop(); diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 9eb5bc6eb409..39dfbe8e84a7 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -27,19 +27,49 @@ jobs: }); return jobsData.data; + - name: Fetch Previous Workflow Run + id: previous-workflow-run + uses: actions/github-script@v7 + with: + script: | + const runId = ${{ github.event.workflow_run.id }}; + const allRuns = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'preDeploy.yml', + }); + const filteredRuns = allRuns.data.workflow_runs.filter(run => run.actor.login !== 'OSBotify' && run.status !== 'cancelled'); + const currentIndex = filteredRuns.findIndex(run => run.id === runId); + const previousRun = filteredRuns[currentIndex + 1]; + return previousRun; + + - name: Fetch Previous Workflow Run Jobs + id: previous-workflow-jobs + uses: actions/github-script@v7 + with: + script: | + const previousRun = ${{ steps.previous-workflow-run.outputs.result }}; + const runId = previousRun.id; + const jobsData = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + return jobsData.data; + - name: Process Each Failed Job uses: actions/github-script@v7 with: script: | const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; - + const previousRun = ${{ steps.previous-workflow-run.outputs.result }}; + const previousRunJobs = ${{ steps.previous-workflow-jobs.outputs.result }}; const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: headCommit, }); - const pr = prData.data[0]; const prLink = pr.html_url; const prAuthor = pr.user.login; @@ -50,14 +80,8 @@ jobs: if (jobs.jobs[i].conclusion == 'failure') { const jobName = jobs.jobs[i].name; const jobLink = jobs.jobs[i].html_url; - const issues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: failureLabel, - state: 'open' - }); - const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); - if (!existingIssue) { + const previousJob = previousRunJobs.jobs.find(job => job.name === jobName); + if (previousJob?.conclusion === 'success') { const annotations = await github.rest.checks.listAnnotations({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index bb850e6eda10..353a898a941f 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -87,12 +87,11 @@ jobs: MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - # Note: Android production deploys are temporarily disabled until https://github.com/Expensify/App/issues/40108 is resolved - # - name: Run Fastlane production - # if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - # run: bundle exec fastlane android production - # env: - # VERSION: ${{ env.VERSION_CODE }} + - name: Run Fastlane production + if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane android production + env: + VERSION: ${{ env.VERSION_CODE }} - name: Archive Android sourcemaps uses: actions/upload-artifact@v3 diff --git a/README.md b/README.md index 3a8aabcdf1be..29a9e9b8ffdc 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ #### Table of Contents * [Local Development](#local-development) -* [Testing on browsers on simulators and emulators](#testing-on-browsers-on-simulators-and-emulators) +* [Testing on browsers in simulators and emulators](#testing-on-browsers-in-simulators-and-emulators) * [Running The Tests](#running-the-tests) * [Debugging](#debugging) * [App Structure and Conventions](#app-structure-and-conventions) diff --git a/android/app/build.gradle b/android/app/build.gradle index aa57fd20f204..7b8271cfcbf2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,20 +2,12 @@ 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' -} - react { /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' @@ -106,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046406 - versionName "1.4.64-6" + versionCode 1001047002 + versionName "1.4.70-2" // 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" @@ -170,7 +162,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 7ecd482b38f0..10600480d8bb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,7 +20,6 @@ buildscript { repositories { google() mavenCentral() - maven {url "https://maven.fullstory.com"} } dependencies { classpath("com.android.tools.build:gradle") @@ -28,9 +27,6 @@ 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.45.1") - // 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") @@ -74,7 +70,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/animations/Plane.lottie b/assets/animations/Plane.lottie new file mode 100644 index 000000000000..5244cb7bea10 Binary files /dev/null and b/assets/animations/Plane.lottie differ diff --git a/assets/images/all.svg b/assets/images/all.svg new file mode 100644 index 000000000000..d1a833d280ce --- /dev/null +++ b/assets/images/all.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 df13c75ca414..8d2ded92e791 100644 --- a/assets/images/arrow-right.svg +++ b/assets/images/arrow-right.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + diff --git a/assets/images/back-left.svg b/assets/images/back-left.svg index 51164100ff59..2ddd554e9720 100644 --- a/assets/images/back-left.svg +++ b/assets/images/back-left.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + diff --git a/assets/images/tax.svg b/assets/images/coins.svg similarity index 100% rename from assets/images/tax.svg rename to assets/images/coins.svg diff --git a/assets/images/connection-complete.svg b/assets/images/connection-complete.svg new file mode 100644 index 000000000000..fbfb2b041358 --- /dev/null +++ b/assets/images/connection-complete.svg @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/invoice-generic.svg b/assets/images/invoice-generic.svg new file mode 100644 index 000000000000..d0e2662c4084 --- /dev/null +++ b/assets/images/invoice-generic.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/assets/images/play.svg b/assets/images/play.svg index cb781459e44e..5f7e14969529 100644 --- a/assets/images/play.svg +++ b/assets/images/play.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg new file mode 100644 index 000000000000..ecdf3cf2e115 --- /dev/null +++ b/assets/images/receipt-scan.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__alert.svg b/assets/images/simple-illustrations/simple-illustration__alert.svg new file mode 100644 index 000000000000..2e7bca02f5e3 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__alert.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__piggybank.svg b/assets/images/simple-illustrations/simple-illustration__piggybank.svg index a9cf2b02c5dc..be87ff34752a 100644 --- a/assets/images/simple-illustrations/simple-illustration__piggybank.svg +++ b/assets/images/simple-illustrations/simple-illustration__piggybank.svg @@ -1,50 +1,50 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/suitcase.svg b/assets/images/suitcase.svg new file mode 100644 index 000000000000..97036db6b5ac --- /dev/null +++ b/assets/images/suitcase.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/x-circle.svg b/assets/images/x-circle.svg new file mode 100644 index 000000000000..c186e41c4244 --- /dev/null +++ b/assets/images/x-circle.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 0660cdb452fb..9f8b7a711d78 100644 --- a/babel.config.js +++ b/babel.config.js @@ -14,23 +14,6 @@ const defaultPlugins = [ // source code transformation as we do not use class property assignment. 'transform-class-properties', - /* Fullstory */ - [ - '@fullstory/react-native', - { - version: '1.4.0', - org: 'o-1WN56P-na1', - enabledVariants: 'all', - }, - ], - [ - '@fullstory/babel-plugin-annotate-react', - { - native: true, - setFSTagName: true, - }, - ], - // Keep it last 'react-native-reanimated/plugin', ]; diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 9d397b9557a3..7cafafca9973 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/thirdPartyScripts.js'}, + {from: 'web/gtm.js'}, {from: 'assets/css', to: 'css'}, {from: 'assets/fonts/web', to: 'fonts'}, {from: 'assets/sounds', to: 'sounds'}, diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 8f32a2d95c99..7a196da6b691 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -21,16 +21,12 @@ const getConfiguration = (environment: Environment): Promise => process.env.USE_WEB_PROXY === 'false' ? {} : { - proxy: { - // eslint-disable-next-line @typescript-eslint/naming-convention - '/api': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/staging': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/chat-attachments': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/receipts': 'http://[::1]:9000', - }, + proxy: [ + { + context: ['/api', '/staging', '/chat-attachments', '/receipts'], + target: 'http://[::1]:9000', + }, + ], }; const baseConfig = getCommonConfiguration(environment); diff --git a/contributingGuides/ACCESSIBILITY.md b/contributingGuides/ACCESSIBILITY.md index b94cbf3087c8..ff73eaf2942e 100644 --- a/contributingGuides/ACCESSIBILITY.md +++ b/contributingGuides/ACCESSIBILITY.md @@ -19,9 +19,9 @@ When implementing pressable components, it's essential to create accessible flow - ensure that after performing press focus is set on the correct next element - this is especially important for keyboard users who rely on focus to navigate the app. All Pressable components have a `nextFocusRef` prop that can be used to set the next focusable element after the pressable component. This prop accepts a ref to the next focusable element. For example, if you have a button that opens a modal, you can set the next focus to the first focusable element in the modal. This way, when the user presses the button, focus will be set on the first focusable element in the modal, and the user can continue navigating the modal using the keyboard. -- size of any pressable component should be at least 44x44dp. This is the minimum size recommended by Apple and Google for touch targets. If the pressable component is smaller than `44x44dp`, it will be difficult for users with motor disabilities to interact with it. Pressable components have a `autoHitSlop` prop that can be used to automatically increase the size of the pressable component to `44x44dp`. This prop accepts a boolean value. If set to true, the pressable component will automatically increase its touchable size to 44x44dp. If set to false, the pressable component will not increase its size. By default, this prop is set to false. +- size of any pressable component should be at least 44x44dp. This is the minimum size recommended by Apple and Google for touch targets. If the pressable component is smaller than `44x44dp`, it will be difficult for users with motor disabilities to interact with it. Pressable components have a `autoHitSlop` prop that can be used to automatically increase the size of the pressable component to `44x44dp`. This prop accepts a boolean value. If set to true, the pressable component will automatically increase its touchable size to `44x44dp`. If set to false, the pressable component will not increase its size. By default, this prop is set to false. -- ensure that the pressable component has a label and hint. This is especially important for users with visual disabilities who rely on screen readers to navigate the app. All Pressable components have a `accessibilitylabel` prop that can be used to set the label of the pressable component. This prop accepts a string value. All Pressable components also have a `accessibilityHint` prop that can be used to set the hint of the pressable component. This prop accepts a string value. The accessibilityHint prop is optional. If not set, the pressable component will fallback to the accessibilityLabel prop. For example, if you have a button that opens a modal, you can set the accessibilityLabel to "Open modal" and the accessibilityHint to "Opens a modal with more information". This way, when the user focuses on the button, the screen reader will read "Open modal. Opens a modal with more information". This will help the user understand what the button does and what to expect after pressing it. +- ensure that the pressable component has a label and hint. This is especially important for users with visual disabilities who rely on screen readers to navigate the app. All Pressable components have a `accessibilitylabel` prop that can be used to set the label of the pressable component. This prop accepts a string value. All Pressable components also have a `accessibilityHint` prop that can be used to set the hint of the pressable component. This prop accepts a string value. The `accessibilityHint` prop is optional. If not set, the pressable component will fallback to the `accessibilityLabel` prop. For example, if you have a button that opens a modal, you can set the `accessibilityLabel` to "Open modal" and the `accessibilityHint` to "Opens a modal with more information". This way, when the user focuses on the button, the screen reader will read "Open modal. Opens a modal with more information". This will help the user understand what the button does and what to expect after pressing it. - the `enableInScreenReaderStates` prop proves invaluable when aiming to enhance the accessibility of clickable elements, particularly when desiring to enlarge the clickable area of a component, such as an entire row. This can be especially useful, for instance, when dealing with tables where only a small portion of a row, like a checkbox, might traditionally trigger an action. By employing this prop, developers can ensure that the entirety of a designated component, in this case a row, is made accessible to users employing screen readers. This creates a more inclusive user experience, allowing individuals relying on screen readers to interact with the component effortlessly. For instance, in a table, using this prop on a row component enables users to click anywhere within the row to trigger an action, significantly improving accessibility and user-friendliness. diff --git a/contributingGuides/API.md b/contributingGuides/API.md index aea684417e41..cf17b4093244 100644 --- a/contributingGuides/API.md +++ b/contributingGuides/API.md @@ -8,11 +8,11 @@ These are best practices related to the current API used for App. - Data is pushed to the client and put straight into Onyx by low-level libraries. - Clients should be kept up-to-date with many small incremental changes to data. - Creating data needs to be optimistic on every connection (offline, slow 3G, etc), eg. `RequestMoney` or `SplitBill` should work without waiting for a server response. -- For new objects created from the client (reports, reportActions, policies) we're going to generate a random string ID immediately on the client, rather than needing to wait for the server to give us an ID for the created object. +- For new objects created from the client (reports, reportActions, policies), we're going to generate a random string ID immediately on the client, rather than needing to wait for the server to give us an ID for the created object. - This client-generated ID will become the primary key for that record in the database. This will provide more offline functionality than was previously possible. ## Response Handling -When the web server responds to an API call the response is sent to the server in one of two ways. +When the web server responds to an API call, the response is sent to the server in one of two ways. 1. **HTTPS Response** - Data that is returned with the HTTPS response is only sent to the client that initiated the request. The network library will look for any `onyxData` in the response and send it straight to `Onyx.update(response.onyxData)`. @@ -20,31 +20,38 @@ When the web server responds to an API call the response is sent to the server i 1. **Pusher Event** (web socket) - Data returned with a Pusher event is sent to all currently connected clients for the user that made the request, as well as any other necessary participants (eg. like other people in the chat) Pusher listens for an `onyxApiUpdate` event and sends the data straight to `Onyx.update(pushJSON)`. + ### READ Responses This is a response that returns data from the database. -A READ response is very specific to the client making the request, so it's data is returned with the **HTTPS Response**. This prevents a lot of unnecessary data from being sent to other clients that will never use it. +A READ response is very specific to the client making the request, so its data is returned with the **HTTPS Response**. This prevents a lot of unnecessary data from being sent to other clients that will never use it. In PHP, the response is added like this: + ```php $response['onyxData'][] = blahblahblah; ``` + The data will be returned with the HTTPS response. + ### WRITE Responses This response happens when new data is created in the database. New data (`jsonCode===200`) should be sent to all connected clients so a **Pusher Event** is used to update another currently connected clients with the new data that was created. In PHP, the response is added like this: + ```php $onyxUpdate[] = blahblahblah; ``` + The data will automatically be sent to the user via Pusher. #### WRITE Response Errors When there is an error on a WRITE response (`jsonCode!==200`), the error must come back to the client on the HTTPS response. The error is only relevant to the client that made the request and it wouldn't make sense to send it out to all connected clients. Error messages should be returned and stored as an object under the `errors` property, keyed by an integer [microtime](https://github.com/Expensify/Web-Expensify/blob/25d056c9c531ea7f12c9bf3283ec554dd5d1d316/lib/Onyx.php#L148-L154). It's also common to store errors keyed by microtime under `errorFields.fieldName`. Use this format when error messages should be saved on a general object but are only relevant to a specific field / key on the object. If absolutely needed, additional error properties can be stored under other, more specific fields that sit at the same level as `errors`: + ```php [ 'onyxMethod' => Onyx::METHOD_MERGE, diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index e6b999b7cb01..08a444a6b8e4 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -41,7 +41,7 @@ The redirect URI must match a URI in the Google or Apple client ID configuration Pop-up mode opens a pop-up window to show the third-party sign-in form. But it also changes how tokens are given to the client app. Instead of an HTTPS request, they are returned by the JS libraries in memory, either via a callback (Google) or a promise (Apple). -Apple and Google both check that the client app is running on an allowed domain. The sign-in process will fail otherwise. Google allows localhost, but Apple does not, and so testing Apple in development environments requires hosting the client app on a domain that the Apple client ID (or "service ID", in Apple's case) has been configured with. +Apple and Google both check that the client app is running on an allowed domain. The sign-in process will fail otherwise. Google allows `localhost`, but Apple does not, and so testing Apple in development environments requires hosting the client app on a domain that the Apple client ID (or "service ID", in Apple's case) has been configured with. In the case of Apple, sometimes it will silently fail at the very end of the sign-in process, where the only sign that something is wrong is that the pop-up fails to close. In this case, it's very likely that configuration mismatch is the issue. @@ -125,24 +125,24 @@ If you need to check that you received the correct data, check it on [jwt.io](ht Hardcode this token into `Session.beginAppleSignIn`, and but also verify a valid token was passed into the function, for example: -``` +```js function beginAppleSignIn(idToken) { -+ // Show that a token was passed in, without logging the token, for privacy -+ window.alert(`ORIGINAL ID TOKEN LENGTH: ${idToken.length}`); -+ const hardcodedToken = '...'; + // Show that a token was passed in, without logging the token, for privacy + window.alert(`ORIGINAL ID TOKEN LENGTH: ${idToken.length}`); + const hardcodedToken = '...'; const {optimisticData, successData, failureData} = signInAttemptState(); -+ API.write('SignInWithApple', {idToken: hardcodedToken}, {optimisticData, successData, failureData}); -- API.write('SignInWithApple', {idToken}, {optimisticData, successData, failureData}); + API.write('SignInWithApple', {idToken: hardcodedToken}, {optimisticData, successData, failureData}); + API.write('SignInWithApple', {idToken}, {optimisticData, successData, failureData}); } ``` #### Configure the SSH tunneling -You can use any SSH tunneling service that allows you to configure custom subdomains so that we have a consistent address to use. We'll use ngrok in these examples, but ngrok requires a paid account for this. If you need a free option, try serveo.net. +You can use any SSH tunneling service that allows you to configure custom subdomains so that we have a consistent address to use. We'll use ngrok in these examples, but ngrok requires a paid account for this. If you need a free option, try [serveo.net](https://serveo.net). After you've set ngrok up to be able to run on your machine (requires configuring a key with the command line tool, instructions provided by the ngrok website after you create an account), test hosting the web app on a custom subdomain. This example assumes the development web app is running at `dev.new.expensify.com:8082`: -``` +```shell ngrok http 8082 --host-header="dev.new.expensify.com:8082" --subdomain=mysubdomain ``` @@ -183,7 +183,7 @@ Desktop will require the same configuration, with these additional steps: #### Configure web app URL in .env -Add `NEW_EXPENSIFY_URL` to .env, and set it to the HTTPS URL where the web app can be found, for example: +Add `NEW_EXPENSIFY_URL` to `.env`, and set it to the HTTPS URL where the web app can be found, for example: ``` NEW_EXPENSIFY_URL=https://subdomain.ngrok-free.app @@ -195,7 +195,7 @@ Note that changing this value to a domain that isn't configured for use with Exp #### 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". +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: @@ -203,7 +203,7 @@ Within the `.env` file, set `envName` to something other than "Development", for envName=Staging ``` -Alternatively, within the `DeepLinkWrapper/index.website.js` file you can set the `CONFIG.ENVIRONMENT` to something other than "Development". +Alternatively, within the `DeepLinkWrapper/index.website.js` file, you can set the `CONFIG.ENVIRONMENT` to something other than "Development". #### Handle deep links in dev on MacOS @@ -211,7 +211,7 @@ If developing on MacOS, the development desktop app can't handle deeplinks corre 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 diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 25f54c668b24..13f7592b65e1 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -34,7 +34,7 @@ At this time, we are not hiring contractors in Crimea, North Korea, Russia, Iran ## Slack channels All contributors should be a member of a shared Slack channel called [#expensify-open-source](https://expensify.slack.com/archives/C01GTK53T8Q) -- this channel is used to ask **general questions**, facilitate **discussions**, and make **feature requests**. -Before requesting an invite to Slack please ensure your Upwork account is active, since we only pay via Upwork (see [below](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#payment-for-contributions)). To request an invite to Slack, email contributors@expensify.com with the subject `Slack Channel Invites`. We'll send you an invite! +Before requesting an invite to Slack, please ensure your Upwork account is active, since we only pay via Upwork (see [below](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#payment-for-contributions)). To request an invite to Slack, email contributors@expensify.com with the subject `Slack Channel Invites`. We'll send you an invite! Note: Do not send direct messages to the Expensify team in Slack or Expensify Chat, they will not be able to respond. @@ -44,7 +44,7 @@ Note: if you are hired for an Upwork job and have any job-specific questions, pl If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue. ## Payment for Contributions -We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing, please create an Upwork account, apply for an available job in [GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22), and finally apply for the job in Upwork once your proposal gets selected in GitHub. Please make sure your Upwork profile is **fully verified** before applying, otherwise you run the risk of not being paid. If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted. +We hire and pay external contributors via [Upwork.com](https://www.upwork.com). If you'd like to be paid for contributing, please create an Upwork account, apply for an available job in [GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22), and finally apply for the job in Upwork once your proposal gets selected in GitHub. Please make sure your Upwork profile is **fully verified** before applying, otherwise you run the risk of not being paid. If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted. Payment for your contributions will be made no less than 7 days after the pull request is deployed to production to allow for [regression](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions) testing. If you have not received payment after 8 days of the PR being deployed to production, and there are no [regressions](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#regressions), please add a comment to the issue mentioning the BugZero team member (Look for the melvin-bot "Triggered auto assignment to... (`Bug`)" to see who this is). @@ -75,10 +75,10 @@ This is the most common scenario for contributors. The Expensify team posts new >**Solution:** Start up time will perceptibly decrease by 1042ms if we prevent the unnecessary re-renders of this component. ## Working on Expensify Jobs -*Reminder: For technical guidance please refer to the [README](https://github.com/Expensify/App/blob/main/README.md)*. +*Reminder: For technical guidance, please refer to the [README](https://github.com/Expensify/App/blob/main/README.md)*. ## Posting Ideas -Additionally if you want to discuss an idea with the open source community without having a P/S statement yet, you can post it in #expensify-open-source with the prefix `IDEA:`. All ideas to build the future of Expensify are always welcome! i.e.: "`IDEA:` I don't have a P/S for this yet, but just kicking the idea around... what if we [insert crazy idea]?". +Additionally, if you want to discuss an idea with the open source community without having a P/S statement yet, you can post it in #expensify-open-source with the prefix `IDEA:`. All ideas to build the future of Expensify are always welcome! i.e.: "`IDEA:` I don't have a P/S for this yet, but just kicking the idea around... what if we [insert crazy idea]?". #### Make sure you can test on all platforms * Expensify requires that you can test the app on iOS, MacOS, Android, Web, and mWeb. @@ -147,7 +147,7 @@ Additionally if you want to discuss an idea with the open source community witho #### Timeline expectations and asking for help along the way - If you have made a change to your pull request and are ready for another review, leave a comment that says "Updated" on the pull request itself. - Please keep the conversation in GitHub, and do not ping individual reviewers in Slack or Upwork to get their attention. -- Pull Request reviews can sometimes take a few days. If your pull request has not been addressed after four days please let us know via the #expensify-open-source Slack channel. +- Pull Request reviews can sometimes take a few days. If your pull request has not been addressed after four days, please let us know via the #expensify-open-source Slack channel. - On occasion, our engineers will need to focus on a feature release and choose to place a hold on the review of your PR. #### Important note about JavaScript Style diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index b2f912277dc5..e53be6f6b269 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -5,7 +5,8 @@ This document lists specific guidelines for using our Form component and general ## General Form UI/UX ### Inputs -Any form input needs to be wrapped in [InputWrapper](https://github.com/Expensify/App/blob/029d009731dcd3c44cd1321672b9672ef0d3d7d9/src/components/Form/InputWrapper.js) and passed as `InputComponent` property additionally it's necessary po pass an unique `inputID`. All other props of the input can be passed as `InputWrapper` props. +Any form input needs to be wrapped in [InputWrapper](https://github.com/Expensify/App/blob/029d009731dcd3c44cd1321672b9672ef0d3d7d9/src/components/Form/InputWrapper.js) and passed as `InputComponent` property, additionally, it's necessary to pass an unique `inputID`. All other props of the input can be passed as `InputWrapper` props. + ```jsx ``` -We also have [keyboardType](https://github.com/Expensify/App/blob/9418b870515102631ea2156b5ea253ee05a98ff1/src/CONST.js#L760-L763) and should be used for specific use cases when there is no `inputMode` equivalent of the value exist. and should be used like so: +We also have [keyboardType](https://github.com/Expensify/App/blob/9418b870515102631ea2156b5ea253ee05a98ff1/src/CONST.js#L760-L763) and should be used for specific use cases when there is no `inputMode` equivalent of the value exist, and should be used like so: ```jsx ` is inside a `` we will want to disable the default safe area padding applied there e.g. +Any `FormProvider.js` that has a button will also add safe area padding by default. If the `` is inside a ``, we will want to disable the default safe area padding applied there e.g. -```js +```jsx {...} diff --git a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md index e7dcf5404c34..a72af88ae00b 100644 --- a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md +++ b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md @@ -6,7 +6,7 @@ C+ are contributors who are experienced at working with Expensify and have gaine ## Why would someone want to be a C+ - C+ are compensated the same price as the contributor for reviewing proposals and the associated PR. (ie. if a job is listed at $1000, that’s how much the C+ will make if they review both the proposals and PR). If regressions are found that should have* been caught after the PR has been approved, C+ payment is reduced by 50% for each regression found. - * Should have = C+ should have caught the bug by fully following the PR checklist. If C+ skips a step or completed the checklist incompletely, payment will be cut in half. -- C+ can also work on jobs as a contributor +- C+ can also work on jobs as a contributor. - Earning potential is variable, it depends on how much a C+ wants to work and other jobs they’re hired for. We’ve seen C+ make ~$100k/year. - There isn’t a set number of hours a C+ needs to work in a week. Proposals and PRs reviews are expected to be addressed within 24 hours on weekdays. - Dedicated #contributor-plus Slack room to discuss issues, processes and proposals. diff --git a/contributingGuides/HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md b/contributingGuides/HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md index a62939aebc25..0f90d71d52ea 100644 --- a/contributingGuides/HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md +++ b/contributingGuides/HOW_TO_BUILD_APP_ON_PHYSICAL_IOS_DEVICE.md @@ -11,7 +11,7 @@ > [!Important] > You must have a Apple Developer account to run your app on a physical device. If you don't have one, you can register here: [Apple Developer Program](https://developer.apple.com/). - 2.1. Go to `Signing and Capabilities` then in the section called `Signing (Debug/Development and Release/Development)` + 2.1. Go to `Signing and Capabilities`, then in the section called `Signing (Debug/Development and Release/Development)` ![Step 2.1 Screenshot](https://github.com/Expensify/App/assets/104348397/4c668612-ab29-4a91-8e2d-a146e2940017) @@ -24,7 +24,7 @@ ![Step 2.4 Screenshot](https://github.com/Expensify/App/assets/104348397/4ce3f250-4b7c-4e7c-9f1d-09df7bdfc5e0) > [!Note] ->Please be aware that the app built with your own bundle id doesn't support authenticated services like push notification, apple signin, deeplinking etc. which should be only available in Expensify developer account. +>Please be aware that the app built with your own bundle id doesn't support authenticated services like push notification, Apple signin, deeplinking etc. which should be only available in Expensify developer account. 2.5. Scroll down and Remove Associated Domains, Communication Notifications, Push Notifications, and Sign In With Apple capabilities diff --git a/contributingGuides/HOW_TO_CREATE_A_PLAN.md b/contributingGuides/HOW_TO_CREATE_A_PLAN.md index 28ebf1502e71..4f1b2e78a650 100644 --- a/contributingGuides/HOW_TO_CREATE_A_PLAN.md +++ b/contributingGuides/HOW_TO_CREATE_A_PLAN.md @@ -34,7 +34,7 @@ Once you have your solutions, it’s time to decide on the preferred solution (a - Future-proofing - does one solution have more longevity or pave the way for future development? - Independence - does a solution rely on a different problem to be solved first? Does it rely on another piece to be done later? -If you are finding the solution to be difficult, go back and beat harder on the problem to break it up into smaller pieces. Keep repeating until you have a general list of prioritized stages, with early stages solving the dependencies required by later stages, each of which is extremely well defined, with a reasonably obvious preferred solution. +If you are finding the solution to be difficult, go back and beat harder on the problem to break it up into smaller pieces. Keep repeating until you have a general list of prioritized stages, with early stages solving the dependencies required by later stages, each of which is extremely well-defined, with a reasonably obvious preferred solution. ## Step 3: Write out each problem and solution (P/S statement) Have a trusted peer or two proof your P/S statement and help you ensure it is well-defined. If you're in need of a peer to proof, post in #expensify-open-source to ask for help. Refine it and then share with another peer or two until you have a clear, understandable P/S statement. The more complex the problem and solution, the more people should review it. Keep going back to step 1 if needed. diff --git a/contributingGuides/KSv2.md b/contributingGuides/KSv2.md index 881d191ad886..44b1f5b36fe8 100644 --- a/contributingGuides/KSv2.md +++ b/contributingGuides/KSv2.md @@ -24,13 +24,13 @@ In the dashboard, you can first see the PRs assigned to you as `Reviewer`. As pa ### Issues assigned to you -In the next section you can see all issues assigned to you, prioritized from most urgent (on the left) to least urgent (on the right). Issues will also change color depending on other factors - e.g. if they have "HOLD" in the title or if they have the `Overdue`, `Planning`, or `Waiting for copy` labels applied. +In the next section, you can see all issues assigned to you, prioritized from most urgent (on the left) to least urgent (on the right). Issues will also change color depending on other factors - e.g. if they have "HOLD" in the title or if they have the `Overdue`, `Planning`, or `Waiting for copy` labels applied. If a GitHub issue has the `Overdue` label, the text will be red. This means that the issue hasn't been updated in the amount of time allotted for an update (ex - A weekly issue becomes overdue if it hasn't been updated in a week). ### Your Pull Requests -After the issues section you will find a section that lists the PRs you've created. +After the issues section, you will find a section that lists the PRs you've created. diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 5bb6dfb85851..6c0a5b460654 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -4,13 +4,13 @@ The navigation in the App consists of a top-level Stack Navigator (called `RootS ## Terminology -`RHP` - Right hand panel that shows content inside a dismissible modal that takes up a partial portion of the screen on large format devices e.g. desktop/web/tablets. On smaller screens the content shown in the RHP fills the entire screen. +`RHP` - Right hand panel that shows content inside a dismissible modal that takes up a partial portion of the screen on large format devices e.g. desktop/web/tablets. On smaller screens, the content shown in the RHP fills the entire screen. Navigation Actions - User actions correspond to resulting navigation actions that we will define now. The navigation actions are: `Back`, `Up`, `Dismiss`, `Forward` and `Push`. - `Back` - Moves the user “back” in the history stack by popping the screen or stack of screens. Note: This can potentially make the user exit the app itself (native) or display a previous app (not Expensify), or just the empty state of the browser. -- `Up` - Pops the current screen off the current stack. This action is very easy to confuse with `Back`. Unless you’ve navigated from one screen to a nested screen in a stack of screens these actions will almost always be the same. Unlike a “back” action, “up” should never result in the user exiting the app and should only be an option if there is somewhere to go “up” to. +- `Up` - Pops the current screen off the current stack. This action is very easy to confuse with `Back`. Unless you’ve navigated from one screen to a nested screen in a stack of screens, these actions will almost always be the same. Unlike a “back” action, “up” should never result in the user exiting the app and should only be an option if there is somewhere to go “up” to. - `Dismiss` - Closes any modals (outside the navigation hierarchy) or pops a nested stack of screens off returning the user to the previous screen in the main stack. @@ -26,7 +26,7 @@ Most of the time, if you want to add some of the flows concerning one of your re - If you want to create new flow, add a `Screen` in `RightModalNavigator.tsx` and make new modal in `ModalStackNavigators.tsx` with chosen pages. -When creating RHP flows, you have to remember a couple things: +When creating RHP flows, you have to remember a couple of things: - Since you can deeplink to different pages inside the RHP navigator, it is important to provide the possibility for the user to properly navigate back from any page with UP press (`HeaderWithBackButton` component). @@ -87,7 +87,7 @@ Using [react-freeze](https://github.com/software-mansion/react-freeze) allows us - The wide layout is rendered with our custom `ThreePaneView.js` and the narrow layout is rendered with `StackView` from `@react-navigation/stack` -- To make sure that we have the correct navigation state after changing the layout we need to force react to create new instance of the `NavigationContainer`. Without this, the navigation state could be broken after changing the type of layout. +- To make sure that we have the correct navigation state after changing the layout, we need to force react to create new instance of the `NavigationContainer`. Without this, the navigation state could be broken after changing the type of layout. - We are getting the new instance by changing the `key` prop of `NavigationContainer` that depends on the `isSmallScreenWidth`. @@ -115,10 +115,11 @@ Broken behavior is the outcome of two things: The reason why `getActionFromState` provided by `react-navigation` is dispatched at the top level of the navigation hierarchy is that it doesn't know about current navigation state, only about desired one. -In this example it doesn't know if the `RightModalNavigator` and `Settings` are already mounted. +In this example, it doesn't know if the `RightModalNavigator` and `Settings` are already mounted. -The action for the first step looks like that: +The action for the first step looks like that: + ```json { "type": "NAVIGATE", @@ -193,7 +194,7 @@ If we can create simple action that will only push one screen to the existing na The `getMinimalAction` compares action generated by the `getActionFromState` with the current navigation state and tries to find the smallest action possible. -The action for the first step created with `getMinimalAction` looks like this: +The action for the first step created with `getMinimalAction` looks like this: ```json { diff --git a/contributingGuides/OFFLINE_UX.md b/contributingGuides/OFFLINE_UX.md index cd45bebdce4b..9fefbaeea111 100644 --- a/contributingGuides/OFFLINE_UX.md +++ b/contributingGuides/OFFLINE_UX.md @@ -60,7 +60,7 @@ This is the pattern where we queue the request to be sent when the user is onlin - the user should be given instant feedback and - the user does not need to know when the change is done on the server in the background -**How to implement:** Use [`API.write()`](https://github.com/Expensify/App/blob/3493f3ca3a1dc6cdbf9cb8bd342866fcaf45cf1d/src/libs/API.js#L7-L28) to implement this pattern. For this pattern we should only put `optimisticData` in the options. We don't need `successData` or `failureData` as we don't care what response comes back at all. +**How to implement:** Use [`API.write()`](https://github.com/Expensify/App/blob/3493f3ca3a1dc6cdbf9cb8bd342866fcaf45cf1d/src/libs/API.js#L7-L28) to implement this pattern. For this pattern, we should only put `optimisticData` in the options. We don't need `successData` or `failureData` as we don't care what response comes back at all. **Example:** Pinning a chat. @@ -77,7 +77,7 @@ When the user is offline: **How to implement:** - Use API.write() to implement this pattern - Optimistic data should include `pendingAction` ([with these possible values](https://github.com/Expensify/App/blob/15f7fa622805ee2971808d6bc67181c4715f0c62/src/CONST.js#L775-L779)) -- To ensure the UI is shown as described above, you should enclose the components that contain the data that was added/updated/deleted with the OfflineWithFeedback component +- To ensure the UI is shown as described above, you should enclose the components that contain the data that was added/updated/deleted with the `OfflineWithFeedback` component - Include this data in the action call: - `optimisticData` - always include this object when using the Pattern B - `successData` - include this if the action is `update` or `delete`. You do not have to include this if the action is `add` (same data was already passed using the `optimisticData` object) @@ -86,9 +86,9 @@ When the user is offline: **Handling errors:** - The [OfflineWithFeedback component](https://github.com/Expensify/App/blob/main/src/components/OfflineWithFeedback.js) already handles showing errors too, as long as you pass the error field in the [errors prop](https://github.com/Expensify/App/blob/128ea378f2e1418140325c02f0b894ee60a8e53f/src/components/OfflineWithFeedback.js#L29-L31) -- When dismissing the error, the onClose prop will be called, there we need to call an action that either: +- When dismissing the error, the `onClose` prop will be called, there we need to call an action that either: - If the pendingAction was `delete`, it removes the data altogether - - Otherwise, it would clear the errors and pendingAction properties from the data + - Otherwise, it would clear the errors and `pendingAction` properties from the data - We also need to show a Red Brick Road (RBR) guiding the user to the error. We need to manually do this for each piece of data using pattern B Optimistic WITH Feedback. Some common components like `MenuItem` already have a prop for it (`brickRoadIndicator`) - A Brick Road is the pattern of guiding members towards places that require their attention by following a series of UI elements that have the same color diff --git a/contributingGuides/PERFORMANCE.md b/contributingGuides/PERFORMANCE.md index 0e8ee14d70a4..1942c97af913 100644 --- a/contributingGuides/PERFORMANCE.md +++ b/contributingGuides/PERFORMANCE.md @@ -4,7 +4,7 @@ - Use [`PureComponent`](https://reactjs.org/docs/react-api.html#reactpurecomponent), [`React.memo()`](https://reactjs.org/docs/react-api.html#reactmemo), and [`shouldComponentUpdate()`](https://reactjs.org/docs/react-component.html#shouldcomponentupdate) to prevent re-rendering expensive components. - Using a combination of [React DevTools Profiler](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) and [Chrome Dev Tools Performance Timing](https://calibreapp.com/blog/react-performance-profiling-optimization) can help identify unnecessary re-renders. Both tools can be used to time an interaction like the app starting up or navigating to a new screen. - Watch out for [very large lists](https://reactnative.dev/docs/optimizing-flatlist-configuration) and things like `Image` components re-fetching images on render when a remote uri did not change. -- Avoid the temptation to over-optimize. There is added cost in both code complexity and performance when adding checks like `shouldComponentUpdate()`. Be selective about when you use this and make sure there is a measureable difference before proposing the change. As a very general rule it should be measurably faster to run logic to avoid the re-render (e.g. do a deep comparison) than it would be to let React take care of it without any extra intervention from us. +- Avoid the temptation to over-optimize. There is added cost in both code complexity and performance when adding checks like `shouldComponentUpdate()`. Be selective about when you use this and make sure there is a measureable difference before proposing the change. As a very general rule, it should be measurably faster to run logic to avoid the re-render (e.g. do a deep comparison) than it would be to let React take care of it without any extra intervention from us. - Use caution when adding subscriptions that might re-render very large trees of components e.g. subscribing to state that changes often (current report, current route, etc) in the app root. - Avoid using arrow function callbacks in components that are expensive to re-render. React will re-render this component since each time the parent renders it creates a new instance of that function. **Alternative:** Bind the method in the constructor instead. @@ -22,12 +22,12 @@ It's possible, but slightly trickier to profile the JS running on Android devices as it does not run in a browser but a JS VM that React Native must spin up first then run the app code. The VM we are currently using on both Android and iOS is called [Hermes](https://reactnative.dev/docs/profile-hermes) and is developed by Facebook. -In order to profile with Hermes follow these steps: +In order to profile with Hermes, follow these steps: -- In the metro bundler window press `d` on your keyboard to bring up the developer menu on your device. +- In the metro bundler window, press `d` on your keyboard to bring up the developer menu on your device. - Select "Settings" - Select "Start Sampling Profiler on Init" -- In metro bundler refresh by pressing r +- In metro bundler, refresh by pressing r - The app will start up and a profile will begin - Once the app loads take whatever action you want to profile - Press `d` again and select "Disable Sampling Profiler" @@ -52,7 +52,7 @@ In order to profile with Hermes follow these steps: ### Performance Metrics (Opt-In on local release builds) -To capture reliable performance metrics for native app launch we must test against a release build. To make this easier for everyone to do we created an opt-in tool (using [`react-native-performance`](https://github.com/oblador/react-native-performance) that will capture metrics and display them in an alert once the app becomes interactive. To set this up just set `CAPTURE_METRICS=true` in your `.env` file then create a release build on iOS or Android. The metrics this tool shows are as follows: +To capture reliable performance metrics for native app launch, we must test against a release build. To make this easier for everyone to do, we created an opt-in tool (using [`react-native-performance`](https://github.com/oblador/react-native-performance)) that will capture metrics and display them in an alert once the app becomes interactive. To set this up, just set `CAPTURE_METRICS=true` in your `.env` file, then create a release build on iOS or Android. The metrics this tool shows are as follows: - `nativeLaunch` - Total time for the native process to initialize - `runJSBundle` - Total time to parse and execute the JS bundle @@ -73,22 +73,23 @@ signingConfigs { keyAlias 'your_key_alias' keyPassword 'Password1' } +} ``` - Delete any existing apps off emulator or device - Run `react-native run-android --variant release` ## Reconciliation -React is pretty smart and in many cases is able to tell if something needs to update. The process by which React goes about updating the "tree" or view heirarchy is called reconciliation. If React thinks something needs to update it will render it again. React also assumes that if a parent component rendered then it's child should also re-render. +React is pretty smart and in many cases is able to tell if something needs to update. The process by which React goes about updating the "tree" or view hierarchy is called reconciliation. If React thinks something needs to update, it will render it again. React also assumes that if a parent component rendered, then its child should also re-render. -Re-rendering can be expensive at times and when dealing with nested props or state React may render when it doesn't need to which can be wasteful. A good example of this is a component that is being passed an object as a prop. Let's say the component only requires one or two properties from that object in order to build it's view, but doesn't care about some others. React will still re-render that component even if nothing it cares about has changed. Most of the time this is fine since reconciliation is pretty fast. But we might run into performance issues when re-rendering massive lists. +Re-rendering can be expensive at times and when dealing with nested props or state React may render when it doesn't need to which can be wasteful. A good example of this is a component that is being passed an object as a prop. Let's say the component only requires one or two properties from that object in order to build its view, but doesn't care about some others. React will still re-render that component even if nothing it cares about has changed. Most of the time this is fine since reconciliation is pretty fast. But we might run into performance issues when re-rendering massive lists. In this example, the most preferable solution would be to **only pass the properties that the object needs to know about** to the component in the first place. Another option would be to use `shouldComponentUpdate` or `React.memo()` to add more specific rules comparing `props` to **explicitly tell React not to perform a re-render**. -React might still take some time to re-render a component when it's parent component renders. If it takes a long time to re-render the child even though we have no props changing then we can use `PureComponent` or `React.memo()` (without a callback) which will "shallow compare" the `props` to see if a component should re-render. +React might still take some time to re-render a component when its parent component renders. If it takes a long time to re-render the child even though we have no props changing, then we can use `PureComponent` or `React.memo()` (without a callback) which will "shallow compare" the `props` to see if a component should re-render. -If you aren't sure what exactly is changing about some deeply nested object prop you can use `Performance.diffObject()` method in `componentDidUpdate()` which should show you exactly what is changing from one update to the next. +If you aren't sure what exactly is changing about some deeply nested object prop, you can use `Performance.diffObject()` method in `componentDidUpdate()` which should show you exactly what is changing from one update to the next. **Suggested:** [React Docs - Reconciliation](https://reactjs.org/docs/reconciliation.html) diff --git a/contributingGuides/PROPOSAL_TEMPLATE.md b/contributingGuides/PROPOSAL_TEMPLATE.md index 53330dfe96c9..8c9fa7968fe2 100644 --- a/contributingGuides/PROPOSAL_TEMPLATE.md +++ b/contributingGuides/PROPOSAL_TEMPLATE.md @@ -14,13 +14,13 @@ - -# About -## Overview - - -This document explains how to manage employee expense reports and approval workflows in Expensify. - - -### Approval workflow modes - - -#### Submit and close -- This is a workflow where no approval occurs in Expensify. -- *What happens after submission?* The report state becomes Closed and is available to view by the member set in Submit reports to and any Workspace Admins. -- *Who should use this workflow?* This mode should be used where you don't require approvals in Expensify. - - -#### Submit and approve -- *Submit and approve* is a workflow where all reports are submitted to a single member for approval. New policies have Submit and Approve enabled by default. -- *What happens after submission?* The report state becomes Processing and it will be sent to the member indicated in Submit reports to for approval. When the member approves the report, the state will become Approved. -- *Who should use this workflow?* This mode should be used where the same person is responsible for approving all reports for your organization. If submitters have different approvers or multiple levels of approval are required, then you will need to use Advance Approval. - - -#### Advanced Approval -- This approval mode is used to handle more complex workflows, including: - - *Multiple levels of approval.* This is for companies that require more than one person to approve a report before it can be reimbursed. The most common scenario is when an employee needs to submit to their manager, and their manager needs to approve and forward that report to their finance department for final approval. - - *Varying approval workflows.* For example, if a company has Team A submitting reports to Manager A, and Team B to Manager B, use Advanced Approval. Group Workspace Admins can also set amount thresholds in the case that a report needs to go to a different approver based on the amount. -- *What happens after submission?* After the report is submitted, it will follow the set approval chain. The report state will be Processing until it is Final Approved. We have provided examples of how to set this up below. -- *Who should use this workflow?* Organizations with complex workflows or 2+ levels of approval. This could be based on manager approvals or where reports over a certain size require additional approvals. -- *For further automation:* use Concierge auto-approval for reports. You can set specific rules and guidelines in your Group Workspace for your team's expenses; if all expenses are below the Manual Approval Threshold and adhere to all the rules, then we will automatically approve these reports on behalf of the approver right after they are submitted. - - -### How to set an approval workflow - -- Step-by-step instructions on how to set this up at the Workspace level [here](link-to-instructions). - -# Deep Dive - -### Setting multiple levels of approval -- 'Submits to' is different than 'Approves to'. - - *Submits to* - is the person you are sending your reports to for 1st level approval - - *Approves to* - is the person you are sending the reports you've approved for higher-level approval -- In the example below, a report needs to be approved by multiple managers: *Submitter > Manager > Director > Finance/Accountant* - - *Submitter (aka. Employee):* This is the person listed under the member column of the People page. - - *First Approver (Manager):* This is the person listed under the Submits to column of the People Page. - - *Second Approver (Director):* This is the person listed as 'Approves to' in the Settings of the First Approver. - - *Final Approver (Finance/Accountant):* This is the person listed as the 'Approves to' in the Settings of the Second Approver. -- This is what this setup looks like in the Workspace Members table. - - Bryan submits his reports to Jim for 1st level approval. -![Screenshot showing the People section of the workspace]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_1.png){:width="100%"} - - - All of the reports Jim approves are submitted to Kevin. Kevin is the 'approves to' in Jim's Settings. -![Screenshot of Policy Member Editor]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_2.png){:width="100%"} - - - All of the reports Kevin approves are submitted to Lucy. Lucy is the 'approves to' in Kevin's Settings. -![Screenshot of Policy Member Editor Approves to]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_3.png){:width="100%"} - - - - Lucy is the final approver, so she doesn't submit her reports to anyone for review. -![Screenshot of Policy Member Editor Final Approver]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_4.png){:width="100%"} - - -- The final outcome: The member in the Submits To line is different than the person noted as the Approves To. -### Adding additional approver levels -- You can also set a specific approver for Reports Totals in Settings. -![Screenshot of Policy Member Editor Approves to]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_5.png){:width="100%"} - -- An example: The submitter's manager can approve any report up to a certain limit, let's say $500, and forward it to accounting. However, if a report is over that $500 limit, it has to be also approved by the department head before being forwarded to accounting. -- To configure, click on Edit Settings next to the approving manager's email address and set the "If Report Total is Over" and "Then Approves to" fields. -![Screenshot of Workspace Member Settings]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_6.png){:width="100%"} -![Screenshot of Policy Member Editor Configure]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_7.png){:width="100%"} - - -### Setting category approvals -- If your expense reports should be reviewed by an additional approver based on specific categories or tags selected on the expenses within the report, set up category approvers and tag approvers. -- Category approvers can be set in the Category settings for each Workspace -- Tag approvers can be set in the Tag settings for each Workspace - - -#### Category approver -- A category approver is a member who is added to the approval workflow for any reports in your Expensify Workspace that contain expenses with a specific category. -- For example: Your HR director Jim may need to approve any relocation expenses submitted by employees. Set Jim up as the category approver for your Relocation category, then any reports containing Relocation expenses will first be routed to Jim before continuing through the approval workflow. -- Adding category approvers - - To add a category approver in your Workspace: - - Navigate to *Settings > Policies > Group > [Workspace Name] > Categories* - - Click *"Edit Settings"* next to the category that requires the additional approver - - Select an approver and click *"Save"* - - -#### Tag approver -- A tag approver is a member who is added to the approval workflow for any reports in your Expensify Workspace that contain expenses with a specific tag. -- For example: If employees must tag project-based expenses with the corresponding project tag. Pam, the project manager is set as the project approver for that project, then any reports containing expenses with that project tag will first be routed to Pam for approval before continuing through the approval workflow. -- Please note: Tag approvers are only supported for a single level of tags, not for multi-level tags. The order in which the report is sent to tag approvers relies on the date of the expense. -- Adding tag approvers - - To add a tag approver in your Workspace: - - Navigate to *Settings > Policies > Group > [Workspace Name] > Tags* - - Click in the "Approver" column next to the tag that requires an additional approver - - -Category and Tag approvers are inserted at the beginning of the approval workflow already set on the People page. This means the workflow will look something like: * *Submitter > Category Approver(s) > Tag Approver(s) > Submits To > Previous approver's Approves To.* - - -### Workflow enforcement -- If you want to ensure your employees cannot override the workflow you set - enable workflow enforcement. As a Workspace Admin, you can choose to enforce your approval workflow by going to Settings > Workspaces > Group > [Workspace Name] > People > Approval Mode. When enabled (which is the default setting for a new workspace), submitters and approvers must adhere to the set approval workflow (recommended). This setting does not apply to Workspace Admins, who are free to submit outside of this workflow diff --git a/docs/articles/expensify-classic/expenses/expenses/Add-an-expense.md b/docs/articles/expensify-classic/expenses/Add-an-expense.md similarity index 100% rename from docs/articles/expensify-classic/expenses/expenses/Add-an-expense.md rename to docs/articles/expensify-classic/expenses/Add-an-expense.md diff --git a/docs/articles/expensify-classic/expenses/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/Apply-Tax.md similarity index 100% rename from docs/articles/expensify-classic/expenses/expenses/Apply-Tax.md rename to docs/articles/expensify-classic/expenses/Apply-Tax.md diff --git a/docs/articles/expensify-classic/expenses/expenses/Merge-expenses.md b/docs/articles/expensify-classic/expenses/Merge-expenses.md similarity index 100% rename from docs/articles/expensify-classic/expenses/expenses/Merge-expenses.md rename to docs/articles/expensify-classic/expenses/Merge-expenses.md diff --git a/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md b/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md deleted file mode 100644 index e8dfdbf44bcb..000000000000 --- a/docs/articles/expensify-classic/expenses/reports/Create-A-Report.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Create a Report -description: Learn how to create and edit reports in Expensify ---- - - -# Overview - -This article covers all the basics of creating, editing, deleting and managing your reports. - -# How to create a report - -_Using the web app:_ - -To create a report on the Expensify website, click the New Report button on the **Reports** page. - -_Using the mobile app:_ - -Tap the ☰ icon. -Tap **Reports**. -Tap the **+** icon. -Choose your desired report type. - -# How to edit a report - -## Adding expenses to a report - -You can add expenses to the report by clicking **Add Expenses** at the top of the report. - -## Removing expenses from a report on the Expensify web app - -To remove expenses from the report on the web app, click the red ❌ next to the expense. - -## Removing expenses from a report on the Expensify mobile app - -To remove an expense on an Android device, hold the expense and tap **Delete**. - -To remove an expense on an iOS device, swipe the expense to the left and tap **Delete**. - -## Editing the report title - -To edit the report title, click the pencil icon next to the name. To save your changes, tap the enter key on your keyboard. - -**Note:** You may be unable to edit your reports' titles based on the settings. - -## Bulk-editing expenses on a report - -Click Details in the top-right of the report on the web app, then click the pencil icon to bring up the editing modal. You can click the pencil icon to the left of an expense to edit it, or you can edit multiple expenses at once by ticking the checkbox of the expenses you’d like to bulk-edit and then clicking **Edit Multiple** at the top of the modal. - -## Commenting on the report - -You can comment on the report by adding your comment to the **Report Comments** section at the bottom. Expensify will also log report actions here. - -## Attachments - -If you’d like to attach a photo or document to the report, follow the instructions below to add the attachment to your report comment section. - -_Using the web app:_ - -1. Click the **Paperclip** icon in the comment box of the **Report Comments** section. -2. Select the file to attach. -3. Check the preview of the attachment and click Upload. - -_Using the mobile app:_ - -1. Tap into the report. -2. Scroll to the bottom of the report and tap the paper clip icon to attach a file. - -**Note:** Report comments support jpeg, jpg, png, gif, csv, and pdf files. - -## Changing the report's workspace - -To change the report's workspace, click **Details** in the top-right of the report on the web app, then select the correct workspace from the **Workspace** drop-down. - -## Changing the report type (Expense Report/Invoice) - -To change the report type, click **Details** in the top-right of the report on the web app, then select the correct report type from the **Type** drop-down. - -## Changing the layout of the report - -There are three ways you can change the report layout under the Details section of the report. To do this, select the desired layout from the relevant drop-down menu: - - - **View** - Choose between a Basic or Detailed report view. - - **Group By** - Choose to group expenses on the report based on their Category or Tag. - - **Split By** - Split out the expenses based on their Reimbursable or Billable status. - -# How to submit a report - -1. Click **Submit** in the top-left of the report (or **Submit Report** at the top in the mobile app). -2. Verify the approver and click **Submit** again. - -# How to retract your report (Undo Submit) - -You can edit expenses on a report in a **Processing** state so long as it hasn't been approved yet. If a report has been through a level of approval and is still in the **Processing** state, you can retract this submission to put the report back to Draft status to make corrections and re-submit. - -To retract a **Processing** report on the web app, click the Undo Submit button at the upper left-hand corner of the report. - -To complete this from the mobile app, simply open the report from within your app and click the **Retract** button at the top of the report. - -# How to share a report - -Click Details in the top-right of the report on the web app to bring up the sharing settings. The following options are available: - - - Click the **Printer** icon to print the report. - - Click the **Download** icon to download a PDF of the report - - Click the **Share** icon to share the report via email or SMS. - -# How to close a report - -You can close your report if you don't need it approved by your employer. - -_To close a report on the Expensify website:_ - -1. Navigate to the report in question. -2. Click **Mark as Closed** at the top of the report. -3. You can re-open a report once it’s closed by clicking **Undo Close** at the top of the report. - -# How to delete a report - -_Deleting a report on the web app:_ - -Click Details in the top-right of the report on the web app, then click the Trash icon to delete the report. Any expenses on the report will move to an Unreported state. - -_Deleting a report on the mobile app:_ - -To delete a Draft report on an Android, press and hold the report name and tap **Delete**. - -To delete a Draft report on an iOS device, go to the **Reports** screen, swipe the report to the left, and tap **Delete**. - -_Deleting a report in the Processing, Approved, Reimbursed or Closed state:_ - -If you want to delete a Processing or Closed report, please follow the How to undo your report submission instructions in this article to move the report back into an Draft status, then follow the steps above. - -If you want to delete an Approved or Reimbursed report, please speak to your Company Admin as this may not be possible. - -# How to move expenses between reports - -Navigate to your Expenses page. -Tick the checkbox next to each expense you'd like to move. -Click the Add To Report button in the top right corner. -Select your desired report from the drop-down. - -# How to use Guided Review to clean up your report - -Open your report on the web app and click Review at the top. The system will walk you through each violation on the report. -As you go through each violation, click View to look at the expense in more detail or resolve any violations. -Click Next to move on to the next item. -Click Finish to complete the review process when you’re done. - -{% include faq-begin.md %} - -## Is there a difference between Expense Reports, Bills, and Invoices? - -**Expense Reports** are submitted by an employee to their employer. They contain either personally incurred expenses that the employee should be reimbursed for, or non-reimbursable expenses (such as company card expenses) incurred by the employee that require tracking for accounting purposes. - -**Invoices** are reports that a business or contractor will send to another business to charge them for goods or services the business received. Each invoice will have a matching **Bill** owned by the recipient so they may use it to pay the invoice sender. - -## Which report type should I use? - -If you bought something on a company card or need to be reimbursed by your employer, you’ll need an **Expense Report**. - -If someone external to the business sends you an invoice for their services, you’ll want a **Bill** (or even better - use our Bill Pay process) - -## When should I submit my report? - -Your Company Admin can answer this one, and they may have configured the workspace’s [Scheduled Submit] setting to enforce a regular cadence for you. If not, you can still set this up under your [Individual workspace]. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md index 3fe5ec41f5f6..a998e279c3f6 100644 --- a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md +++ b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md @@ -15,26 +15,28 @@ Each month, you’ll be billed for the amount of members you originally set in y For example, let’s say you set your annual subscription size at 10 members and you’re on the Control plan. You’ll be billed $18/member for 10 members each month. However, let’s say in one particular month you had 12 active members, you’d be billed at $18/member for the 10 members in your subscription size + $36/member (pay-per-use rate) for the additional 2 active members. -You can always increase your annual subscription size, which will extend your annual subscription length. You cannot reduce your annual subscription size until your current subscription has ended. If you have any questions about this, reach out to Concierge or your account manager. +You can always increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. If you have any questions about this, contact Concierge or your account manager. ## Pay-per-use -The pay-per-use rate is the full rate per active member without any discounts. The pay-per-use rate for each member on the Collect plan is $20 and on the Control plan is $36. +The pay-per-use rate is the full rate per active member without any discounts. The pay-per-use rate for each member on the Collect plan is $20, and on the Control plan, it is $36. ## How the Expensify Card can reduce your bill Bundling the Expensify Card with an annual subscription ensures you pay the lowest possible monthly price for Expensify. And the more you spend on Expensify Cards, the lower your bill will be. If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per member. This additional 50% discount, when coupled with an annual subscription, brings the price per member to $5 on a Collect plan and $9 on a Control plan. -Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more (_applies to USD purchases only_). Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. +Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more (_applies to US purchases only_). Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. ## Savings calculator To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save! + {% include faq-begin.md %} + ## What if we put less than 50% of our total spend on the Expensify Card? -If you put less than 50% of your total USD spend on your Expensify Card, your bill gets discounted on a sliding scale based on the percentage of use. So if you don't use the Expensify Card at all, you'll be charged the full rate for each member based on your plan and subscription. -Example: +If less than 50% of your total USD spend is on the Expensify Card, the bill is discounted on a sliding scale. + +**Example:** - Annual subscription discount: 50% -- % of Expensify Card spend (USD) across all workspaces: 20% +- % of Expensify Card spend (US purchases only) across all workspaces: 20% - Expensify Card discount: 20% -You save 70% on the price per member on your bill for that month. -Note: USD spend refers to approved USD transactions on the Expensify Card in any given month, compared to all approved USD spend on workspaces in that same month. +In that case, you'd save 70% on the price per member for that month's bill. {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md new file mode 100644 index 000000000000..c5578249289a --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md @@ -0,0 +1,86 @@ +--- +title: Unlimited Virtual Cards +description: Learn more about virtual cards and how they can help your business gain efficiency and insight into company spending. +--- + +# Overview + +For admins to issue virtual cards, your company **must upgrade to Expensify’s new Expensify Visa® Commercial Card.** + +Once upgraded to the new Expensify Card, admins can issue an unlimited number of virtual cards with a fixed or monthly limit for specific company purchases or recurring subscription payments _(e.g., Marketing purchases, Advertising, Travel, Amazon Web Services, etc.)._ + +This feature supports businesses that require tighter controls on company spending, allowing customers to set fixed or monthly spending limits for each virtual card. + +Use virtual cards if your company needs or wants: + +- To use one card per vendor or subscription, +- To issue cards for one-time purchases with a fixed amount, +- To issue cards for events or trips, +- To issue cards with a low limit that renews monthly, + +Admins can also name each virtual card, making it easy to categorize and assign them to specific accounts upon creation. Naming the card ensures a clear and organized overview of expenses within the Expensify platform. + +# How to set up virtual cards + +After adopting the new Expensify Card, domain admins can issue virtual cards to any employee using an email matching your domain. Once created and assigned, the card will be visible under the name given to the card. + +**To assign a virtual card:** + +1. Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards). +2. Click the **Issue Virtual Cards** button. +3. Enter a card name (i.e., "Google Ads"). +4. Select a domain member to assign the card to. +5. Enter a card limit. +6. Select a **Limit Type** of _Fixed_ or _Monthly_. +7. Click **Issue Card**. + +![The Issue Virtual Cards modal is open in the middle of the screen. There are four options to set; Card Name, Assignee, Card Limit, and Limit type. A cancel (left) and save (right) button are at the bottom right of the modal.]({{site.url}}/assets/images/AdminissuedVirtualCards.png){:width="100%"} + +# How to edit virtual cards + +Domain admin can update the details of a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page. + +**To edit a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Change the editable details. +3. Click **Edit Card** to save the changes. + +# How to terminate a virtual card + +Domain admin can also terminate a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page by setting the limit to $0. + +**To terminate a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Set the limit to $0. +3. Click **Save**. +4. Refresh your web page, and the card will be removed from the list. + +{% include faq-begin.md %} + +**What is the difference between a fixed limit and a monthly limit?** + +There are two different limit types that are best suited for their intended purpose. + +- _Fixed limit_ spend cards are ideal for one-time expenses or providing employees access to a card for a designated purchase. +- _Monthly_ limit spend cards are perfect for managing recurring expenses such as subscriptions and memberships. + +**Where can employees see their virtual cards?** + +Employees can see their assigned virtual cards by navigating to **Settings** > **Account** > [**Credit Cards Import**](https://www.expensify.com/settings?param=%7B%22section%22:%22creditcards%22%7D) in their account. + +On this page, employees can see the remaining card limit, the type of card it is (i.e., fixed or monthly), and view the name given to the card. + +When the employee needs to use the card, they’ll click the **Show Details** button to expose the card details for making purchases. + +_Note: If the employee doesn’t have Two-Factor Authentication (2FA) enabled when they display the card details, they’ll be prompted to enable it. Enabling 2FA for their account provides the best protection from fraud and is **required** to dispute virtual card expenses._ + +**What do I do when there is fraud on one of our virtual cards?** + +If you or an employee loses their virtual card, experiences fraud, or suspects the card details are no longer secure, please [request a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction) immediately. A domain admin can also set the limit for the card to $0 to terminate the specific card immediately if the employee cannot take action. + +When the employee requests a new card, the compromised card will be terminated immediately. This is best practice for any Expensify Card and if fraud is suspected, action should be taken as soon as possible to reduce financial impact on the company. + +{% include faq-end.md %} + diff --git a/docs/articles/new-expensify/expenses/Referral-Program.md b/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md similarity index 100% rename from docs/articles/new-expensify/expenses/Referral-Program.md rename to docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md diff --git a/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md new file mode 100644 index 000000000000..9467c07d95ba --- /dev/null +++ b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md @@ -0,0 +1,35 @@ +--- +title: Assign tag and category approvers +description: Require an approver for expenses coded with a specific tag or category +--- +
+ +Once your workplace has created tags and categories, approvers can be assigned to them. Tag and category approvers are automatically added to the report approval workflow when a submitted expense contains a specific tag or category. + +For example, if all employees are required to tag project-based expenses with a tag for the project, you can assign the project manager as the approver for that tag. This way, when a report is submitted containing expenses with that project tag, it will first be routed to the project manager for approval before continuing through the rest of the approval workflow. + +If a report contains multiple categories or tags that each require a different reviewer, then each reviewer must review the report before it can be submitted. The report will first go to the category approvers, the tag approvers, and then the approvers assigned in the approval workflow. + +# Assign category approvers + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Categories** tab on the left. +5. Locate the category in the list of categories and click **Edit**. +6. Click the Approver field to select an approver. +7. Click **Save**. + +# Assign tag approvers + +{% include info.html %} +Tag approvers are only supported for a single level of tags, not for multi-level tags. +{% include end-info.html %} + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Tags** tab on the left. +5. Locate the tag in the list of tags and click the Approver field to assign an approver. + +
diff --git a/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md new file mode 100644 index 000000000000..49b5bd522464 --- /dev/null +++ b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md @@ -0,0 +1,41 @@ +--- +title: Automatically submit employee reports +description: Use Expensify's Scheduled Submit feature to have your employees' expenses submitted automatically for them +--- +
+ +Scheduled Submit automatically adds expenses to a report and sends them for approval so that your employees do not have to remember to manually submit their reports each week. This allows you to automatically collect employee expenses on a schedule of your choosing. + +With Scheduled Submit, an employee's expenses are automatically gathered onto a report as soon as they create them. If there is not an existing report, a new one is created. The report is then automatically submitted at the cadence you choose—daily, weekly, monthly, twice per month, or by trip. + +{% include info.html %} +If an expense has a violation, Scheduled Submit will not automatically submit it until the violations are corrected. In the meantime, the expense will be removed from the report and added to an open report. +{% include end-info.html %} + +# Enable Scheduled Submit + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left (or click the Individual tab to enable Scheduled Submit for your individual workspace). +3. Click the desired workspace name. +4. Click the **Reports** tab on the left. +5. Click the Scheduled Submit toggle to enable it. +6. Click the “How often expenses submit” dropdown and select the submission schedule: + - **Daily**: Expenses are submitted every evening. Expenses with violations are submitted the day after the violations are corrected. + - **Weekly**: Expenses are submitted once a week. Expenses with violations are submitted the following Sunday after the violations are corrected. + - **Twice a month**: Expenses are submitted on the 15th and the last day of each month. Expenses with violations are submitted at the next cycle (either on the 15th or the last day of the month, whichever is closest). + - **Monthly**: Expenses are submitted once per month. If you select Monthly, you will also select which day of the month the reports will be submitted. Expenses with violations are submitted on the next monthly submission date. + - **By trip**: All expenses that occur in a similar time frame are grouped together. The trip report is created after no new expenses have been submitted for two calendar days. Then the report is submitted the second day, and any new expenses are added to a new trip report. + - **Manually**: Expenses are automatically added to an open report, but the report will require manual submission—it will not be submitted automatically. This is a great option for automatically gathering an employee’s expenses on a report while still requiring the employee to review and submit their report. + +{% include info.html %} +- All submission times are in the evening PDT. +- If you enable Scheduled Submit for your individual workspace and one of your group workspaces also has Scheduled Submit enabled, the group’s submission settings will override your individual workspace settings. +{% include end-info.html %} + +# FAQs + +**I disabled Scheduled Submit. Why do I still get reports submitted by Concierge?** + +Although an Admin can disable scheduled submit for a workspace, employees have the ability to activate schedule submit for their account. If you disable Scheduled Submit but still receive reports from Concierge, the employee has Schedule Submit activated for their individual workspace. + +
diff --git a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md index 857217189e50..aa5aea545a23 100644 --- a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md +++ b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md @@ -60,6 +60,8 @@ You can retract a submitted report to edit the reported expenses and re-submit t 4. Tap **Retract** at the top of the report. {% include end-option.html %} +**Note:** Workspaces with Instant Submit set as the Scheduled Submit frequency won’t have the option to Retract entire reports, only individual expenses. + {% include end-selector.html %} diff --git a/docs/articles/expensify-classic/settings/Close-or-reopen-account.md b/docs/articles/expensify-classic/settings/account-settings/Close-or-reopen-account.md similarity index 100% rename from docs/articles/expensify-classic/settings/Close-or-reopen-account.md rename to docs/articles/expensify-classic/settings/account-settings/Close-or-reopen-account.md diff --git a/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md b/docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md similarity index 99% rename from docs/articles/expensify-classic/settings/Notification-Troubleshooting.md rename to docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md index 22df0dc7f6ca..0e18d6f22cf5 100644 --- a/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md +++ b/docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md @@ -1,5 +1,5 @@ --- -title: Notification Troubleshooting +title: Set notifications description: This article is about how to troubleshoot notifications from Expensify. --- diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md deleted file mode 100644 index 2d561ea598d9..000000000000 --- a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Set notifications -description: Select your Expensify notification preferences ---- -
- -{% include info.html %} -This process is currently not available from the mobile app and must be completed from the Expensify website. -{% include end-info.html %} - -1. Hover over Settings and click **Account**. -2. Click the **Preferences** tab on the left. -3. Scroll down to the Contact Preferences section. -4. Select the checkbox for the types of notifications you wish to receive. -
diff --git a/docs/articles/expensify-classic/spending-insights/Insights.md b/docs/articles/expensify-classic/spending-insights/Insights.md index ce07f4b56450..c5ee218352fd 100644 --- a/docs/articles/expensify-classic/spending-insights/Insights.md +++ b/docs/articles/expensify-classic/spending-insights/Insights.md @@ -4,16 +4,16 @@ description: How to get the most out of the Custom Reporing and Insights redirect_from: articles/other/Insights/ --- -{% raw %} -# What is Custom Reporting and Insights? -The Insights dashboard allows you to monitor all aspects of company spend across categories, employees, projects, departments, and more. You can see trends in real time, forecast company budgets, and build unlimited custom reports with help from our trained specialist team. + +# Overview +The Insights dashboard allows you to monitor all aspects of company spending across categories, employees, projects, departments, and more. You can see trends in real-time, forecast company budgets, and build unlimited custom reports with help from our trained specialist team. ![Insights Pie Chart](https://help.expensify.com/assets/images/insights-chart.png){:width="100%"} ## Review your Insights data -1. Navigate to your [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}), located in the left hand menu +1. Navigate to your [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}), located in the left-hand menu 2. Select a specific date range (the default view has the current month pre-selected) -3. Use the filter options to select the categories, tags, employees etc that you want insights on +3. Use the filter options to select the categories, tags, employees, or any other parameter 4. Make sure that View in the top right corner is set to the pie chart icon 5. You can view any dataset in more detail by clicking in the “View Raw Data” column @@ -21,50 +21,50 @@ The Insights dashboard allows you to monitor all aspects of company spend across 1. Switch the View in the top right corner of the [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}) to the lists icon 2. Select the expenses you want to export, either by selecting individual expenses, or checking the select all box (next to Date at the top) -3. Select **Export To** in the top right hand corner to download the report as a .csv file +3. Select **Export To** in the top right-hand corner to download the report as a .csv file ## Create a Custom Export Report for your Expenses 1. Navigate to **Settings > Account > Preferences > scroll down to CSV Export Formats** -2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) +2. Build up a report using these [formulas]((https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates)) 3. Click the **Custom Export** button on the Insights page and your Account Manager will help get you started on building up your report -## Create a Custom Export Report for your Policy +## Create a Custom Export Report for your Workspace -1. Navigate to **Settings > Policies > Group > [Policy Name] > Export Formats** -2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) +1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Export Formats** +2. Build up a report using these [formulas](https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates) 3. If you need any help, click the **Support** button on the top left to contact your Account Manager {% include faq-begin.md %} #### Can I share my custom export report? -If you would like to create a custom export report that can be shared with other policy admins, you can create these by navigating to the **[Settings > Policies > Group > [Policy Name] > Export Formats](https://www.expensify.com/admin_policies?param={"section":"group"})** page. Custom export reports created under **Settings > Account > Preferences** page are only available to the member who created them. +If you would like to create a custom export report that can be shared with other workspace admins, you can do so by navigating to the **[Settings > Workspaces > Group > [Workspace Name] > Export Formats** page. Custom export reports created under the **Settings > Account > Preferences** page are only available to the member who created them. -#### Can I put expenses from different policies on the same report? +#### Can I put expenses from different workspaces on the same report? -Custom export reports created under Settings > Account > Preferences page are able to export expenses from multiple policies, and custom export formats created under Settings > Policies > Group > [Policy Name] > Export Formats are for expenses reported under that policy only. +Custom export reports created under the Settings > Account > Preferences page can export expenses from multiple workspaces, and custom export formats created under Settings > Workspaces> Group > [Workspace Name] > Export Formats are for expenses reported under that workspace only. #### Are there any default export reports available? -Yes! We have [seven default reports](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) available to export directly from the Reports page: +Yes! We have [seven default reports](https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates) available to export directly from the Reports page: - **All Data** - Expense Level Export** - the name says it all! This is for the people who want ALL the details from their expense reports. We're talking Tax, Merchant Category Codes, Approvers - you name it, this report's got it! -- **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line by line breakdown at a report level - submitter, total amount, report ID - that kind of stuff +- **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line-by-line breakdown at a report level - submitter, total amount, report ID - that kind of stuff - **Basic Export** - this is the best way to get a simple breakdown of all your expenses - just the basics - **Canadian Multiple Tax Export** - tax, GST, PST...if you need to know tax then this is the export you want! - **Category Export** - want to see a breakdown of your expenses by Category? This is the export you - **Per Diem Export** - the name says it all - **Tag Export** - much like the Category Export, but for Tags -*To note: these reports will be emailed directly to your email address rather than downloaded on your computer.* +*These reports will be emailed directly to your email address rather than automatically downloaded.* #### How many expenses can I export in one report? -The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) that you can run from the Reports page. +The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates) that you can run from the Reports page. #### What other kinds of export reports can my Account Manager help me create? -We’ve built a huge variety of custom reports for customers, so make sure to reach out to your Account Manager for more details. Some examples of custom reports we’ve build for customers before are: +We’ve built a huge variety of custom reports for customers, so make sure to reach out to your Account Manager for more details. Some examples of custom reports we’ve built for customers before are: - Accrual Report - Aged Approval Reports @@ -97,7 +97,5 @@ We’ve built a huge variety of custom reports for customers, so make sure to re - Unposted Procurement Aging Report - Unposted Travel Aging Report - Vendor Spend -- … or anything you can imagine! -{% endraw %} {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md index 6a6f99fa398f..2cd00155ae83 100644 --- a/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md +++ b/docs/articles/expensify-classic/workspaces/Change-member-workspace-roles.md @@ -11,14 +11,15 @@ To change the roles and permissions for members of your workspace, 3. Click the desired workspace name. 4. Click the **Members** tab on the left. 5. Click the Settings icon next to the desired member. -6. Select a new role for the member. +6. Select a new role for the member. -| | Employee | Auditor | Workspace Admin | -|---------------------------|----------------------------------|---------|-----------------| -| Submit reports | Yes | Yes | Yes | -| Comment on reports | Yes | Yes | Yes | -| Approve workspace reports | Only reports submitted to them | Yes | Yes | -| Edit workspace settings | No | No | Yes | +| Employee | Auditor | Workspace Admin | +| ----------------------------------------- | ------------------------------------------ | ----------------------------------------- | +| ✔ Submit their own reports | ✔ Submit their own reports | ✔ Submit their own reports | +| ✔ Approve reports submitted to them| ✔ Approve reports submitted to them | ✔ Approve reports submitted to them| +| | ✔ View all workspace reports | ✔ View all workspace reports | +| | | ✔ Approve all workspace reports | +| | | ✔ Edit workspace settings | {:start="7"} 7. If your workspace uses Advanced Approvals, select an “Approves to.” This determines who the member’s reports must be approved by, if applicable. If “no one” is selected, then any one with the Auditor or Workspace Admin role can approve the member’s reports. diff --git a/docs/articles/expensify-classic/workspaces/Invite-members-and-assign-roles.md b/docs/articles/expensify-classic/workspaces/Invite-members-and-assign-roles.md index 26032d06c1d0..1e698f593732 100644 --- a/docs/articles/expensify-classic/workspaces/Invite-members-and-assign-roles.md +++ b/docs/articles/expensify-classic/workspaces/Invite-members-and-assign-roles.md @@ -50,12 +50,13 @@ If you’re inviting multiple people who will be assigned the same role, you can 7. Select a role for the new member. The following table shows the permissions available for each role: -| | Employee | Auditor | Workspace Admin | -|---------------------------|----------------------------------|---------|-----------------| -| Submit reports | Yes | Yes | Yes | -| Comment on reports | Yes | Yes | Yes | -| Approve workspace reports | Only reports submitted to them | Yes | Yes | -| Edit workspace settings | No | No | Yes | +| Employee | Auditor | Workspace Admin | +| ----------------------------------------- | ------------------------------------------ | ----------------------------------------- | +| ✔ Submit their own reports | ✔ Submit their own reports | ✔ Submit their own reports | +| ✔ Approve reports submitted to them| ✔ Approve reports submitted to them | ✔ Approve reports submitted to them| +| | ✔ View all workspace reports | ✔ View all workspace reports | +| | | ✔ Approve all workspace reports | +| | | ✔ Edit workspace settings | 8. If your workspace uses Advanced Approvals, select “Approves to.” This determines who the member’s reports must be approved by, if applicable. If “no one” is selected, then if the member submits a report, anyone with the Auditor or Workspace Admin role can approve their reports. 9. Add a personal message, if desired. This message will appear in the invitation email or message. diff --git a/docs/articles/expensify-classic/workspaces/Remove-Members.md b/docs/articles/expensify-classic/workspaces/Remove-Members.md new file mode 100644 index 000000000000..a79d75edc77d --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Remove-Members.md @@ -0,0 +1,33 @@ +--- +title: Remove a Workspace Member +description: How to remove a member from a Workspace in Expensify +--- + +Removing a member from a workspace prevents them from submitting reports to or accessing the workspace. Please note that it does not delete their account or deactivate their Expensify Card. + +## Remove a Workspace Member +Important: Make sure the employee has submitted all reports, and the reports have been approved and reimbursed, and are in the final approval state. +1. Go to Settings > Workspaces > Group > [Workspace Name] > Members > Workspace Members +2. Select the member you'd like to remove and click the **Remove** button at the top of the Members table +3. If this member was an approver, update the approval workflow so that reports are no longer routed to them + +![image of members table in a workspace]({{site.url}}/assets/images/ExpensifyHelp_RemovingMembers.png){:width="100%"} + +{% include faq-begin.md %} + +## Will reports from this member on this workspace still be available? +Yes, as long as the reports have been submitted. You can navigate to the Reports page and enter the member's email in the search field to find them. However, Draft reports will be removed from the workspace, so these will no longer be visible to the Workspace Admin. + +## Can members still access their reports on a workspace after they have been removed? +Yes. Any report that has been approved will now show the workspace as “(not shared)” in their account. If it is a Draft Report they will still be able to edit it and add it to a new workspace. If the report is Approved or Reimbursed they will not be able to edit it further. + +## Who can remove members from a workspace? +Only Workspace Admins. It is not possible for a member to add or remove themselves from a workspace. It is not possible for a Domain Admin who is not also a Workspace Admin to remove a member from a workspace. + +## How do I remove a member from a workspace if I am seeing an error message? +If a member is a **preferred exporter, billing owner, report approver** or has **processing reports**, to remove them from the workspace you will first need to: + +* **Preferred Exporter**: Go to Settings > Workspaces > Group > [Workspace Name] > Connections > Configure and select a different Workspace Admin in the dropdown for **Preferred Exporter**. +* **Billing Owner**: Take over billing on the Settings > Workspaces > Group > [Workspace Name] > Overview page. +* **Processing reports**: Approve or reject the member’s reports on your Reports page. +* **Approval Workflow**: Remove them as a workflow approver on your Settings > Workspaces > Group > [Workspace Name] > Members > Approval Mode > page by changing the "**Submit reports to**" field. diff --git a/docs/articles/expensify-classic/workspaces/tax-tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/workspaces/tax-tracking.md rename to docs/articles/expensify-classic/workspaces/Tax-Tracking.md diff --git a/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md index c05df92bbbff..18ad693a1c56 100644 --- a/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md +++ b/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md @@ -29,6 +29,8 @@ For individuals or employees: To enable Scheduled Submit on your individual work **Manually**: An open report will be created, and expenses will be added to it automatically. However, it's important to note that the report will not be submitted automatically; manual submission of reports will be required.This is a great option for automatically gathering all an employee’s expenses on a report for the employee’s convenience, but they will still need to review and submit the report. +**Instantly**: Expenses are automatically added to a report in the Processing state, and all expenses will continue to accumulate on one report until it is Approved or Reimbursed. This removes the need to submit expenses, and Processing reports can either be Reimbursed right away (if Submit and Close is enabled), or Approved and then Reimbursed (if Submit and Approve is enabled) by a workspace admin. + # Deep Dive ## Schedule Submit Override @@ -36,3 +38,6 @@ If Scheduled Submit is disabled at the group workspace level or configured the f ## Personal Card Transactions Personal card transactions are handled differently compared to other expenses. If a user has added a card through Settings > Account > Credit Card Import, they need to make sure it is set as non-reimbursable and transactions must be automatically merged with a SmartScanned receipt. If transactions are set to come in as reimbursable or they aren’t merged with a SmartScanned receipt, Scheduled Submit settings will not apply. + +## A note on Instantly +Setting Scheduled Submit frequency to Instantly will limit some employee actions on reports, such as the ability to retract or self-close reports, or create multiple reports. When Instantly is selected, expenses are automatically added to a Processing expense report, and new expenses will continue to accumulate on a single report until the report is Closed or Reimbursed by a workspace admin. diff --git a/docs/articles/new-expensify/chat/Create-a-new-chat.md b/docs/articles/new-expensify/chat/Create-a-new-chat.md new file mode 100644 index 000000000000..db76946b3d83 --- /dev/null +++ b/docs/articles/new-expensify/chat/Create-a-new-chat.md @@ -0,0 +1,112 @@ +--- +title: Create a new chat +description: Start a new private, group, or room chat +redirect_from: articles/other/Everything-About-Chat/ +--- +
+ +Expensify Chat is an instant messaging system that helps you converse with people both inside and outside of your workspace about payments, company updates, and more. Expensify Chats are held in private chats, groups, and rooms. +- **Private chats**: Private conversations for 1-on-1 chats +- **Groups**: Private conversations for 2+ participants +- **Rooms**: Public conversations that are available for all members of your workspace + +# Start a private 1-on-1 chat + +{% include info.html %} +You cannot add more people to a private chat. If later you wish to add more people to the conversation, you’ll need to create a group chat. +{% include end-info.html %} + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and click their name to start a new chat with them. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and tap their name to start a new chat with them. +{% include end-option.html %} + +{% include end-selector.html %} + +# Start a group chat + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and click **Add to group**. Repeat this step until all desired participants are added. *Note: Group participants are listed with a green checkmark.* +3. Click **Next**. +4. Update the group image or name. + - **Name**: Click **Group Name** and enter a new name. Then click **Save**. + - **Image**: Click the profile image and select **Upload Image**. Then choose a new image from your computer files and select the desired image zoom. +5. Click **Start group**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and tap **Add to group**. Repeat this step until all desired participants are added. *Note: Group participants are listed with a green checkmark.* +3. Tap **Next** (or **Create chat** if you add only one person to the group). +4. Update the group image or name. + - **Name**: Tap **Group Name** and enter a new name. Then tap **Save**. + - **Image**: Tap the profile image and select **Upload Image**. Then choose a new image from your photos and select the desired image zoom. +5. Tap **Start group**. + +{% include end-option.html %} + +{% include end-selector.html %} + +# Start a chat room + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Click the #Room tab at the top. +3. Enter a name for the room. *Note: It cannot be the same as another room in the Workspace.* +4. (Optional) Add a description of the room. +5. Click **Workspace** to select the workspace for the room. +6. Click **Who can post** to determine if all members can post or only Admins. +7. Click **Visibility** to determine who can find the room. + - **Public**: Anyone can find the room (perfect for conferences). + - **Private**: Only people explicitly invited can find the room. + - **Workspace**: Only workspace members can find the room. + +{% include info.html %} +Anyone, including non-Workspace Members, can be invited to a private or restricted room. +{% include end-info.html %} + +8. Click **Create room**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Tap the #Room tab at the top. +3. Enter a name for the room. *Note: It cannot be the same as another room in the Workspace.* +4. (Optional) Add a description of the room. +5. Tap **Workspace** to select the workspace for the room. +6. Tap **Who can post** to determine if all members can post or only Admins. +7. Tap **Visibility** to determine who can find the room. + - **Public**: Anyone can find the room (perfect for conferences). + - **Private**: Only people explicitly invited can find the room. + - **Workspace**: Only workspace members can find the room. + +{% include info.html %} +Anyone, including non-Workspace Members, can be invited to a private or restricted room. +{% include end-info.html %} + +8. Tap **Create room**. +{% include end-option.html %} + +{% include end-selector.html %} + +# FAQs + +**What's the difference between a private 1-on-1 chat and a group chat with only 2 people?** +With a group chat, you can add additional people to the chat at any time. But you cannot add additional people to a private 1-on-1 chat. +
+ + + + diff --git a/docs/articles/new-expensify/chat/Edit-or-delete-messages.md b/docs/articles/new-expensify/chat/Edit-or-delete-messages.md new file mode 100644 index 000000000000..a19fee42e740 --- /dev/null +++ b/docs/articles/new-expensify/chat/Edit-or-delete-messages.md @@ -0,0 +1,31 @@ +--- +title: Edit or delete messages +description: Edit or delete chat messages you've sent +--- +
+ +{% include info.html %} +You can edit or delete your *own* messages only. Deleting a message cannot be undone. +{% include end-info.html %} + +You have the option to edit or delete any of your messages: +- **Edit message**: Reopens a message so you can make changes. Once a message has been updated, an “edited” label will appear next to it. +- **Delete message**: Removes a message or image for all viewers. + +To edit or delete a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open a chat in your inbox. +2. Right-click a message and select **Edit comment** or **Delete comment**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open a chat in your inbox. +2. Press and hold a message and select **Edit comment** or **Delete comment**. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md deleted file mode 100644 index 30eeb4158902..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Expensify Chat for Conference Attendees -description: Best Practices for Conference Attendees -redirect_from: articles/other/Expensify-Chat-For-Conference-Attendees/ ---- - -# Overview -Expensify Chat is the best way to meet and network with other event attendees. No more hunting down your contacts by walking the floor or trying to find someone in crowds at a party. Instead, you can use Expensify Chat to network and collaborate with others throughout the conference. - -To help get you set up for a great event, we’ve created a guide to help you get the most out of using Expensify Chat at the event you’re attending. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your fellow 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) - -# Chat Best Practices -To get the most out of your experience at your conference and engage people in a meaningful conversation that will fulfill your goals instead of turning people off, here are some tips on what to do and not to do as an event attendee using Expensify Chat: - -**Do:** -- Chat about non-business topics like where the best coffee is around the event, what great lunch options are available, or where the parties are happening that night! -- Share pictures of your travel before the event to hype everyone up, during the event if you met that person you’ve been meaning to see for years, or a fun pic from a party. -- Try to create fun groups with your fellow attendees around common interests like touring a local sight, going for a morning run, or trying a famous restaurant. - -**Don't:** -- Pitch your services in public rooms like #social or speaking session rooms. -- Start a first message with a stranger with a sales pitch. -- Discuss controversial topics such as politics, religion, or anything you wouldn’t say on a first date. -- In general just remember that you are still here for business, your profile is public, and you’re representing yourself & company, so do not say anything you wouldn’t feel comfortable sharing in a business setting. - -**Pro-Tips:** -Get active in Chat early and often by having real conversations around thought leadership or non-business discussions to stand out from the crowd! Also if you’re in a session and are afraid to ask a question, just ask in the chat room to make sure you can discuss it with the speaker after the session ends. - -By following these tips you’ll ensure that your messages will not be [flagged for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) and you will not mess it up for the rest of us. diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md deleted file mode 100644 index 652fc2ee4d2b..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Expensify Chat for Conference Speakers -description: Best Practices for Conference Speakers -redirect_from: articles/other/Expensify-Chat-For-Conference-Speakers/ ---- - -# Overview -Are you a speaker at an event? Great! We're delighted to provide you with an extraordinary opportunity to connect with your session attendees using Expensify Chat — before, during, and after the event. Expensify Chat offers a powerful platform for introducing yourself and your topic, fostering engaging discussions about your presentation, and maintaining the conversation with attendees even after your session is over. - -# 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) - -# Setting Up a Chatroom for Your Session: Checklist -To make the most of Expensify Chat for your session, here's a handy checklist: -- Confirm that your session has an Expensify Chat room, and have the URL link ready to share with attendees in advance. - - You can find the link by clicking on the avatar for your chatroom > “Share Code” > “Copy URL to dashboard” -- Join the chat room as soon as it's ready to begin engaging with your audience right from the start. -- Consider having a session moderator with you on the day to assist with questions and discussions while you're presenting. -- Include the QR code for your session's chat room in your presentation slides. Displaying it prominently on every slide ensures that attendees can easily join the chat throughout your presentation. - -# Tips to Enhance Engagement Around Your Session -By following these steps and utilizing Expensify Chat, you can elevate your session to promote valuable interactions with your audience, and leave a lasting impact beyond the conference. We can't wait to see your sessions thrive with the power of Expensify Chat! - -**Before the event:** -- Share your session's QR code or URL on your social media platforms, your website or other platforms to encourage attendees to join the conversation early on. -- Encourage attendees to ask questions in the chat room before the event, enabling you to tailor your session and address their specific interests. - -**During the event:** -- Keep your QR code readily available during the conference by saving it as a photo on your phone or setting it as your locked screen image. This way, you can easily share it with others you meet. -- Guide your audience back to the QR code and encourage them to ask questions, fostering interactive discussions. - -**After the event:** -- Continue engaging with attendees by responding to their questions and comments, helping you expand your audience and sustain interest. -- Share your presentation slides after the event as well as any photos from your session, allowing attendees to review and share your content with their networks if they want to. - -If you have any questions on how Expensify Chat works, head to our guide [here](https://help.expensify.com/articles/other/Everything-About-Chat). diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md b/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md deleted file mode 100644 index caeccd1920b1..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Expensify Chat Playbook for Conferences -description: Best practices for how to deploy Expensify Chat for your conference -redirect_from: articles/playbooks/Expensify-Chat-Playbook-for-Conferences/ ---- -# Overview -To help make setting up Expensify Chat for your event and your attendees super simple, we’ve created a guide for all of the technical setup details. - -# Who you are -As a conference organizer, you’re expected to amaze and inspire attendees. You want attendees to get to the right place on time, engage with the speakers, and create relationships with each other that last long after the conference is done. Enter Expensify Chat, a free feature that allows attendees to interact with organizers and other attendees in realtime. With Expensify Chat, you can: - -- Communicate logistics and key information -- Foster conference wide attendee networking -- Organize conversations by topic and audience -- Continue conversations long after the event itself -- Digitize attendee social interaction -- Create an inclusive environment for virtual attendees - -Sounds good? Great! In order to ensure your team, your speakers, and your attendees have the best experience possible, we’ve created a guide on how to use Expensify Chat at your event. - -*Let’s get started!* - - -# Support -Connect with your dedicated account manager in any new.expensify.com #admins room. Your account manager is excited to brainstorm the best ways to make the most out of your event and work through any questions you have about the setup steps below. - -We also have a number of [moderation tools](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) available to admins to help make sure your event is seamless, safe, and fun! - -# Step by step instructions for setting up your conference on Expensify Chat -Based on our experience running conferences atop Expensify Chat, we recommend the following simple steps: - -## Step 1: Create your event workspace in Expensify -To create your event workspace in Expensify: -1. In [new.expensify.com](https://new.expensify.com): “+” > “New workspace” -1. Name the workspace (e.g. “ExpensiCon”) - -## Step 2: Set up all the Expensify Chat rooms you want to feature at your event -**Protip**: Your Expensify account manager can complete this step with you. Chat them in #admins on new.expensify.com to coordinate! - -To create a new chat room: -1. Go to [new.expensify.com](https://new.expensify.com) -1. Go to “+” > New room -1. Name the room (e.g. “#social”) -1. Select the workspace created in step 1 -1. Select “Public” visibility -1. Repeat for each room - -For an easy-to-follow event, we recommend creating these chat rooms: - -- **#social** - This room will include all attendees, speakers, and members of your organizing team. You can use this room to discuss social events, happy hours, dinners, or encourage attendees to mingle, share photos and connect. -- **#announcements** - This room will be used as your main announcement channel, and should only be used by organizers to announce schedule updates or anything important that your attendees need to know. Everyone in your policy will be invited to this channel, but chatting in here isn’t encouraged so to keep the noise to a minimum. -- **Create an individual room for each session** - Attendees will be able to engage with the speaker/session leader and can ask questions about their content either before/during/after the session. -- **Create a room with your Expensify account manager/s** - We can use this room to coordinate using Expensify Chat before, during, and after the event. - -**Protip** Check out our [moderation tools](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) to help flag comments deemed to be spam, inconsiderate, intimidating, bullying, harassment, assault. On any comment just click the flag icon to moderate conversation. - -## Step 3: Add chat room QR codes to the applicable session slide deck -Gather QR codes: -1. Go to [new.expensify.com](https://new.expensify.com) -1. Click into a room and click the room name or avatar in the top header -1. Go into Share Code -1. Screenshot the QR code to add to your deck - -Add the QR code to every slide so that if folks forget to scan the QR code at the beginning of the presentation, they can still join the discussion. - -## Step 4: Plan out your messaging and cadence before the event begins -Expensify Chat is a great place to provide updates leading up to your event -- share news, get folks excited about speakers, and let attendees know of crucial event information like recommended attire, travel info, and more. For example, you might consider: - -**Prep your announcements:** -- Create a document containing drafts of the key messages you intend to send throughout the day. -- If your event's agenda is broken up into hourly blocks, create a separate section for each hour of the event, to make it easy to find the correct section at the right time. -- Start each day with a review of the daily agenda, such as a bullet list summarizing what's happening hour by hour. - -**Post your updates:** -- Designate a team member to post each update in #announce at the designated time. -- Each hour, send a message listing exactly what is happening next – if there are multiple sessions happening simultaneously, list out each, along with a description of the session, a reminder of where it's located, and (most importantly) a link to the chat room for that session -- Write the messages in [markdown format](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text), such that they can be copy/pasted directly into Expensify Chat for sending. - - If there is some formatting issue upon posting, no problem: just edit the comment after sending, and it'll be fixed for everyone. -- We’d also recommend posting your updates on new lines so that if someone has a question about a certain item they can ask in a thread pertaining to that topic, rather than in one consolidated block. - -**Protip**: Your account manager can help you create this document, and would be happy to send each message at the appointed time for you. - -## Step 5: Share Expensify Chat How-To Resources with Speakers, Attendees, Admins -We’ve created a few helpful best practice docs for your speakers, admins, and attendees to help navigate using Expensify Chat at your event. Feel free to share the links below with them! - -- [Expensify Chat for Conference Attendees](https://help.expensify.com/articles/other/Expensify-Chat-For-Conference-Attendees) -- [Expensify Chat for Conference Speakers](https://help.expensify.com/articles/other/Expensify-Chat-For-Conference-Speakers) -- [Expensify Chat for Admins](https://help.expensify.com/articles/other/Expensify-Chat-For-Admins) - -## Step 6: Follow up with attendees after the event -Continue the connections by using Expensify Chat to keep your conference community connected. Encourage attendees to share photos, their favorite memories, funny stories, and more. - -# Conclusion -Once you have completed the above steps you are ready to host your conference on Expensify Chat! Let your account manager know any questions you have over in your [new.expensify.com](https://new.expensify.com) #admins room and start driving activity in your Expensify Chat rooms. Once you’ve reviewed this doc you should have the foundations in place, so a great next step is to start training your speakers on how to use Expensify Chat for their sessions. Coordinate with your account manager to make sure everything goes smoothly! diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md b/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md new file mode 100644 index 000000000000..ff341d4de68f --- /dev/null +++ b/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md @@ -0,0 +1,50 @@ +--- +title: Expensify Chat rooms for admins +description: Use the announce and admins chat rooms +--- +
+ +When a workspace is created, an #announce and #admins chat room is automatically created. + +# #announce + +All Workspace Members can use this room to share or discover important company announcements and have conversations with other members. + +By default, all Workspace Members are allowed to send messages in #announce rooms. However, Workspace Admins can update the permissions to allow only admins to post messages in the #announce room. + +## Update messaging permissions + +To allow only admins to post in an #announce room, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the #announce room chat in your inbox. +2. Click the room header. +3. Click **Settings**. +4. Click **Who can post** and select **Admins only**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the #announce room chat in your inbox. +2. Tap the room header. +3. Tap **Settings**. +4. Tap **Who can post** and select **Admins only**. +{% include end-option.html %} + +{% include end-selector.html %} + +# #admins + +Only Workspace Admins can access this room to collaborate with the other admins in the workspace. You can also use this space to: +- Chat with your dedicated Expensify Setup Specialist. +- Chat with your Account Manager (if you have a subscription with 10 or more members). +- Review changes made to your Workspace settings (includes changes made by someone on your team, your dedicated Expensify Setup Specialist, or your dedicated Account Manager). + +# FAQs + +**Someone I don’t recognize is in my #admins room for my Workspace.** + +Your #admins room also includes your dedicated Expensify Setup Specialist who will help you onboard and answer your questions. You can chat with them directly from your #admins room. If you have a subscription of 10 or more members, you can chat with your dedicated Account Manager, who is also added to your #admins room for ongoing product support. + +
diff --git a/docs/articles/new-expensify/chat/Flag-chat-messages.md b/docs/articles/new-expensify/chat/Flag-chat-messages.md new file mode 100644 index 000000000000..4955298bbd6b --- /dev/null +++ b/docs/articles/new-expensify/chat/Flag-chat-messages.md @@ -0,0 +1,33 @@ +--- +title: Flag chat messages +description: Report a message as offensive, spam, etc. +--- +
+ +Flagging a message as offensive (including unwanted behavior or offensive messages or attachments) escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified of the flag anonymously, and the moderation team will decide what further action is needed. + +Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed. In extreme cases, the sender of the message may be temporarily or permanently blocked from posting. + +{% include info.html %} +Messages sent in public chat rooms are automatically reviewed for offensive content by an automated system. If offensive content is found, the message is sent to Expensify’s internal moderation team for further review. +{% include end-info.html %} + +To flag a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat in your inbox. +2. Hover over the message and click the three dot menu icon that appears in the menu at the top right of the message. Then select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat in your inbox. +2. Press and hold the message and select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md deleted file mode 100644 index 096a3d1527be..000000000000 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: Introducing Expensify Chat -description: Everything you need to know about Expensify Chat! -redirect_from: articles/other/Everything-About-Chat/ ---- - - - -# Overview - -For a quick snapshot of how Expensify Chat works, and New Expensify in general, check out our website! - -# What’s Expensify Chat? - -Expensify Chat is an instant messaging and payment platform. You can manage all your payments, whether for business or personal, and discuss the transactions themselves. - -With Expensify Chat, you can start a conversation about that missing receipt your employee forgot to submit or chat about splitting that electric bill with your roommates. Expensify makes sending and receiving money as easy as sending and receiving messages. Chat with anyone directly, in groups, or in rooms. - -# Getting started - -Download New Expensify from the [App Store](https://apps.apple.com/us/app/expensify-cash/id1530278510) or [Google Play](https://play.google.com/store/apps/details?id=com.expensify.chat) to use the chat function. You can also access your account at new.expensify.com from your favorite web browser. - -After downloading the app, log into your new.expensify.com account (you’ll use the same login information as your Expensify Classic account). From there, you can customize your profile and start chatting. - -## How to send messages - -1. Click **+** then **Send message** in New Expensify -2. Choose **Chat** -3. Search for any name, email or phone number -4. Select the individual to begin chatting - -## How to create a group - -1. Click **+**, then **Send message** in New Expensify -2. Search for any name, email or phone number -3. Click **Add to group** -4. Group participants are listed with a green check -5. Repeat steps 1-3 to add more participants to the group -6. Click **Create chat** to start chatting - -## How to create a room - -1. Click **+**, then **Send message** in New Expensify -2. Click **Room** -3. Enter a room name that doesn’t already exist on the intended Workspace -4. Choose the Workspace you want to associate the room with. -5. Choose the room’s visibility setting: -6. Private: Only people explicitly invited can find the room* -7. Restricted: Workspace members can find the room* -8. Public: Anyone can find the room - -*Anyone, including non-Workspace Members, can be invited to a Private or Restricted room. - -## How to invite and remove members - -You can invite people to a Group or Room by @mentioning them or from the Members pane. - -## Mentions: - -1. Type **@** and start typing the person’s name or email address -2. Choose one or more contacts -3. Input message, if desired, then send - - -## Members pane invites: - -1. Click the **Room** or **Group** header -2. Select **Members** -3. Click **Invite** -4. Find and select any contact/s you’d like to invite -5. Click **Next** -6. Write a custom invitation if you like -7. Click **Invite** - -## Members pane removals: - -1. Click the **Room** or **Group** header -2. Select **Members** -3. Find and select any contact/s you’d like to remove -4. Click **Remove** -5. Click **Remove members** - -## How to format text - -- To italicize your message, place an underscore on both sides of the text: _text_ -- To bold your message, place an asterisk on both sides of the text: *text* -- To strikethrough your message, place a tilde on both sides of the text: ~text~ -- To turn your message into code, place a backtick on both sides of the text: `text` -- To turn your text into a blockquote, add an angled bracket (>) in front of the text: - >your text -- To turn your message into a heading, place a number sign (#) in front of the text: -# Heading -- To turn your entire message into code block, place three backticks on both sides of the text: -``` -here's some text -and even more text -``` - -## Message actions - -If you mouse-over a message (or long-press on mobile), you will see the action menu. This allows you to add a reaction, start a thread, copy the link, mark it as unread, edit your own message, delete your own message, or flag it as offensive. - -**Add a reaction**: React with an emoji to the message -**Start a thread**: Start a thread by responding to the message instead of replying in the parent room -**Copy the link**: Share the message link with people who have access (i.e. Group or Room members). Note: Anyone can access messages in Public rooms -**Mark is as unread**: This will highlight the message in your left hand menu -**Edit message**: You can edit your own messages anytime. When you edit a message it will show as *edited* -**Delete message**: Deleting a message will remove it entirely for all viewers -**Flag as offensive**: Flagging a message as offensive escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified anonymously of the flag and the moderation team will decide what further action is needed - -## Workspace chat rooms - -In addition to 1:1 and group chat, members of a Workspace will have access to two additional rooms; the #announce and #admins rooms. -All Workspace members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between Workspace members. - -All Workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your Workspace, and chat with your dedicated Expensify Setup Specialist. If you have a subscription of 10 or more members, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this same #admins room. Anytime someone on your team, your dedicated Setup Specialist, or your dedicated account manager makes any changes to your Workspace settings, that update is logged in the #admins room. - -# Deep Dive - -## Flagging content as offensive - -In order to maintain a safe community for our members, Expensify provides tools to report offensive content and unwanted behavior in Expensify Chat. If you see a message (or attachment/image) from another member that you’d like our moderators to review, you can flag it by clicking the flag icon in the message context menu (on desktop) or holding down on the message and selecting “Flag as offensive” (on mobile). - -![Moderation Context Menu](https://help.expensify.com/assets/images/moderation-context-menu.png){:width="100%"} - -Once the flag is selected, you will be asked to categorize the message (such as spam, bullying, and harassment). Select what you feel best represents the issue is with the content, and you’re done - the message will be sent off to our internal team for review. - -![Moderation Flagging Options](https://help.expensify.com/assets/images/moderation-flag-page.png){:width="100%"} - -Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed, and in extreme cases, the sender of the message can be temporarily or permanently blocked from posting. - -You will receive a whisper from Concierge any time your content has been flagged, as well as when you have successfully flagged a piece of content. - -![Moderation Reportee Whisper](https://help.expensify.com/assets/images/moderation-reportee-whisper.png){:width="100%"} -![Moderation Reporter Whisper](https://help.expensify.com/assets/images/moderation-reporter-whisper.png){:width="100%"} - -*Note: Any message sent in public chat rooms are automatically reviewed by an automated system looking for offensive content and sent to our moderators for final decisions if it is found.* - -{% include faq-begin.md %} - -## What are the #announce and #admins rooms? - -In addition to 1:1, Groups, and Workspace rooms, members of a Workspace will have access to two additional rooms; the #announce and #admins rooms. - -All domain members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between Workspace members. - -All Workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your Workspace, and chat with your dedicated Expensify Setup Specialist. If you have an existing subscription, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this room. - -## Someone I don’t recognize is in my #admins room for my Workspace; who is it? - -After creating your Workspace, you’ll have a dedicated Expensify Setup Sspecialist who will help you onboard and answer your questions. You can chat with them directly in the #admins room or request a call to talk to them over the phone. Later, once you've finished onboarding, if you have a subscription of 10 or more members, a dedicated Account Manager is added to your #admins room for ongoing product support. - -## Can I force a chat to stay at the top of the chats list? - -You sure can! Click on the chat you want to keep at the top of the list, and then click the small **pin** icon. If you want to unpin a chat, just click the **pin** icon again. - -## Can I change the way my chats are displayed? - -The way your chats display in the left-hand menu is customizable. We offer two different options; Most Recent mode and _#focus_ mode. - -- Most Recent mode will display all chats by default, sort them by the most recent, and keep your pinned chats at the top of the list. -- #focus mode will display only unread and pinned chats, and will sort them alphabetically. This setting is perfect for when you need to cut distractions and focus on a crucial project. - -You can find your display mode by clicking on your Profile > Preferences > Priority Mode. -{% include faq-end.md %} diff --git a/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md b/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md new file mode 100644 index 000000000000..d6877f71be07 --- /dev/null +++ b/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md @@ -0,0 +1,97 @@ +--- +title: Invite members to a chat group or room +description: Add new people to a chat group or room +--- +
+ +You can invite people to a group or room by: +- @mentioning them +- Using the Members pane of the chat +- Sharing a link or QR code + +{% include info.html %} +These options are available only for rooms and groups. You cannot add additional people to a private 1-on-1 chat between two people. +{% include end-info.html %} + +# Invite with mention + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. In the message field, type @ and the person’s name or email address. Repeat this step until all desired participants are listed. +3. Enter a message, if desired. +4. Press Enter on your keyboard or click the Send icon. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. In the message field, type @ and the person’s name or email address. Repeat this step until all desired participants are listed. +3. Enter a message, if desired. +4. Tap the Send icon. +{% include end-option.html %} + +{% include end-selector.html %} + +# Invite from the Members pane + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. Click the room or group header. +3. Click **Members**. +4. Click **Invite member**. +5. Find and select any contact(s) you’d like to invite. +6. Click **Invite**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. Tap the room or group header. +3. Tap **Members**. +4. Tap **Invite member**. +5. Find and select any contact(s) you’d like to invite. +6. Tap **Invite**. +{% include end-option.html %} + +{% include end-selector.html %} + +# Share chat link or QR code + +{% include info.html %} +If your group/room is Private, you can only share the chat link with other members of the group/room. If it is a public group/room, anyone can access the chat via the link. +{% include end-info.html %} + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. Click the room or group header. +3. Click **Share code**. +4. Copy the link or share the QR code. + - **Copy link**: Click **Copy URL** and paste the link into another chat, email, Slack, etc. + - **Share QR Code**: Present your device to another user so they can scan the QR code with their device. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. Tap the room or group header. +3. Tap **Share code**. +4. Copy the link or share the QR code. + - **Copy link**: Tap **Copy URL** and paste the link into another chat, email, Slack, etc. + - **Share QR Code**: Present your device to another user so they can scan the QR code with their device. +{% include end-option.html %} + +{% include end-selector.html %} + +# FAQs + +**How do I remove someone from a chat group or room?** + +Currently, members have to remove themselves from a chat. + +
+ + + diff --git a/docs/articles/new-expensify/chat/Leave-a-chat-room.md b/docs/articles/new-expensify/chat/Leave-a-chat-room.md new file mode 100644 index 000000000000..252e7e94f1ac --- /dev/null +++ b/docs/articles/new-expensify/chat/Leave-a-chat-room.md @@ -0,0 +1,23 @@ +--- +title: Leave a chat room +description: Remove a chat room from your inbox +--- +
+ +If you wish to no longer be part of a chat room, you can leave the room. This means that the chat room will no longer be visible in your inbox, and you will no longer see updates posted to the room or be notified of new messages. + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat room. +2. Click the 3 dot menu icon in the top right and select **Leave**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat room. +2. Tap the 3 dot menu icon in the top right and select **Leave**. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Reorder-chat-inbox.md b/docs/articles/new-expensify/chat/Reorder-chat-inbox.md new file mode 100644 index 000000000000..cd62f95e63e8 --- /dev/null +++ b/docs/articles/new-expensify/chat/Reorder-chat-inbox.md @@ -0,0 +1,49 @@ +--- +title: Reorder chat inbox +description: Change how your chats are displayed in your inbox +--- +
+ +You can customize the order of the chat messages in your inbox by pinning them to the top and/or changing your message priority to Most Recent or #focus: +- **Pin**: Bumps a specific chat up to the top of your inbox list. +- **Message priority**: Determines the order that messages are sorted and displayed: + - **Most Recent**: Displays all chats by default sorted by the most recent, and keep your pinned chats at the top of the list. + - **#focus**: Displays only unread and pinned chats sorted alphabetically. + +# Pin a message + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +Right-click a chat in your inbox and select **Pin**. The chat will now be pinned to the top of your inbox above all of the others. + +To unpin a chat, repeat this process to click the pin icon again to remove it. +{% include end-option.html %} + +{% include option.html value="mobile" %} +Press and hold a chat in your inbox and select **Pin**. The chat will now be pinned to the top of your inbox above all of the others. + +To unpin a chat, repeat this process to tap the pin icon again to remove it. +{% include end-option.html %} + +{% include end-selector.html %} + +# Change message priority + +{% 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 the **Preferences** tab on the left. +3. Click **Priority Mode** to select either #focus or Most recent. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap the **Preferences** tab. +3. Tap **Priority Mode** to select either #focus or Most recent. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md new file mode 100644 index 000000000000..2aa2b026fdd6 --- /dev/null +++ b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md @@ -0,0 +1,52 @@ +--- +title: Send and format chat messages +description: Send chat messages and stylize them with markdown +--- +
+ +Once you are added to a chat or create a new chat, you can send messages to other members in the chat and even format the text to include bold, italics, and more. + +{% include info.html %} +Some chat rooms may have permissions that restrict who can send messages. In this case, you won’t be able to send messages in the room if you do not have the required permission level. +{% include end-info.html %} + +To send a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click any chat in your inbox to open it. +2. Use the message bar at the bottom of the message to enter a message, add attachments, and add emojis. + - **To add a message**: Click the field labeled “Write something” and type a message. + - **To add an attachment**: Click the plus icon and select **Add attachment**. Then choose the attachment from your files. + - **To add an emoji**: Click the emoji icon to the right of the message field. +3. Press Enter on your keyboard or click the Send icon to send the message. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap any chat in your inbox to open it. +2. Use the message bar at the bottom of the message to enter a message, add attachments, and add emojis. + - **To add a message**: Tap the field labeled “Write something” and type a message. + - **To add an attachment**: Tap the plus icon and select **Add attachment**. Then choose the attachment from your files. + - **To add an emoji**: Tap the emoji icon to the right of the message field. +3. Tap the Send icon to send the message. +{% include end-option.html %} + +{% include end-selector.html %} + +# Format text in a chat message + +You can format the text in a chat message using markdown. + +- _Italicize_: Add an underscore _ on both sides of the text. +- **Bold**: Add two asterisks ** on both sides of the text. +- ~~Strikethrough~~: Add two tildes ~~ on both sides of the text. +- Heading: Add a number sign # in front of the text. +- Inline image: Add `![Alt text](image URL)` and add the URL to the image and alt text that describes the image. +- Tag another member: Add an at symbol @ and enter the member's name, username, or email address. +- Mention a room: Add a number sign # and enter the room name +- > Blockquote: Add an angled bracket > in front of the text. +- `Code block for a small amount of text`: Add a backtick ` on both sides of the text. +- Code block for the entire message: Add three backticks ``` at the beginning and the end of the message. + +
diff --git a/docs/articles/new-expensify/chat/Start-a-conversation-thread.md b/docs/articles/new-expensify/chat/Start-a-conversation-thread.md new file mode 100644 index 000000000000..cb3a3aa69296 --- /dev/null +++ b/docs/articles/new-expensify/chat/Start-a-conversation-thread.md @@ -0,0 +1,33 @@ +--- +title: Start a conversation thread +description: Start a private conversation related to a different message +--- +
+ +You can respond directly to a message sent in a chat group or room to start a private 1-on-1 chat with another member about the message (instead of replying to the entire group or room). + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat in your inbox. +2. Right-click the message and select **Reply in thread**. +3. Enter and submit your reply in the new chat. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat in your inbox. +2. Press and hold the message and select **Reply in thread**. +3. Enter and submit your reply in the new chat. +{% include end-option.html %} + +{% include end-selector.html %} + +To return to the conversation where the thread originated from, you can click the link at the top of the thread. + +
+ + + + + + diff --git a/docs/articles/new-expensify/connections/Coming-Soon.md b/docs/articles/new-expensify/connections/Coming-Soon.md new file mode 100644 index 000000000000..4d32487a14b5 --- /dev/null +++ b/docs/articles/new-expensify/connections/Coming-Soon.md @@ -0,0 +1,6 @@ +--- +title: Coming soon +description: Coming soon +--- + +# Coming soon \ No newline at end of file diff --git a/docs/articles/new-expensify/expenses/Create-an-expense.md b/docs/articles/new-expensify/expenses/Create-an-expense.md new file mode 100644 index 000000000000..cf6a13f9d5ac --- /dev/null +++ b/docs/articles/new-expensify/expenses/Create-an-expense.md @@ -0,0 +1,125 @@ +--- +title: Create an expense +description: Request payment from an employer or a friend +redirect_from: articles/request-money/Request-and-Split-Bills/ +--- +
+ +You can create an expense to request payment from an employer’s workspace or from a friend using any of the following options: +- **SmartScan**: Take a picture of a receipt to capture the expense details automatically. +- **Add manually**: Manually enter the expense details. +- **Create a distance expense**: Capture mileage expenses by entering the addresses you traveled to. Expensify automatically calculates the distance, the rate per mile, and the total cost. + +# SmartScan a receipt + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Submit Expense**. +2. Click **Scan**. +3. Drag and drop the receipt into Expensify, or click **Choose File** to select it from your saved files. *Note: The SmartScan process will auto-populate the merchant, date, and amount.* +4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. +5. Add a description, category, tags, or tax as desired, or as required by your workspace. +6. (Optional) Enable the expense as billable if it should be billed to a client. +7. Click **Submit Expense**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon at the bottom of the screen and select **Submit Expense**. +2. Tap **Scan**. +3. Tap the green button to take a photo of a receipt, or tap the Image icon to the left of it to upload a receipt from your phone. *Note: The SmartScan process will auto-populate the merchant, date, and amount.* +4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. +5. Add a description, category, tags, or tax as desired, or as required by your workspace. +6. (Optional) Enable the expense as billable if it should be billed to a client. +7. Tap **Submit**. +{% include end-option.html %} + +{% include end-selector.html %} + +{% include info.html %} +You can also forward receipts to receipts@expensify.com using an email address that is your primary or secondary email address. SmartScan will automatically pull all of the details from the receipt and add it to your expenses. +{% include end-info.html %} + +# Manually add an expense + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Submit Expense**. +2. Click **Manual**. +3. Enter the amount on the receipt and click **Next**. *Note: Click the currency symbol to select a different currency.* +4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. +5. (Optional) Add a description. +6. Add a merchant. +7. Click **Show more** to add additional fields (like a category) as desired, or as required by your workspace. +8. Click **Submit**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon at the bottom of the screen and select **Submit Expense**. +2. Tap **Manual**. +3. Enter the amount on the receipt and tap **Next**. *Note: Click the currency symbol to select a different currency.* +4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. +5. (Optional) Add a description. +6. Add a merchant. +7. Tap **Show more** to add additional fields (like a category) as desired, or as required by your workspace. +8. Tap **Submit**. +{% include end-option.html %} + +{% include end-selector.html %} + +# Create a distance expense + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Submit Expense**. +2. Click **Distance**. +3. Click **Start** and enter the starting location of your trip. +4. Click **Stop** and enter the ending location of your trip. +5. (Optional) Click **Add stop** to add additional stops, if applicable. +6. Tap **Next**. +7. Use the search field to find the desired workspace or an individual’s name, email, or phone number. +8. (Optional) Add a description. +9. Click **Submit**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon at the bottom of the screen and select **Submit Expense**. +2. Tap **Distance**. +3. Tap **Start** and enter the starting location of your trip. +4. Tap **Stop** and enter the ending location of your trip. +5. (Optional) Tap **Add stop** to add additional stops, if applicable. +6. Tap **Next**. +7. Use the search field to find the desired workspace or an individual’s name, email, or phone number. +8.(Optional) Add a description. +9. Tap **Submit**. +{% include end-option.html %} + +{% include end-selector.html %} + +# Next Steps + +The next steps for the expense depend on whether it was submitted to a workspace or to an individual: +- **Expenses submitted to a workspace** are automatically added to a report and checked for any violations or inconsistencies. A chat thread for the expense is also added to your chat inbox. When you open the chat, the top banner will show the expense status and any next steps. By default, reports are automatically submitted for approval every Sunday. However, if it is ready for early submission, you can manually submit a report for approval. Once a report is submitted, your approver will be prompted to review your expense report. If changes are required, you will receive a notification to resolve any violations and resubmit. You will also be notified once your approver approves or denies your expenses. +- **Expenses submitted to a friend** are sent right to that individual via email or text. You can chat with them about the expense in Expensify Chat, and you can receive payments through your Expensify Wallet or outside of Expensify. + +{% include faq-begin.md %} +**Can I divide a payment between multiple people?** + +Yes, you can split an expense to share the cost between multiple people. + +**Can I pay someone in another currency?** + +While you can record your expenses in different currencies, Expensify wallets are only available for members who can add a U.S. personal bank account. + +**Can I change an expense once I’ve submitted it?** + +Yes, you can edit an expense until it is paid. When an expense is submitted to a workspace, you, your approvers, and admins can edit the details on an expense except for the amount and date. + +**What are expense reports?** + +In Expensify, expenses are submitted on an expense report. When a draft report is open, all new expenses are added to the draft report. Once a report is submitted, it shows what stage of the approval process the expenses are in and any required next steps. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expenses/Manually-submit-reports-for-approval.md b/docs/articles/new-expensify/expenses/Manually-submit-reports-for-approval.md new file mode 100644 index 000000000000..0bfae655835f --- /dev/null +++ b/docs/articles/new-expensify/expenses/Manually-submit-reports-for-approval.md @@ -0,0 +1,43 @@ +--- +title: Manually submit reports for approval +description: Submit a report before the submission date +--- +
+ +By default, reports are automatically submitted for approval every Sunday. However, if it is ready for early submission, you can manually submit your report for approval. + +To manually submit an expense for approval, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. In your chat inbox, click the expense you want to submit for approval. *Note: A green dot will appear to the right of newly created expenses.* +2. Review the next step provided at the top of the expense report. +3. If the next step is to submit the report, click **Submit**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. In your chat inbox, tap the expense you want to submit for approval. *Note: A green dot will appear to the right of newly created expenses.* +2. Review the next step provided at the top of the expense report. +3. If the next step is to submit the report, tap **Submit**. +{% include end-option.html %} + +{% include end-selector.html %} + +{% include faq-begin.md %} +**How do I know the status of my expense report?** + +You’ll see a Next Steps prompt at the top of your expense report to guide you through the next steps and give you a status of your report. Your workspace may have an automation set up that will automatically submit your expense reports when they are due, or you may be required to manually submit your expenses. + +**Can I add more expenses to my expense report?** + +Yes, you can add expenses to a report that is in a Draft or Processing status. Once a report is Approved or Reimbursed, expenses cannot be added or edited. + +**How can I get reimbursed?** + +The reimbursement options depend on the workspace’s settings. +- If the workspace is set up to send reimbursements directly to your personal bank account, you can connect a personal bank account to receive ACH payments. +- You can receive reimbursements with your Expensify Wallet or outside of Expensify. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expenses/Request-Money.md b/docs/articles/new-expensify/expenses/Request-Money.md deleted file mode 100644 index 9aac4787484c..000000000000 --- a/docs/articles/new-expensify/expenses/Request-Money.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Request Money and Split Bills with Friends -description: Everything you need to know about Requesting Money and Splitting Bills with Friends! -redirect_from: articles/request-money/Request-and-Split-Bills/ ---- - - - -# How do these Payment Features work? -Our suite of money movement features enables you to request money owed by an individual or split a bill with a group. - -**Request Money** lets your friends pay you back directly in Expensify. When you send a payment request to a friend, Expensify will display the amount owed and the option to pay the corresponding request in a chat between you. - -**Split Bill** allows you to split payments between friends and ensures the person who settled the tab gets paid back. - -These two features ensure you can live in the moment and settle up afterward. - -# How to Request Money -- Select the Green **+** button and choose **Request Money** -- Enter the amount **$** they owe and click **Next** -- Search for the user or enter their email! -- Enter a reason for the request (optional) -- Click **Request!** -- If you change your mind, all you have to do is click **Cancel** -- The user will be able to **Settle up outside of Expensify** or pay you via **Venmo** or **PayPal.me** - -# How to Split a Bill -- Select the Green **+** button and choose **Split Bill** -- Enter the total amount for the bill and click **Next** -- Search for users or enter their emails and **Select** -- Enter a reason for the split -- The split is then shared equally between the attendees - -{% include faq-begin.md %} -## Can I request money from more than one person at a time? -If you need to request money for more than one person at a time, you’ll want to use the Split Bill feature. The Request Money option is for one-to-one payments between two people. -{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses/Send-an-invoice.md b/docs/articles/new-expensify/expenses/Send-an-invoice.md new file mode 100644 index 000000000000..588f0da20154 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Send-an-invoice.md @@ -0,0 +1,52 @@ +--- +title: Send an invoice +description: Notify a customer that a payment is due +--- +
+ +You can send invoices directly from Expensify to notify customers that a payment is due. + +To create and send an invoice, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and click **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including a description, date, category, tag, and/or tax. +5. Click **Send**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and tap **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including a description, date, category, tag, and/or tax. +5. Tap **Send**. +{% include end-option.html %} + +{% include end-selector.html %} + +# How the customer pays an invoice + +Once an invoice is sent, the customer receives an automated email or text message to notify them of the invoice. They can use this notification to pay the invoice whenever they are ready. They will: + +1. Click the link in the email or text notification they receive from Expensify. +2. Click **Pay**. +3. Choose **Paying as an individual** or **Paying as a business**. +4. Click **Pay Elsewhere**, which will mark the invoice as Paid. + +Currently, invoices must be paid outside of Expensify. However, the ability to make payments through Expensify is coming soon. + +# FAQs + +**How do I communicate with the sender/recipient about the invoice?** + +You can communicate with the recipient in New Expensify. After sending an invoice, Expensify automatically creates an invoice room between the invoice sender and the payer to discuss anything related to the invoice. You can invite users to join the conversation, remove them from the room, and leave the room at any time. + +**Can you import and export invoices between an accounting integration?** + +Yes, you can export and import invoices between Expensify and your QuickBooks Online or Xero integration. + +
diff --git a/docs/articles/new-expensify/expenses/Split-an-expense.md b/docs/articles/new-expensify/expenses/Split-an-expense.md new file mode 100644 index 000000000000..d411469fc298 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Split-an-expense.md @@ -0,0 +1,39 @@ +--- +title: Split an expense +description: Divide an expense between multiple people +--- +
+ +Splitting an expense allows the person who paid the bill to request money from multiple people who will split the cost with them. + +To split an expense with other people, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Split Expense**. +2. Upload a photo of your receipt, or manually enter the total bill amount. +3. Click **Next**. +4. Enter the names, email addresses, or phone numbers for the people you want to request money from. *Note: You can select multiple people.* +5. Click **Next**. +6. (Optional) Enter a reason for the request in the Description field. +7. (Optional) If you manually entered the bill amount, add the merchant and date of purchase. +8. Click **Split**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon at the bottom of the screen and select **Split Expense**. +2. Take a photo of your receipt, or manually enter the total bill amount. +3. Tap **Next**. +4. Enter the names, email addresses, or phone numbers for the people you want to request money from. Note: You can select multiple people +5. Tap **Next**. +6. (Optional) Enter a reason for the request in the Description field. +7. (Optional) If you manually entered the bill amount, add the merchant and date of purchase. +8. Click **Split**. +{% include end-option.html %} + +{% include end-selector.html %} + +Each person will receive an email or text with the details of the request. You can also chat with them about the expense in Expensify Chat, and you can receive payments through your Expensify Wallet or outside of Expensify. + +
diff --git a/docs/articles/new-expensify/getting-started/Coming-Soon.md b/docs/articles/new-expensify/getting-started/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/getting-started/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- diff --git a/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md b/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md new file mode 100644 index 000000000000..cbe21c9db20a --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md @@ -0,0 +1,104 @@ +--- +title: Create a company workspace +description: Get started with Expensify by creating a workspace for your company +--- +
+ +To create an Expensify account for your company, follow the 6 steps below. + +{% include info.html %} +After you create your new company workspace, you can schedule a free private onboarding session with one of our Setup Specialists. After you complete the steps below, check your email and notifications in Expensify for your unique signup link. +{% include end-info.html %} + +# 1. Meet Concierge + +Concierge is your personal assistant that walks you through setting up your account and also provides: +- Reminders to do things like submit your expenses +- Alerts when more information is needed on an expense report +- Updates on new and improved account features + +You can get support any time by locating your chat with Concierge in your chat inbox. You can ask questions and receive direct support in this thread. + +# 2. Create a new workspace + +
    +
  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. Click New workspace.
  6. +
  7. Click the Edit pencil icon next to your workspace image or icon and select Upload Image to choose an image from your saved files.
  8. +
  9. Click Name to enter a new name for the workspace.
  10. +
  11. Click Default Currency to set the currency for all expenses submitted under the workspace. Expensify automatically converts all other currencies to your default currency.
  12. +
+ +# 3. Invite members + +
    +
  1. From the workspace view, click Members on the left.
  2. +
  3. Click Invite member.
  4. +
  5. Use the search field to find the individual by name, email, or phone number. Note: You can select multiple people.
  6. +
  7. Click Next.
  8. +
  9. (Optional) Enter a custom message into the Message field.
  10. +
  11. Click Invite.
  12. +
+Once the invite is accepted, the new members will appear in your members list. + +{% include info.html %} +You can also invite members on the workspace’s Profile page by clicking **Share** to share the workspace’s URL or QR code. +{% include end-info.html %} + +# 4. Set admins + +Admins are members of your workspace that have permissions to manage the workspace. The table below shows the difference between member and admin permissions: + +| Employee | Workspace Admin | +| ----------------------------------------- | ------------------------------------------ | +| ✔ Submit their own reports | ✔ Submit their own reports | +| ✔ Approve reports submitted to them| ✔ Approve reports submitted to them | +| | ✔ View all workspace reports | +| | ✔ Approve all workspace reports | +| | ✔ Edit workspace settings | + +To assign a member as an admin, + +
    +
  1. From the Members page of your workspace, click any member’s name.
  2. +
  3. Click Role to change their role to Admin.
  4. +
+ +# 5. Add categories + +Categories help you code your expenses. + +
    +
  1. Click Categories in the left menu.
  2. +
  3. Disable or add categories.
  4. +
      +
    • To disable a category: Click the category, then click the green toggle to disable it.
    • +
    • To add a new category: Click Add category in the top right. Then enter a name for the category and click Save.
    • +
    +
+ +{% include info.html %} +Categories are enabled by default. However, if you want to completely disable all categories, you can do so by clicking **More Features** in the left menu and clicking the Categories toggle to disable it. +{% include end-info.html %} + +# 6. Add more features + +The items that appear in your left menu under each workspace are determined by the features that are enabled for the workspace. For example, you can choose to enable or disable any of the following features: +- Distance rates +- Workflows +- Categories +- Tags +- Taxes + +Once enabled, a new menu option for the feature will appear in the left menu will additional settings for the feature. + +To add more features, + +
    +
  1. Click More features in the left menu.
  2. +
  3. Enable any desired feature.
  4. +
  5. Click the related menu item that appears in the left menu to update its settings.
  6. +
+ +
diff --git a/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md b/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md new file mode 100644 index 000000000000..9c5aea0c61ae --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md @@ -0,0 +1,185 @@ +--- +title: Join your company's workspace +description: Get started with Expensify by joining your company's workspace +--- + +
+ +Welcome to Expensify! If you received an invitation to join your company’s Expensify workspace, follow the 5 steps below to get started. + +# 1. Download the mobile app + +Upload your expenses and check your reports right from your phone by downloading the Expensify mobile app. You can search for “Expensify” in the app store, or tap one of the links below. + +[iOS](https://apps.apple.com/us/app/expensify-expense-tracker/id471713959) +| [Android](https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&gl=US) + +# 2. Add your name, photo, and preferences + +{% 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 the Edit pencil icon next to your profile image or icon and select Upload Image to choose a new image from your saved files.
  4. +
  5. Click Profile in the left menu and edit any of the following details as desired:
  6. +
      +
    • Display Name: Enter your first and last name into the fields and click Save. This name will be visible to anyone in your company workspace.
    • +
    • Contact method: Review your primary email address and/or add a secondary email address.
    • +
    • Status: Update your status to let your coworkers know if you are out of the office, in a meeting, or unavailable.
    • +
    • Pronouns: Select your personal pronouns.
    • +
    • Timezone: Select your timezone.
    • +
    • Private details: Add private details that are only visible to you, such as your legal name, date of birth, and/or address. This is useful for booking travel and for payment purposes.
    • +
    +
+{% include end-option.html %} + +{% include option.html value="mobile" %} +
    +
  1. Tap your profile image or icon in the bottom menu.
  2. +
  3. Tap the Edit pencil icon next to your profile image or icon and select Upload Image to choose a new image from your saved files.
  4. +
  5. Tap Profile in the left menu and edit any of the following details as desired:
  6. +
      +
    • Display Name: Enter your first and last name into the fields and tap Save. This name will be visible to anyone in your company workspace.
    • +
    • Contact method: Review your primary email address and/or add a secondary email address.
    • +
    • Status: Update your status to let your coworkers know if you are out of the office, in a meeting, or unavailable.
    • +
    • Pronouns: Select your personal pronouns.
    • +
    • Timezone: Select your timezone.
    • +
    • Private details: Add private details that are only visible to you, such as your legal name, date of birth, and/or address. This is useful for booking travel and for payment purposes.
    • +
    +
+{% include end-option.html %} + +{% include end-selector.html %} + +# 3. Meet Concierge + +Concierge is your personal assistant that walks you through setting up your account and also provides: +- Reminders to do things like submit your expenses +- Alerts when more information is needed on an expense report +- Updates on new and improved account features + +You can get support any time by locating your chat with Concierge in your chat inbox. You can ask questions and receive direct support in this thread. + +# 4. Learn how to add an expense + +You can create an expense by SmartScanning a receipt to automatically capture the receipt details, or you can enter the expense manually. + +## SmartScan a receipt + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +
    +
  1. Click the + icon in the bottom left menu and select Submit Expense.
  2. +
  3. Click Scan.
  4. +
  5. Drag and drop the receipt into Expensify, or click Choose File to select it from your saved files. Note: The SmartScan process will auto-populate the merchant, date, and amount.
  6. +
  7. Use the search field to find the desired workspace or an individual’s name, email, or phone number.
  8. +
  9. Add a description, category, tags, or tax as desired, or as required by your workspace.
  10. +
  11. (Optional) Enable the expense as billable if it should be billed to a client.
  12. +
  13. Click Submit Expense.
  14. +
+{% include end-option.html %} + +{% include option.html value="mobile" %} +
    +
  1. Tap the + icon at the bottom of the screen and select Submit Expense.
  2. +
  3. Tap Scan.
  4. +
  5. Tap the green button to take a photo of a receipt, or tap the Image icon to the left of it to upload a receipt from your phone. Note: The SmartScan process will auto-populate the merchant, date, and amount.
  6. +
  7. Use the search field to find the desired workspace or an individual’s name, email, or phone number.
  8. +
  9. Add a description, category, tags, or tax as desired, or as required by your workspace.
  10. +
  11. (Optional) Enable the expense as billable if it should be billed to a client.
  12. +
  13. Tap Submit.
  14. +
+{% include end-option.html %} + +{% include end-selector.html %} + +{% include info.html %} +You can also forward receipts to receipts@expensify.com using an email address that is your primary or secondary email address. SmartScan will automatically pull all of the details from the receipt and add it to your expenses. +{% include end-info.html %} + +## Manually add an expense + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +
    +
  1. Click the + icon in the bottom left menu and select Submit Expense.
  2. +
  3. Click Manual.
  4. +
  5. Enter the amount on the receipt and click Next. Note: Click the currency symbol to select a different currency.
  6. +
  7. Use the search field to find the desired workspace or an individual’s name, email, or phone number.
  8. +
  9. (Optional) Add a description.
  10. +
  11. Add a merchant.
  12. +
  13. Click Show more to add additional fields (like a category) as desired, or as required by your workspace.
  14. +
  15. Click Submit.
  16. +
+{% include end-option.html %} + +{% include option.html value="mobile" %} +
    +
  1. Tap the + icon at the bottom of the screen and select Submit Expense.
  2. +
  3. Tap Manual.
  4. +
  5. Enter the amount on the receipt and tap Next. Note: Tap the currency symbol to select a different currency.
  6. +
  7. Use the search field to find the desired workspace or an individual’s name, email, or phone number.
  8. +
  9. (Optional) Add a description.
  10. +
  11. Add a merchant.
  12. +
  13. Tap Show more to add additional fields (like a category) as desired, or as required by your workspace.
  14. +
  15. Tap Submit.
  16. +
+{% include end-option.html %} + +{% include end-selector.html %} + +# 5. Secure your account + +Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication. This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. + +To enable two-factor authentication, + +{% 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 Security.
  4. +
  5. Click Two-factor authentication.
  6. +
  7. Save a copy of your backup codes. This step is critical! You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes.
  8. +
      +
    • Click Download to save a copy of your backup codes to your computer.
    • +
    • Click Copy to paste the codes into a document or other secure location.
    • +
    +
  9. Click Next.
  10. +
  11. Download or open your authenticator app and connect it to Expensify by either:
  12. +
      +
    • Scanning the QR code
    • +
    • Entering the code into your authenticator app
    • +
    +
  13. Enter the 6-digit code from your authenticator app into Expensify and click Verify.
  14. +
+{% include end-option.html %} + +{% include option.html value="mobile" %} +
    +
  1. Tap your profile image or icon at the bottom of the screen.
  2. +
  3. Tap Security.
  4. +
  5. Tap Two-factor authentication.
  6. +
  7. Save a copy of your backup codes. This step is critical! You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes.
  8. +
      +
    • Tap Download to save a copy of your backup codes to your device
    • +
    • Tap Copy to paste the codes into a document or other secure location.
    • +
    +
  9. Tap Next.
  10. +
  11. Download or open your authenticator app and connect it to Expensify by either:
  12. +
      +
    • Scanning the QR code
    • +
    • Entering the code into your authenticator app
    • +
    +
  13. Enter the 6-digit code from your authenticator app into Expensify and tap Verify.
  14. +
+ +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan.md b/docs/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan.md new file mode 100644 index 000000000000..80ee4d46b444 --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan.md @@ -0,0 +1,53 @@ +--- +title: Upgrade from a Free plan to Collect +description: You've been automatically upgraded to a Collect plan +--- +
+ +All customers on a Free plan have been automatically upgraded to a Collect plan! + +The Collect plan is an enhanced version of the Free Plan and unlocks several new features: +- A dedicated Setup Specialist +- Expense approvals +- Invoicing and bill pay +- Custom expense categories and tags +- Multiple active expense reports at a time +- Direct connection to Quickbooks Online +- Direct connection to Xero (coming soon!) + +**The upgrade is free until June 1st!** + +To give you time to try the upgraded plan, you won't be charged for another 30 days. Additionally, if you add a payment card by June 1st, you’ll qualify for a discount over the next year. The discount will be applied to your Expensify bill on a sliding scale for the first 12 months on the Collect plan. + +If you have questions, contact your dedicated Setup Specialist using the #admins room, or email Expensify Support at concierge@expensify.com. + +{% include faq-begin.md %} + +**Is the upgrade optional?** + +No, the upgrade is not optional. This upgrade ensures that every customer gets access to Expensify's best offerings and any new features going forward. + +**Does this mean Expensify will no longer be free for me?** + +Yes, but only if you want to continue using a workspace in Expensify. As always, you can still use Expensify for free without a workspace to track your expenses, chat with your friends, etc. + +**How does the sliding-scale discount work?** + +You’ll receive a discount on your Expensify bill that gradually decreases each month until you reach the full payment amount on June 1, 2025. For the first month, you’ll pay 1/12 of the Collect plan’s cost, and the price will gradually increase over the course of a year. + +**Let's break that down:** +- July 1: You'll receive a ~90% discount on your monthly bill +- December 1: You'll receive a 50% discount +- March 1, 2025: You'll receive a 25% discount +- May 1, 2025: You'll receive a ~10% discount +- June 1, 2025: You pay the full bill amount for the Collect plan (starting at $5 per active member) from this date forward + +The discount will be reflected on your monthly Expensify bill as a “Workspace upgrade discount” and can be combined with any other Expensify discounts—- the annual subscription discount, the Expensify Card discount, and Expensify Card cash back. + +**How do I get in touch with my Setup Specialist?** + +You can reach your Setup Specialist by opening your workspace’s #admins room in your chat inbox and sending a message. + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/settings/Add-personal-information.md b/docs/articles/new-expensify/settings/Add-personal-information.md new file mode 100644 index 000000000000..492d349357ec --- /dev/null +++ b/docs/articles/new-expensify/settings/Add-personal-information.md @@ -0,0 +1,27 @@ +--- +title: Add personal information +description: Add your legal name, DOB, and/or address for travel and payments +--- +
+ +You can add private details to your Expensify account that are only visible to you, such as your legal name, date of birth, and/or address. This information is useful for booking travel and for payment purposes. + +To add or update your private account 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 **Profile** in the left menu. +3. Scroll down to the Private details section and click the Legal Name, Date of Birth, and/or Address fields to update them. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Profile** in the left menu. +3. Scroll down to the Private details section and tap the Legal Name, Date of Birth, and/or Address fields to update them. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/settings/Change-or-add-email-address.md b/docs/articles/new-expensify/settings/Change-or-add-email-address.md new file mode 100644 index 000000000000..28ef00bc5d16 --- /dev/null +++ b/docs/articles/new-expensify/settings/Change-or-add-email-address.md @@ -0,0 +1,45 @@ +--- +title: Change or add email address +description: Add additional email addresses for your Expensify account or update your email +--- +
+ +The default email address on your Expensify account is the email that receives email updates and notifications for your account. You can add additional contact methods in order to +- Change your default email to a new one. +- Connect your personal email address as an additional way to log in if your default email address is one from your employer. This allows you to always have access to your Expensify account, even if your employer changes. + +{% include info.html %} +Before you can remove a default email address, you must add a new one to your Expensify account and make it the default using the steps below. Email addresses must be added as a contact method before they can be made the default. +{% include end-info.html %} + +To change or add an email address, + +{% 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 **Profile** in the left menu. +3. Click **Contact method**. +4. Click **New contact method**. +5. Enter the email address or phone number you want to use as a new default or contact method. +6. Click **Add**. +7. You’ll receive an email with a code to verify your email address. Enter the code into the field in Expensify and click **Verify**. + +You can click any email address in your list to set it as the default, remove it, or verify it. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Profile** in the left menu. +3. Tap **Contact method**. +4. Tap **New contact method**. +5. Enter the email address or phone number you want to use as a new default or secondary email. +6. Tap **Add**. +7. You’ll receive an email with a code to verify your email address. Enter the code into the field in Expensify and tap **Verify**. + +You can tap any email address in your list to set it as the default, remove it, or verify it. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/settings/Set-timezone.md b/docs/articles/new-expensify/settings/Set-timezone.md new file mode 100644 index 000000000000..11ce1340c7bb --- /dev/null +++ b/docs/articles/new-expensify/settings/Set-timezone.md @@ -0,0 +1,23 @@ +--- +title: Set timezone +description: Set your timezone +--- +
+ +{% 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 **Profile** in the left menu. +3. Click **Timezone** to select your timezone. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Profile** in the left menu +3. Tap **Timezone** to select your timezone. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/settings/Update-your-name.md b/docs/articles/new-expensify/settings/Update-your-name.md new file mode 100644 index 000000000000..d6b65def12ac --- /dev/null +++ b/docs/articles/new-expensify/settings/Update-your-name.md @@ -0,0 +1,33 @@ +--- +title: Update your name +description: Update your display or legal name +--- +
+ +Your Expensify account includes two names: +- Your display name that everyone can see (which can include a nickname) +- Your legal name that only you can see (for booking travel and for payment purposes) + +To update your display or legal name, + +{% 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 **Profile** in the left menu. +3. Edit your name. + - **Display name**: Click **Display Name** and enter your first name (or nickname) and last name into the fields and click **Save**. This name will be visible to anyone in your company workspace. + - **Legal name**: Scroll down to the Private Details section and click **Legal name**. Then enter your legal first and last name and click **Save**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap Profile in the left menu. +3. Edit your name. + - **Display name**: Tap **Display Name** and enter your first name (or nickname) and last name into the fields and tap **Save**. This name will be visible to anyone in your company workspace. + - **Legal name**: Scroll down to the Private Details section and tap **Legal name**. Then enter your legal first and last name and tap **Save**. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/settings/Update-your-profile-status.md b/docs/articles/new-expensify/settings/Update-your-profile-status.md new file mode 100644 index 000000000000..5e5130f69cd5 --- /dev/null +++ b/docs/articles/new-expensify/settings/Update-your-profile-status.md @@ -0,0 +1,34 @@ +--- +title: Update your profile status +description: Share your status with your team +--- +
+ +You can update your status in Expensify to let your coworkers know if you are out of the office, in a meeting, or even list your work hours or a different message. This message will appear when someone clicks on your profile or in a chat conversation. + +{% 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 **Profile** in the left menu. +3. Click **Status**. +4. (Optional) Click the emoji icon to add an emoji. +5. Click the message field and enter a status. For example, out of office, in a meeting, at lunch, etc. +6. Click **Clear After** to select an expiration for the status. For example, if you select 30 minutes, the status will be automatically cleared after 30 minutes. +7. Click **Save**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Profile** in the left menu. +3. Tap **Status**. +4. (Optional) Tap the emoji icon to add an emoji. +5. Tap the message field and enter a status. For example, out of office, in a meeting, at lunch, Office Hours: M-F 8-5 PT, etc. +6. Tap **Clear After** to select an expiration for the status. For example, if you select 30 minutes, the status will be automatically cleared after 30 minutes. +7. Tap **Save**. +{% include end-option.html %} + +{% include end-selector.html %} + +
+ diff --git a/docs/articles/new-expensify/settings/Update-your-pronouns.md b/docs/articles/new-expensify/settings/Update-your-pronouns.md new file mode 100644 index 000000000000..bf0e902092ff --- /dev/null +++ b/docs/articles/new-expensify/settings/Update-your-pronouns.md @@ -0,0 +1,23 @@ +--- +title: Update your pronouns +description: Display your pronouns on your account +--- +
+ +{% 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 **Profile** in the left menu. +3. Click **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon at the bottom of the screen. +2. Tap **Profile** in the left menu. +3. Tap **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/assets/images/AdminissuedVirtualCards.png b/docs/assets/images/AdminissuedVirtualCards.png new file mode 100644 index 000000000000..88df9b2f3fec Binary files /dev/null and b/docs/assets/images/AdminissuedVirtualCards.png differ diff --git a/docs/new-expensify/hubs/connections/index.html b/docs/new-expensify/hubs/connections/index.html new file mode 100644 index 000000000000..e467ce8a0f3e --- /dev/null +++ b/docs/new-expensify/hubs/connections/index.html @@ -0,0 +1,6 @@ +--- +layout: default +title: Connections +--- + +{% include hub.html %} diff --git a/docs/redirects.csv b/docs/redirects.csv index 95404c2326a0..f775d2f97094 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -67,6 +67,7 @@ https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,ht https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support https://help.expensify.com/articles/expensify-classic/settings/Copilot,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/ +https://help.expensify.com/articles/expensify-classic/workspaces/Domains-Overview,https://help.expensify.com/expensify-classic/hubs/domains/ https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/reports/Currency,https://help.expensify.com/articles/expensify-classic/reports/Currency https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles,https://help.expensify.com/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit,https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit @@ -156,7 +157,16 @@ https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing- https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO,https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets https://help.expensify.com/articles/expensify-classic/workspaces/Categories,https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories +https://help.expensify.com/articles/expensify-classic/expenses/Per-Diem-Expenses.html,https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses +https://help.expensify.com/articles/expensify-classic/workspaces/Budgets.html,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets https://help.expensify.com/articles/expensify-classic/workspaces/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags https://help.expensify.com/expensify-classic/hubs/manage-employees-and-report-approvals,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows +https://help.expensify.com/articles/expensify-classic/expenses/expenses/Add-an-expense,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/expenses/Apply-Tax,https://help.expensify.com/articles/expensify-classic/expenses/Apply-Tax +https://help.expensify.com/articles/expensify-classic/expenses/expenses/Merge-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Merge-expenses +https://help.expensify.com/articles/expensify-classic/expenses/reports/Reimbursements,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursements +https://help.expensify.com/articles/new-expensify/expenses/Referral-Program,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Referral-Program https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-and-Comments,https://help.expensify.com/articles/expensify-classic/reports/Print-or-download-a-report https://help.expensify.com/articles/expensify-classic/reports/The-Reports-Page,https://help.expensify.com/articles/expensify-classic/reports/Report-statuses +https://help.expensify.com/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan,https://help.expensify.com/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan +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 diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index e7ce320f65d6..9a9ca9c7dcbb 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 059DC4EFD39EF39437E6823D /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */; }; 083353EB2B5AB22A00C603C0 /* attention.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E72B5AB22900C603C0 /* attention.mp3 */; }; 083353EC2B5AB22A00C603C0 /* done.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E82B5AB22900C603C0 /* done.mp3 */; }; 083353ED2B5AB22A00C603C0 /* receive.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E92B5AB22900C603C0 /* receive.mp3 */; }; @@ -26,7 +25,6 @@ 26AF3C3540374A9FACB6C19E /* ExpensifyMono-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */; }; 2A9F8CDA983746B0B9204209 /* ExpensifyNeue-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 52796131E6554494B2DDB056 /* ExpensifyNeue-Bold.otf */; }; 30581EA8AAFD4FCE88C5D191 /* ExpensifyNeue-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */; }; - 3661A1374980E5F6804511FE /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */; }; 374FB8D728A133FE000D84EF /* OriginImageRequestHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */; }; 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 383643672B6D4AE2005BB9AE /* DeviceCheck.framework */; }; 7041848526A8E47D00E09F4D /* RCTStartupTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */; }; @@ -36,14 +34,15 @@ 7F9DD8DA2B2A445B005E3AFA /* ExpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */; }; 7FD73C9E2B23CE9500420AF3 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */; }; 7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 976CCB5F8C921482E6AEAE71 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AB40AC8872A3DD6EF53D8B94 /* libPods-NewExpensify.a */; }; + 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */; }; + 9E17CB36A6B22BDD4BE53561 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */; }; + ACA597C323AA39404655647F /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EF33B19FC6A7FE676839430D /* libPods-NewExpensify-NewExpensifyTests.a */; }; BDB853621F354EBB84E619C2 /* ExpensifyNewKansas-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; - EEAE4F8907465429AA5B5520 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; FF941A8D48F849269AB85C9A /* ExpensifyNewKansas-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */; }; /* End PBXBuildFile section */ @@ -83,7 +82,6 @@ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 00E356EE1AD99517003FC87E /* NewExpensifyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewExpensifyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 083353E72B5AB22900C603C0 /* attention.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = attention.mp3; path = ../assets/sounds/attention.mp3; sourceTree = ""; }; 083353E82B5AB22900C603C0 /* done.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = done.mp3; path = ../assets/sounds/done.mp3; sourceTree = ""; }; 083353E92B5AB22900C603C0 /* receive.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = receive.mp3; path = ../assets/sounds/receive.mp3; sourceTree = ""; }; @@ -98,66 +96,53 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = NewExpensify/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = NewExpensify/main.m; sourceTree = ""; }; 18D050DF262400AF000D658B /* BridgingFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgingFile.swift; sourceTree = ""; }; - 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 1DDE5449979A136852B939B5 /* Pods-NewExpensify.release adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release adhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release adhoc.xcconfig"; sourceTree = ""; }; - 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; - 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; - 32181F72DC539FFD1D1F0CA4 /* Pods-NewExpensify.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseproduction.xcconfig"; sourceTree = ""; }; - 34A8FDD1F9AA58B8F15C8380 /* Pods-NewExpensify.release production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release production.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release production.xcconfig"; sourceTree = ""; }; + 1C839668BD5515A8ADE6B15E /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseproduction.xcconfig"; sourceTree = ""; }; + 24454472DB373F58A96B1B5C /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; sourceTree = ""; }; + 289D101B1119F719AAC9EB8B /* Pods-NewExpensify.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugadhoc.xcconfig"; sourceTree = ""; }; 374FB8D528A133A7000D84EF /* OriginImageRequestHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OriginImageRequestHandler.h; path = NewExpensify/OriginImageRequestHandler.h; sourceTree = ""; }; 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = OriginImageRequestHandler.mm; path = NewExpensify/OriginImageRequestHandler.mm; sourceTree = ""; }; 383643672B6D4AE2005BB9AE /* DeviceCheck.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceCheck.framework; path = System/Library/Frameworks/DeviceCheck.framework; sourceTree = SDKROOT; }; - 3BBA44B891E03FAB8255E6F1 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; + 3EA3D64F00384537597190CE /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; sourceTree = ""; }; + 3F17376D588832EE0C4E7E13 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; sourceTree = ""; }; + 417E30386DDC804B3693037A /* Pods-NewExpensify.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseproduction.xcconfig"; sourceTree = ""; }; 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-Medium.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-Medium.otf"; sourceTree = ""; }; + 46B1FE4DE317D30C25A74C15 /* Pods-NewExpensify.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugdevelopment.xcconfig"; sourceTree = ""; }; + 48E7775E0D42D3E3F53A5B99 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; sourceTree = ""; }; + 4A39BBFB1A6AA6A0EB08878C /* Pods-NotificationServiceExtension.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugproduction.xcconfig"; sourceTree = ""; }; 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify/ExpoModulesProvider.swift"; sourceTree = ""; }; - 4E9593A0EE1C84B8A8EC062F /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; 52796131E6554494B2DDB056 /* ExpensifyNeue-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Bold.otf"; path = "../assets/fonts/native/ExpensifyNeue-Bold.otf"; sourceTree = ""; }; - 52E63EFD054926BFEA3EC143 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig"; sourceTree = ""; }; - 68F4F270A8D1414FC14F356F /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; + 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 687A6DD50C2B5D0DC530C207 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; sourceTree = ""; }; 7041848326A8E40900E09F4D /* RCTStartupTimer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTStartupTimer.h; path = NewExpensify/RCTStartupTimer.h; sourceTree = ""; }; 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTStartupTimer.m; path = NewExpensify/RCTStartupTimer.m; sourceTree = ""; }; 70CF6E81262E297300711ADC /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = NewExpensify/BootSplash.storyboard; sourceTree = ""; }; - 76BE68DA894BB75DDFE278DC /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; - 7B318CF669A0F7FE948D5CED /* Pods-NewExpensify.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugadhoc.xcconfig"; sourceTree = ""; }; 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpError.swift; sourceTree = ""; }; 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 7FD73C9F2B23CE9500420AF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; - 8D3B36BF88E773E3C1A383FA /* Pods-NewExpensify.debug staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debug staging.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debug staging.xcconfig"; sourceTree = ""; }; - 90E08F0C8C924EDA018C8866 /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseproduction.xcconfig"; sourceTree = ""; }; - 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release production.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release production.xcconfig"; sourceTree = ""; }; - AB40AC8872A3DD6EF53D8B94 /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 8EFE0319D586C1078DB926FD /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; + 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + BBE493797E97F2995E627244 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify-NewExpensifyTests/ExpoModulesProvider.swift"; sourceTree = ""; }; - BD6E1BA27D6ABE0AC9D70586 /* Pods-NewExpensify-NewExpensifyTests.release development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release development.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release development.xcconfig"; sourceTree = ""; }; - BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; - C3788801E65E896FA7C77298 /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; - C3FF914C045A138C061D306E /* Pods-NotificationServiceExtension.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugproduction.xcconfig"; sourceTree = ""; }; - CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; + C0417E996D1C834CDF0BF0F7 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig"; sourceTree = ""; }; D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-MediumItalic.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf"; sourceTree = ""; }; - D3F458C994019E6A571461B7 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; - DB76E0D5C670190A0997C71E /* Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig"; sourceTree = ""; }; + D7C206AC464C89FB4899E0AD /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; sourceTree = ""; }; + D846D749FDDC2C914007C87D /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Bold.otf"; path = "../assets/fonts/native/ExpensifyMono-Bold.otf"; sourceTree = ""; }; DD7904292792E76D004484B4 /* RCTBootSplash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTBootSplash.h; path = NewExpensify/RCTBootSplash.h; sourceTree = ""; }; DD79042A2792E76D004484B4 /* RCTBootSplash.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTBootSplash.mm; path = NewExpensify/RCTBootSplash.mm; sourceTree = ""; }; - E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; - E2F1036F70CBFE39E9352674 /* Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig"; sourceTree = ""; }; - E2F78D2A9B3DB96F0524690B /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig"; sourceTree = ""; }; - E61AD6D2DE65B6FB14945CDF /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; sourceTree = ""; }; - E681F80D97E6E4BB26194246 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig"; sourceTree = ""; }; + E5428460BDBED9E1BA8B3599 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; }; E704648954784DDFBAADF568 /* ExpensifyMono-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Regular.otf"; path = "../assets/fonts/native/ExpensifyMono-Regular.otf"; sourceTree = ""; }; E9DF872C2525201700607FDC /* AirshipConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AirshipConfig.plist; sourceTree = ""; }; - EA58D43E81BC49541F7FC7E7 /* Pods-NewExpensify.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugdevelopment.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; - F082D95EE104912B48EA98BA /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releasedevelopment.xcconfig"; sourceTree = ""; }; + EF33B19FC6A7FE676839430D /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + EFA5CA89CC675CA3370CF89E /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; F0C450E92705020500FD2970 /* colors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = colors.json; path = ../colors.json; sourceTree = ""; }; F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Regular.otf"; path = "../assets/fonts/native/ExpensifyNeue-Regular.otf"; sourceTree = ""; }; - FBEBA6FBED49FB41D6F93896 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugdevelopment.xcconfig"; sourceTree = ""; }; - FF0EADDA6099EF76253FA7AB /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig"; sourceTree = ""; }; + F8839E9820F4C312BD1C9339 /* Pods-NewExpensify.releasedevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releasedevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releasedevelopment.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -165,7 +150,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3661A1374980E5F6804511FE /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */, + ACA597C323AA39404655647F /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -175,9 +160,8 @@ files = ( 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - 976CCB5F8C921482E6AEAE71 /* libPods-NewExpensify.a in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - EEAE4F8907465429AA5B5520 /* libPods-NewExpensify.a in Frameworks */, + 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -185,7 +169,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 059DC4EFD39EF39437E6823D /* libPods-NotificationServiceExtension.a in Frameworks */, + 9E17CB36A6B22BDD4BE53561 /* libPods-NotificationServiceExtension.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -232,10 +216,9 @@ 383643672B6D4AE2005BB9AE /* DeviceCheck.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, - AEFE6CD54912D427D19133C7 /* libPods-NewExpensify.a */, - 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */, - AB40AC8872A3DD6EF53D8B94 /* libPods-NewExpensify.a */, - 076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */, + 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */, + EF33B19FC6A7FE676839430D /* libPods-NewExpensify-NewExpensifyTests.a */, + 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */, ); name = Frameworks; sourceTree = ""; @@ -340,37 +323,24 @@ EC29677F0A49C2946A495A33 /* Pods */ = { isa = PBXGroup; children = ( - 8D3B36BF88E773E3C1A383FA /* Pods-NewExpensify.debug staging.xcconfig */, - 1DDE5449979A136852B939B5 /* Pods-NewExpensify.release adhoc.xcconfig */, - 34A8FDD1F9AA58B8F15C8380 /* Pods-NewExpensify.release production.xcconfig */, - E2F1036F70CBFE39E9352674 /* Pods-NewExpensify-NewExpensifyTests.debug development.xcconfig */, - DB76E0D5C670190A0997C71E /* Pods-NewExpensify-NewExpensifyTests.debug production.xcconfig */, - BD6E1BA27D6ABE0AC9D70586 /* Pods-NewExpensify-NewExpensifyTests.release development.xcconfig */, - 96552D489D9F09B6A5ABD81B /* Pods-NewExpensify-NewExpensifyTests.release production.xcconfig */, - CE2F84BEE9A6DCC228AF7E42 /* Pods-NewExpensify.debugproduction.xcconfig */, - 30FFBD291B71222A393D9CC9 /* Pods-NewExpensify.releasedevelopment.xcconfig */, - BD8828A882E2D6B51362AAC3 /* Pods-NewExpensify.releaseadhoc.xcconfig */, - 8709DF3C8D91F0FC1581CDD7 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, - 25A4587E168FD67CF890B448 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, - E2C8555C607612465A7473F8 /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, - FBEBA6FBED49FB41D6F93896 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */, - D3F458C994019E6A571461B7 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */, - C3FF914C045A138C061D306E /* Pods-NotificationServiceExtension.debugproduction.xcconfig */, - F082D95EE104912B48EA98BA /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */, - E61AD6D2DE65B6FB14945CDF /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */, - 90E08F0C8C924EDA018C8866 /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */, - EA58D43E81BC49541F7FC7E7 /* Pods-NewExpensify.debugdevelopment.xcconfig */, - 7B318CF669A0F7FE948D5CED /* Pods-NewExpensify.debugadhoc.xcconfig */, - C3788801E65E896FA7C77298 /* Pods-NewExpensify.debugproduction.xcconfig */, - 76BE68DA894BB75DDFE278DC /* Pods-NewExpensify.releasedevelopment.xcconfig */, - 68F4F270A8D1414FC14F356F /* Pods-NewExpensify.releaseadhoc.xcconfig */, - 32181F72DC539FFD1D1F0CA4 /* Pods-NewExpensify.releaseproduction.xcconfig */, - 3BBA44B891E03FAB8255E6F1 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, - 4E9593A0EE1C84B8A8EC062F /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, - 52E63EFD054926BFEA3EC143 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */, - E681F80D97E6E4BB26194246 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */, - FF0EADDA6099EF76253FA7AB /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, - E2F78D2A9B3DB96F0524690B /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */, + 46B1FE4DE317D30C25A74C15 /* Pods-NewExpensify.debugdevelopment.xcconfig */, + 289D101B1119F719AAC9EB8B /* Pods-NewExpensify.debugadhoc.xcconfig */, + EFA5CA89CC675CA3370CF89E /* Pods-NewExpensify.debugproduction.xcconfig */, + F8839E9820F4C312BD1C9339 /* Pods-NewExpensify.releasedevelopment.xcconfig */, + 8EFE0319D586C1078DB926FD /* Pods-NewExpensify.releaseadhoc.xcconfig */, + 417E30386DDC804B3693037A /* Pods-NewExpensify.releaseproduction.xcconfig */, + E5428460BDBED9E1BA8B3599 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */, + C0417E996D1C834CDF0BF0F7 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */, + 3F17376D588832EE0C4E7E13 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */, + 687A6DD50C2B5D0DC530C207 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */, + D846D749FDDC2C914007C87D /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */, + 3EA3D64F00384537597190CE /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */, + 24454472DB373F58A96B1B5C /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */, + BBE493797E97F2995E627244 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */, + 4A39BBFB1A6AA6A0EB08878C /* Pods-NotificationServiceExtension.debugproduction.xcconfig */, + D7C206AC464C89FB4899E0AD /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */, + 48E7775E0D42D3E3F53A5B99 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */, + 1C839668BD5515A8ADE6B15E /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */, ); path = Pods; sourceTree = ""; @@ -382,13 +352,13 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "NewExpensifyTests" */; buildPhases = ( - A3D1E02743106A34295E533A /* [CP] Check Pods Manifest.lock */, + 9775BC4DC6243DE9D85D1821 /* [CP] Check Pods Manifest.lock */, 04B99F6AA578E2A877802F05 /* [Expo] Configure project */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - 822809AAD6B368BF9F9BA00E /* [CP] Embed Pods Frameworks */, - 5CC6761AF98472E1C710DB80 /* [CP] Copy Pods Resources */, + 79927A2A23B483ABEFC728A9 /* [CP] Embed Pods Frameworks */, + 47017CF8C1CFE59999D45CDC /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -404,7 +374,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "NewExpensify" */; buildPhases = ( - 468C095F6D4C79E555B55A4F /* [CP] Check Pods Manifest.lock */, + 0CB19F4D02046D8132BAA1CD /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, @@ -412,10 +382,10 @@ 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - CB8E29994749C6913C3FA05D /* [CP] Embed Pods Frameworks */, - F6E16E41F88F567A8CDD037C /* [CP] Copy Pods Resources */, - 04A2B3BE14CFE4961BE987E8 /* [CP-User] [RNFB] Core Configuration */, - 2D8F47B51A8E72FBA2BA4874 /* [CP-User] [RNFB] Crashlytics Configuration */, + 5E9C4526A040466B9CE57A2D /* [CP] Embed Pods Frameworks */, + FBC7D704E4E9CC08E91D7919 /* [CP] Copy Pods Resources */, + 9FF963998EFF771D82D473D2 /* [CP-User] [RNFB] Core Configuration */, + A2BE84E8C8EFD6C81A2B41F1 /* [CP-User] [RNFB] Crashlytics Configuration */, ); buildRules = ( ); @@ -431,7 +401,7 @@ isa = PBXNativeTarget; buildConfigurationList = 7FD73CAA2B23CE9500420AF3 /* Build configuration list for PBXNativeTarget "NotificationServiceExtension" */; buildPhases = ( - F3D35ED760B830954BD8A7BB /* [CP] Check Pods Manifest.lock */, + 0B960DEC1F581E1EB7F1342F /* [CP] Check Pods Manifest.lock */, 7FD73C972B23CE9500420AF3 /* Sources */, 7FD73C982B23CE9500420AF3 /* Frameworks */, 7FD73C992B23CE9500420AF3 /* Resources */, @@ -549,19 +519,6 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios relative | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n"; }; - 04A2B3BE14CFE4961BE987E8 /* [CP-User] [RNFB] Core Configuration */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "[CP-User] [RNFB] Core Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; - }; 04B99F6AA578E2A877802F05 /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -581,21 +538,29 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-NewExpensify-NewExpensifyTests/expo-configure-project.sh\"\n"; }; - 2D8F47B51A8E72FBA2BA4874 /* [CP-User] [RNFB] Crashlytics Configuration */ = { + 0B960DEC1F581E1EB7F1342F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", - "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NotificationServiceExtension-checkManifestLockResult.txt", ); - name = "[CP-User] [RNFB] Crashlytics Configuration"; runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 468C095F6D4C79E555B55A4F /* [CP] Check Pods Manifest.lock */ = { + 0CB19F4D02046D8132BAA1CD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -617,7 +582,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 5CC6761AF98472E1C710DB80 /* [CP] Copy Pods Resources */ = { + 47017CF8C1CFE59999D45CDC /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -672,16 +637,15 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-NewExpensify/expo-configure-project.sh\"\n"; }; - 822809AAD6B368BF9F9BA00E /* [CP] Embed Pods Frameworks */ = { + 5E9C4526A040466B9CE57A2D /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", + "${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", @@ -693,7 +657,6 @@ 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", @@ -703,41 +666,18 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - A3D1E02743106A34295E533A /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NewExpensify-NewExpensifyTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - CB8E29994749C6913C3FA05D /* [CP] Embed Pods Frameworks */ = { + 79927A2A23B483ABEFC728A9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", + "${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", @@ -749,7 +689,6 @@ 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", @@ -759,10 +698,10 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - F3D35ED760B830954BD8A7BB /* [CP] Check Pods Manifest.lock */ = { + 9775BC4DC6243DE9D85D1821 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -777,14 +716,41 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NotificationServiceExtension-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-NewExpensify-NewExpensifyTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F6E16E41F88F567A8CDD037C /* [CP] Copy Pods Resources */ = { + 9FF963998EFF771D82D473D2 /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; + A2BE84E8C8EFD6C81A2B41F1 /* [CP-User] [RNFB] Crashlytics Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", + "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Crashlytics Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; + }; + FBC7D704E4E9CC08E91D7919 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -895,7 +861,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3BBA44B891E03FAB8255E6F1 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */; + baseConfigurationReference = E5428460BDBED9E1BA8B3599 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -925,7 +891,7 @@ }; 00E356F71AD99517003FC87E /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E681F80D97E6E4BB26194246 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */; + baseConfigurationReference = 687A6DD50C2B5D0DC530C207 /* Pods-NewExpensify-NewExpensifyTests.releasedevelopment.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -952,7 +918,7 @@ }; 13B07F941A680F5B00A75B9A /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EA58D43E81BC49541F7FC7E7 /* Pods-NewExpensify.debugdevelopment.xcconfig */; + baseConfigurationReference = 46B1FE4DE317D30C25A74C15 /* Pods-NewExpensify.debugdevelopment.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -967,7 +933,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -992,7 +958,7 @@ }; 13B07F951A680F5B00A75B9A /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 76BE68DA894BB75DDFE278DC /* Pods-NewExpensify.releasedevelopment.xcconfig */; + baseConfigurationReference = F8839E9820F4C312BD1C9339 /* Pods-NewExpensify.releasedevelopment.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1006,7 +972,7 @@ DEVELOPMENT_TEAM = 368M544MTT; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1030,7 +996,7 @@ }; 7FD73CA42B23CE9500420AF3 /* DebugDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FBEBA6FBED49FB41D6F93896 /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */; + baseConfigurationReference = 24454472DB373F58A96B1B5C /* Pods-NotificationServiceExtension.debugdevelopment.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1089,7 +1055,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1116,7 +1082,7 @@ }; 7FD73CA52B23CE9500420AF3 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D3F458C994019E6A571461B7 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */; + baseConfigurationReference = BBE493797E97F2995E627244 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1174,7 +1140,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1201,7 +1167,7 @@ }; 7FD73CA62B23CE9500420AF3 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C3FF914C045A138C061D306E /* Pods-NotificationServiceExtension.debugproduction.xcconfig */; + baseConfigurationReference = 4A39BBFB1A6AA6A0EB08878C /* Pods-NotificationServiceExtension.debugproduction.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1260,7 +1226,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1287,7 +1253,7 @@ }; 7FD73CA72B23CE9500420AF3 /* ReleaseDevelopment */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F082D95EE104912B48EA98BA /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */; + baseConfigurationReference = D7C206AC464C89FB4899E0AD /* Pods-NotificationServiceExtension.releasedevelopment.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1340,7 +1306,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1366,7 +1332,7 @@ }; 7FD73CA82B23CE9500420AF3 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E61AD6D2DE65B6FB14945CDF /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */; + baseConfigurationReference = 48E7775E0D42D3E3F53A5B99 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1418,7 +1384,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1444,7 +1410,7 @@ }; 7FD73CA92B23CE9500420AF3 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 90E08F0C8C924EDA018C8866 /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */; + baseConfigurationReference = 1C839668BD5515A8ADE6B15E /* Pods-NotificationServiceExtension.releaseproduction.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -1497,7 +1463,7 @@ INFOPLIST_FILE = NotificationServiceExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationServiceExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1589,7 +1555,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1657,7 +1627,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -1735,7 +1709,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1745,7 +1723,7 @@ }; CF9AF93F29EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C3788801E65E896FA7C77298 /* Pods-NewExpensify.debugproduction.xcconfig */; + baseConfigurationReference = EFA5CA89CC675CA3370CF89E /* Pods-NewExpensify.debugproduction.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1759,7 +1737,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1784,7 +1762,7 @@ }; CF9AF94029EE9276001FA527 /* DebugProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 52E63EFD054926BFEA3EC143 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */; + baseConfigurationReference = 3F17376D588832EE0C4E7E13 /* Pods-NewExpensify-NewExpensifyTests.debugproduction.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -1880,7 +1858,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1890,7 +1872,7 @@ }; CF9AF94529EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7B318CF669A0F7FE948D5CED /* Pods-NewExpensify.debugadhoc.xcconfig */; + baseConfigurationReference = 289D101B1119F719AAC9EB8B /* Pods-NewExpensify.debugadhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconAdHoc; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -1904,7 +1886,7 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/NewExpensify/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1929,7 +1911,7 @@ }; CF9AF94629EE927A001FA527 /* DebugAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4E9593A0EE1C84B8A8EC062F /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */; + baseConfigurationReference = C0417E996D1C834CDF0BF0F7 /* Pods-NewExpensify-NewExpensifyTests.debugadhoc.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -2017,7 +1999,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -2029,7 +2015,7 @@ }; CF9AF94829EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 32181F72DC539FFD1D1F0CA4 /* Pods-NewExpensify.releaseproduction.xcconfig */; + baseConfigurationReference = 417E30386DDC804B3693037A /* Pods-NewExpensify.releaseproduction.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -2042,7 +2028,7 @@ DEVELOPMENT_TEAM = 368M544MTT; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2066,7 +2052,7 @@ }; CF9AF94929EE928E001FA527 /* ReleaseProduction */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E2F78D2A9B3DB96F0524690B /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */; + baseConfigurationReference = 3EA3D64F00384537597190CE /* Pods-NewExpensify-NewExpensifyTests.releaseproduction.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -2152,7 +2138,11 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = "$(inherited)"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-Wl", + "-ld_classic", + ); PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -2164,7 +2154,7 @@ }; CF9AF94E29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 68F4F270A8D1414FC14F356F /* Pods-NewExpensify.releaseadhoc.xcconfig */; + baseConfigurationReference = 8EFE0319D586C1078DB926FD /* Pods-NewExpensify.releaseadhoc.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIconAdHoc; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; @@ -2177,7 +2167,7 @@ DEVELOPMENT_TEAM = 368M544MTT; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT; INFOPLIST_FILE = NewExpensify/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2201,7 +2191,7 @@ }; CF9AF94F29EE9293001FA527 /* ReleaseAdHoc */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FF0EADDA6099EF76253FA7AB /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */; + baseConfigurationReference = D846D749FDDC2C914007C87D /* Pods-NewExpensify-NewExpensifyTests.releaseadhoc.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4f9ef7e60a8b..647c12d51353 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.64 + 1.4.70 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.64.6 + 1.4.70.2 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 09bd4b3d3b1e..c692d84e11a2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.64 + 1.4.70 CFBundleSignature ???? CFBundleVersion - 1.4.64.6 + 1.4.70.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 14d635225c6d..8bb62f9d5f56 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.64 + 1.4.70 CFBundleVersion - 1.4.64.6 + 1.4.70.2 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6ef495a65bd3..3cce12499c22 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,27 +138,6 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (6.2.1) - - FullStory (1.43.1) - - 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) @@ -1401,10 +1380,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-quick-sqlite (8.0.0-beta.2): - - React - - React-callinvoker + - react-native-quick-sqlite (8.0.6): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-release-profiler (0.1.6): - glog - hermes-engine @@ -1837,7 +1831,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.62): + - RNLiveMarkdown (0.1.69): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1855,9 +1849,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.62) + - RNLiveMarkdown/common (= 0.1.69) - Yoga - - RNLiveMarkdown/common (0.1.62): + - RNLiveMarkdown/common (0.1.69): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1878,13 +1872,13 @@ PODS: - Yoga - RNLocalize (2.2.6): - React-Core - - rnmapbox-maps (10.1.11): + - rnmapbox-maps (10.1.12): - MapboxMaps (~> 10.16.4) - React - React-Core - - rnmapbox-maps/DynamicLibrary (= 10.1.11) + - rnmapbox-maps/DynamicLibrary (= 10.1.12) - Turf - - rnmapbox-maps/DynamicLibrary (10.1.11): + - rnmapbox-maps/DynamicLibrary (10.1.12): - hermes-engine - MapboxMaps (~> 10.16.4) - RCT-Folly @@ -2197,7 +2191,6 @@ SPEC REPOS: - FirebasePerformance - FirebaseRemoteConfig - fmt - - FullStory - GoogleAppMeasurement - GoogleDataTransport - GoogleSignIn @@ -2246,10 +2239,6 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" - fullstory_react-native: - :path: "../node_modules/@fullstory/react-native" - FBReactNativeSpec: - :path: "../node_modules/react-native/React/FBReactNativeSpec" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: @@ -2465,8 +2454,6 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - FullStory: e035758fef275fb59c6471f61b179652aeca452b - fullstory_react-native: a56e2bb52753b69f01aab3ae876087db08488034 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -2481,14 +2468,14 @@ SPEC CHECKSUMS: libvmaf: 27f523f1e63c694d14d534cd0fddd2fab0ae8711 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 lottie-ios: 3d98679b41fa6fd6aff2352b3953dbd3df8a397e - lottie-react-native: 80bda323805fa62005afff0583d2927a89108f20 + lottie-react-native: d0e530160e1a0116ab567343d843033c496d0d97 MapboxCommon: 20466d839cc43381c44df09d19f7f794b55b9a93 MapboxCoreMaps: c21f433decbb295874f0c2464e492166db813b56 MapboxMaps: c3b36646b9038706bbceb5de203bcdd0f411e9d0 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062 - onfido-react-native-sdk: 81e930e77236a0fc3da90e6a6eb834734d8ec2f5 + onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2 Plaid: 7829e84db6d766a751c91a402702946d2977ddcb PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 @@ -2513,26 +2500,26 @@ SPEC CHECKSUMS: React-jsitracing: e8a2dafb9878dbcad02b6b2b88e66267fb427b74 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad - react-native-airship: 6ab7a7974d53f92b0c46548fc198f797fdbf371f - react-native-blob-util: a3ee23cfdde79c769c138d505670055de233b07a - react-native-cameraroll: 95ce0d1a7d2d1fe55bf627ab806b64de6c3e69e9 + react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 + react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b + react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 - react-native-geolocation: c1c21a8cda4abae6724a322458f64ac6889b8c2b + react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 - react-native-key-command: 74d18ad516037536c2f671ef0914bcce7739b2f5 + react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d - react-native-netinfo: 6479e7e2198f936e5abc14a3ec4d469ccbaf81e2 - react-native-pager-view: 9ac6bc0fb3fa31c6d403b253ee361e62ff7ccf7f - react-native-pdf: cd256a00b9d65cb1008dcca2792d7bfb8874838d - react-native-performance: 1aa5960d005159f4ab20be15b44714b53b44e075 - react-native-plaid-link-sdk: 93870f8cd1de8e0acca5cb5020188bdc94e15db6 - react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 - react-native-release-profiler: 42fc8e09b4f6f9b7d14cc5a15c72165e871c0918 + react-native-netinfo: 02d31de0e08ab043d48f2a1a8baade109d7b6ca5 + react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa + react-native-pdf: 762369633665ec02ac227aefe2f4558b92475c23 + react-native-performance: fb21ff0c9bd7a10789c69d948f25b0067d29f7a9 + react-native-plaid-link-sdk: 2a91ef7e257ae16d180a1ca14ba3041ae0836fbf + react-native-quick-sqlite: e3ab3e0a29d8c705f47a60aaa6ceaa42eb6a9ec1 + react-native-release-profiler: 14ccdc0eeb03bedf625cf68d53d80275a81b19dd react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c - react-native-safe-area-context: e8bdd57d9f8d34cc336f0ee6acb30712a8454446 + react-native-safe-area-context: 9d79895b60b8be151fdf6faef9d2d0591eeecc63 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 - react-native-webview: a5f5f316527235f869992aaaf05050776198806d + react-native-webview: f8ab7a37905b2366a3e849ce5992b9724f6a528d React-nativeconfig: d7af5bae6da70fa15ce44f045621cf99ed24087c React-NativeModulesApple: 0123905d5699853ac68519607555a9a4f5c7b3ac React-perflogger: 8a1e1af5733004bdd91258dcefbde21e0d1faccd @@ -2557,36 +2544,36 @@ SPEC CHECKSUMS: React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 - RNCClipboard: c73bbc2e9012120161f1012578418827983bfd0c - RNCPicker: c77efa39690952647b83d8085520bf50ebf94ecb - RNDeviceInfo: cbf78fdb515ae73e641ee7c6b474f77a0299e7e6 + RNCClipboard: 081418ae3b391b1012c3f41d045e5e39f1beed71 + RNCPicker: a37026a67de0cf1a33ffe8722783527e3b18ea9f + RNDeviceInfo: 449272e9faf2afe94a3fe2896d169e92277fffa8 RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e RNFBCrashlytics: 2061ca863e8e2fa1aae9b12477d7dfa8e88ca0f9 RNFBPerf: 389914cda4000fe0d996a752532a591132cbf3f9 - RNFlashList: 5b0e8311e4cf1ad91e410fd7c8526a89fb5826d1 + RNFlashList: 76c2fab003330924ab1a140d13aadf3834dc32e0 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb + RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 47dfb50244f9ba1caefbc0efc6404ba41bf6620a + RNLiveMarkdown: bfabd5938e5af5afc1e60e4e34286b17f8308184 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 - rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d - RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 + rnmapbox-maps: 211f3cb9d33b3b8737d6d21c16bd49e9d97be2f8 + RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 - RNReanimated: 605409e0d0ced6f2e194ae585fedc2f8a1935bf2 - RNScreens: 65a936f4e227b91e4a8e2a7d4c4607355bfefda0 + RNReanimated: 51db0fff543694d931bd3b7cab1a3b36bd86c738 + RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 - RNSVG: db32cfcad0a221fd175e0882eff7bcba7690380a + RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 SDWebImageAVIFCoder: 8348fef6d0ec69e129c66c9fe4d74fbfbf366112 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 - VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d diff --git a/jest/setup.ts b/jest/setup.ts index 174e59a7e493..488e3e36a1d3 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -2,11 +2,9 @@ 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 deleted file mode 100644 index 9edfccab9441..000000000000 --- a/jest/setupMockFullstoryLib.ts +++ /dev/null @@ -1,24 +0,0 @@ -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 e0b8f3b52c84..c4c89ebb99cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,25 @@ { "name": "new.expensify", - "version": "1.4.64-6", + "version": "1.4.70-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.64-6", + "version": "1.4.70-2", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.62", + "@expensify/react-native-live-markdown": "0.1.69", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", - "@fullstory/browser": "^2.0.3", - "@fullstory/react-native": "^1.4.0", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -41,7 +39,7 @@ "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.29", "@react-ng/bounds-observer": "^0.2.1", - "@rnmapbox/maps": "10.1.11", + "@rnmapbox/maps": "10.1.12", "@shopify/flash-list": "1.6.3", "@storybook/addon-a11y": "^8.0.6", "@storybook/addon-essentials": "^8.0.6", @@ -58,7 +56,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#c0f7f3b6558fbeda0527c80d68460d418afef219", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -80,7 +78,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "^1.0.12", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -102,7 +100,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.27", + "react-native-onyx": "2.0.32", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -110,7 +108,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.7.2", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", @@ -127,7 +125,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", - "react-pdf": "7.3.3", + "react-pdf": "^7.7.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -215,6 +213,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", + "eslint-plugin-testing-library": "^6.2.2", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", "html-webpack-plugin": "^5.5.0", "jest": "29.4.1", @@ -243,8 +242,8 @@ "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.9.3", + "webpack-cli": "^5.0.4", + "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" }, @@ -3570,9 +3569,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.62", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.62.tgz", - "integrity": "sha512-o70/tFIGZJ1U8U8aqTQu1HAZed6nt5LYWk74mrceRxQHOqsKhZgn2q5EuEy8EMIcnCGKjwxuDyZJbuRexgHx/A==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.69.tgz", + "integrity": "sha512-ZJG6f06lHrNb0s/92JyyvsSDGGZLdU/a/YXir2A5UFCiERVWkgJxcugsYbEMemh2HsWD6GXvhq1Sngj2H620nw==", "engines": { "node": ">= 18.0.0" }, @@ -5590,52 +5589,6 @@ "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.1.0", - "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-react-native/-/babel-plugin-react-native-1.1.0.tgz", - "integrity": "sha512-BqfSUdyrrYrZM286GzdHd3qCdbitxUAIM0Z+HpoOTGWVTLDpkFNNaRw5juq8YhYbcPm6BAtK0RMGY7CvcMNarA==", - "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" @@ -7140,9 +7093,10 @@ "license": "MIT" }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "dev": true, - "license": "MIT" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true }, "node_modules/@lwc/eslint-plugin-lwc": { "version": "1.7.2", @@ -9346,8 +9300,9 @@ } }, "node_modules/@rnmapbox/maps": { - "version": "10.1.11", - "license": "MIT", + "version": "10.1.12", + "resolved": "https://registry.npmjs.org/@rnmapbox/maps/-/maps-10.1.12.tgz", + "integrity": "sha512-2SjSlFZYWNr/6B/yEpIHF6rrmRf7xC08gNsoMzJCDb8bgYuMa7pNCcftA2Ko6NQnGijdbbbTJIZcG2jXePxGpw==", "dependencies": { "@turf/along": "6.5.0", "@turf/distance": "6.5.0", @@ -12222,9 +12177,10 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -12261,9 +12217,10 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -12331,22 +12288,25 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.13", - "license": "MIT", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.30", - "license": "MIT", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/fs-extra": { @@ -12406,6 +12366,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/http-proxy": { "version": "1.17.9", "dev": true, @@ -12517,8 +12482,9 @@ "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==" }, "node_modules/@types/mime": { - "version": "3.0.1", - "license": "MIT" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/minimatch": { "version": "3.0.5", @@ -12537,6 +12503,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -12663,9 +12638,10 @@ } }, "node_modules/@types/retry": { - "version": "0.12.0", - "dev": true, - "license": "MIT" + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true }, "node_modules/@types/scheduler": { "version": "0.16.2", @@ -12675,20 +12651,32 @@ "version": "7.5.4", "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/serve-index": { - "version": "1.9.1", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.0", - "license": "MIT", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dependencies": { - "@types/mime": "*", - "@types/node": "*" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/setimmediate": { @@ -12697,9 +12685,10 @@ "license": "MIT" }, "node_modules/@types/sockjs": { - "version": "0.3.33", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -12753,9 +12742,10 @@ } }, "node_modules/@types/ws": { - "version": "8.5.3", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -13597,31 +13587,42 @@ "license": "MIT" }, "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "1.5.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, - "license": "MIT", - "dependencies": { - "envinfo": "^7.7.3" + "engines": { + "node": ">=14.15.0" }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "1.7.0", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -14287,6 +14288,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/arr-diff": { "version": "4.0.0", "license": "MIT", @@ -15579,21 +15589,15 @@ "license": "MIT" }, "node_modules/bonjour-service": { - "version": "1.0.13", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, - "license": "MIT", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, - "node_modules/bonjour-service/node_modules/array-flatten": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -16009,6 +16013,21 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "license": "MIT", @@ -17922,6 +17941,22 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser-id": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", @@ -17937,6 +17972,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser/node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "dev": true, @@ -18353,15 +18400,11 @@ "license": "MIT", "optional": true }, - "node_modules/dns-equal": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/dns-packet": { - "version": "5.4.0", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, - "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -19568,14 +19611,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/eslint-plugin-jsx-a11y/node_modules/axobject-query": { "version": "3.2.1", "dev": true, @@ -19754,6 +19789,22 @@ "lodash": "^4.17.15" } }, + "node_modules/eslint-plugin-testing-library": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.2.tgz", + "integrity": "sha512-1E94YOTUDnOjSLyvOwmbVDzQi/WkKm3WVrMXu6SmBr6DN95xTGZmI6HJ/eOkSXh/DlheRsxaPsJvZByDBhWLVQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, "node_modules/eslint-plugin-you-dont-need-lodash-underscore": { "version": "6.12.0", "dev": true, @@ -20250,8 +20301,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", - "integrity": "sha512-zz0/y0apISP1orxXEQOgn+Uod45O4wVypwwtaqcDPV4dH1tC3i4L98NoLSZvLn7Y17EcceSkfN6QCEsscgFTDQ==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "integrity": "sha512-9BHjM3kZs7/dil0oykEQFkEhXjVD5liTttmO7ZYtPZkl4j6g97mubY2p9lYpWwpkWckUfvU7nGuZQjahw9xSFA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -23098,11 +23149,12 @@ } }, "node_modules/interpret": { - "version": "2.2.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/invariant": { @@ -23432,6 +23484,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -23501,6 +23586,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "license": "MIT", @@ -26796,6 +26893,16 @@ "language-subtag-registry": "~0.3.2" } }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, "node_modules/lazy-cache": { "version": "1.0.4", "license": "MIT", @@ -28449,8 +28556,9 @@ }, "node_modules/multicast-dns": { "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, - "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -29525,15 +29633,20 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -29883,7 +29996,9 @@ }, "node_modules/path2d-polyfill": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, "engines": { "node": ">=8" } @@ -29919,17 +30034,15 @@ } }, "node_modules/pdfjs-dist": { - "version": "3.6.172", - "license": "Apache-2.0", - "dependencies": { - "path2d-polyfill": "^2.0.1", - "web-streams-polyfill": "^3.2.1" - }, + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { - "canvas": "^2.11.2" + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" } }, "node_modules/peek-stream": { @@ -30946,9 +31059,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.6.tgz", - "integrity": "sha512-CdAnBSZaLCGLSEuiqWLzzXhV9Wvdf1VRixaXCrb3NFrXyeltahF7PY+u7eU6ynrWZGmNI6g0cMLPv0DQhJEeew==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.12.tgz", + "integrity": "sha512-RSIYTwQVKWFqZKtmtzd4JU/FnsqdGPBtHu/N6xl7TsauAFnEouUJNjmC7Rg/pd010OX1UvyraQKdBIZ5Pf2q0A==", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" @@ -30964,55 +31077,6 @@ "react-dom": "18.x" } }, - "node_modules/react-fast-pdf/node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, - "node_modules/react-fast-pdf/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==", - "dependencies": { - "clsx": "^2.0.0", - "dequal": "^2.0.3", - "make-cancellable-promise": "^1.3.1", - "make-event-props": "^1.6.0", - "merge-refs": "^1.2.1", - "pdfjs-dist": "3.11.174", - "prop-types": "^15.6.2", - "tiny-invariant": "^1.0.0", - "warning": "^4.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-fast-pdf/node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/react-freeze": { "version": "1.0.3", "license": "MIT", @@ -31367,9 +31431,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.27.tgz", - "integrity": "sha512-mNtXmJ2r7UwEym2J7Tu09M42QoxIhwEdiGYDw9v26wp/kQCJChKTP0yUrp8QdPKkcwywRFPVlNxt3Rx8Mp0hFg==", + "version": "2.0.32", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.32.tgz", + "integrity": "sha512-tB9wqMJGTLOYfrfplRP+9aq5JdD8w/hV/OZsMAVH+ewbE1zLY8OymUsAsIFdF1v+cB8HhehP569JVLZmhm6bsg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -31480,7 +31544,9 @@ } }, "node_modules/react-native-quick-sqlite": { - "version": "8.0.0-beta.2", + "version": "8.0.6", + "resolved": "git+ssh://git@github.com/margelo/react-native-quick-sqlite.git#abc91857d4b3efb2020ec43abd2a508563b64316", + "integrity": "sha512-/tBM6Oh8ye3d+hIhURRA9hlBausKqQmscgyt4ZcKluPjBti0bgLb0cyL8Gyd0cbCakaVgym25VyGjaeicV/01A==", "license": "MIT", "peerDependencies": { "react": "*", @@ -31893,17 +31959,19 @@ } }, "node_modules/react-pdf": { - "version": "7.3.3", - "license": "MIT", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.1.tgz", + "integrity": "sha512-cbbf/PuRtGcPPw+HLhMI1f6NSka8OJgg+j/yPWTe95Owf0fK6gmVY7OXpTxMeh92O3T3K3EzfE0ML0eXPGwR5g==", "dependencies": { "clsx": "^2.0.0", + "dequal": "^2.0.3", "make-cancellable-promise": "^1.3.1", "make-event-props": "^1.6.0", "merge-refs": "^1.2.1", - "pdfjs-dist": "3.6.172", + "pdfjs-dist": "3.11.174", "prop-types": "^15.6.2", "tiny-invariant": "^1.0.0", - "tiny-warning": "^1.0.0" + "warning": "^4.0.0" }, "funding": { "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" @@ -32701,14 +32769,15 @@ } }, "node_modules/rechoir": { - "version": "0.7.1", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, - "license": "MIT", "dependencies": { - "resolve": "^1.9.0" + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/redent": { @@ -33098,8 +33167,9 @@ }, "node_modules/retry": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -33164,6 +33234,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-node": { "version": "1.0.0", "dev": true, @@ -33346,10 +33428,12 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.0.1", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "license": "MIT", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -35358,8 +35442,9 @@ }, "node_modules/thunky": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true }, "node_modules/time-analytics-webpack-plugin": { "version": "0.1.17", @@ -35465,10 +35550,6 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "license": "MIT" - }, "node_modules/tinycolor2": { "version": "1.6.0", "dev": true, @@ -36519,6 +36600,14 @@ "version": "0.1.1", "license": "MIT" }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "license": "MIT", @@ -36788,13 +36877,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "license": "BSD-2-Clause", @@ -36981,43 +37063,42 @@ } }, "node_modules/webpack-cli": { - "version": "4.10.0", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", - "commander": "^7.0.0", + "commander": "^10.0.1", "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=14.15.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x" + "webpack": "5.x.x" }, "peerDependenciesMeta": { "@webpack-cli/generators": { "optional": true }, - "@webpack-cli/migrate": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -37032,11 +37113,12 @@ "license": "MIT" }, "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=14" } }, "node_modules/webpack-dev-middleware": { @@ -37117,54 +37199,59 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.10.0", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", + "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.1", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { + "webpack": { + "optional": true + }, "webpack-cli": { "optional": true } @@ -37172,8 +37259,9 @@ }, "node_modules/webpack-dev-server/node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -37181,10 +37269,54 @@ "ajv": "^8.8.2" } }, + "node_modules/webpack-dev-server/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", @@ -37194,21 +37326,86 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "3.5.3", + "node_modules/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, - "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.4" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">= 4.0.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/webpack-dev-server/node_modules/schema-utils": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -37224,25 +37421,32 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz", + "integrity": "sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==", "dev": true, - "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-hot-middleware": { diff --git a/package.json b/package.json index 7b31a9771ce9..9530389b1fb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.64-6", + "version": "1.4.70-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -65,15 +65,13 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.62", + "@expensify/react-native-live-markdown": "0.1.69", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", - "@fullstory/browser": "^2.0.3", - "@fullstory/react-native": "^1.4.0", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -93,7 +91,7 @@ "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.29", "@react-ng/bounds-observer": "^0.2.1", - "@rnmapbox/maps": "10.1.11", + "@rnmapbox/maps": "10.1.12", "@shopify/flash-list": "1.6.3", "@storybook/addon-a11y": "^8.0.6", "@storybook/addon-essentials": "^8.0.6", @@ -110,7 +108,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#c0f7f3b6558fbeda0527c80d68460d418afef219", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -132,7 +130,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "^1.0.12", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -154,7 +152,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.27", + "react-native-onyx": "2.0.32", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -162,7 +160,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.7.2", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", @@ -179,7 +177,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", - "react-pdf": "7.3.3", + "react-pdf": "^7.7.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -267,6 +265,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", + "eslint-plugin-testing-library": "^6.2.2", "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", "html-webpack-plugin": "^5.5.0", "jest": "29.4.1", @@ -295,8 +294,8 @@ "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.9.3", + "webpack-cli": "^5.0.4", + "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" }, diff --git a/patches/@onfido+react-native-sdk+10.6.0.patch b/patches/@onfido+react-native-sdk+10.6.0.patch index 90e73ec197a1..225bbf3b9e46 100644 --- a/patches/@onfido+react-native-sdk+10.6.0.patch +++ b/patches/@onfido+react-native-sdk+10.6.0.patch @@ -1106,12 +1106,17 @@ index 0000000..3b65b7a +@end diff --git a/node_modules/@onfido/react-native-sdk/ios/RNOnfidoSdk.mm b/node_modules/@onfido/react-native-sdk/ios/RNOnfidoSdk.mm new file mode 100644 -index 0000000..4d21970 +index 0000000..998f79b --- /dev/null +++ b/node_modules/@onfido/react-native-sdk/ios/RNOnfidoSdk.mm -@@ -0,0 +1,59 @@ +@@ -0,0 +1,64 @@ +#import "RNOnfidoSdk.h" ++ ++#ifdef USE_FRAMEWORKS ++#import ++#else +#import ++#endif + +@implementation RNOnfidoSdk { + OnfidoSdk *_onfidoSdk; @@ -1189,7 +1194,7 @@ index 0000000..c48f86e + +export default TurboModuleRegistry.getEnforcing("RNOnfidoSdk"); diff --git a/node_modules/@onfido/react-native-sdk/js/Onfido.ts b/node_modules/@onfido/react-native-sdk/js/Onfido.ts -index db35471..8bb6a57 100644 +index db35471..fa6c1ef 100644 --- a/node_modules/@onfido/react-native-sdk/js/Onfido.ts +++ b/node_modules/@onfido/react-native-sdk/js/Onfido.ts @@ -1,4 +1,4 @@ @@ -1222,7 +1227,7 @@ index db35471..8bb6a57 100644 addCustomMediaCallback(callback: (result: OnfidoMediaResult) => OnfidoMediaResult) { diff --git a/node_modules/@onfido/react-native-sdk/onfido-react-native-sdk.podspec b/node_modules/@onfido/react-native-sdk/onfido-react-native-sdk.podspec -index a9de0d0..fcd6d14 100644 +index a9de0d0..da83d9f 100644 --- a/node_modules/@onfido/react-native-sdk/onfido-react-native-sdk.podspec +++ b/node_modules/@onfido/react-native-sdk/onfido-react-native-sdk.podspec @@ -2,6 +2,8 @@ require "json" @@ -1234,7 +1239,7 @@ index a9de0d0..fcd6d14 100644 Pod::Spec.new do |s| s.name = "onfido-react-native-sdk" s.version = package["version"] -@@ -15,10 +17,15 @@ Pod::Spec.new do |s| +@@ -15,10 +17,22 @@ Pod::Spec.new do |s| s.platforms = { :ios => "11.0" } s.source = { :git => "https://github.com/onfido/react-native-sdk.git", :tag => "#{s.version}" } @@ -1246,6 +1251,13 @@ index a9de0d0..fcd6d14 100644 - s.dependency "React" - s.dependency "Onfido", "~> 29.6.0" + s.dependency "Onfido", "~> 29.7.0" ++ ++ if ENV['USE_FRAMEWORKS'] == '1' ++ s.pod_target_xcconfig = { ++ "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", ++ "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", ++ } ++ end + + if defined?(install_modules_dependencies()) != nil + install_modules_dependencies(s) diff --git a/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch new file mode 100644 index 000000000000..dc45a6758d5c --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch @@ -0,0 +1,33 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index e338d90..70a59bf 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -1219,7 +1219,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; +- _sentStartForContentLength = 0; ++ _sentStartForFirstVisibleItemKey: ?string = null; + _sentEndForContentLength = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; +@@ -1550,16 +1550,16 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +- this._listMetrics.getContentLength() !== this._sentStartForContentLength ++ this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey + ) { +- this._sentStartForContentLength = this._listMetrics.getContentLength(); ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + if (!isWithinStartThreshold) { +- this._sentStartForContentLength = 0; ++ this._sentStartForFirstVisibleItemKey = null; + } + if (!isWithinEndThreshold) { + this._sentEndForContentLength = 0; diff --git a/patches/@rnmapbox+maps+10.1.11.patch b/patches/@rnmapbox+maps+10.1.11.patch deleted file mode 100644 index 9f2df5f4ee6e..000000000000 --- a/patches/@rnmapbox+maps+10.1.11.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt -index dbd6d0b..1d043f2 100644 ---- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt -+++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt -@@ -188,7 +188,7 @@ class RNMBXCamera(private val mContext: Context, private val mManager: RNMBXCame - - private fun setInitialCamera() { - mDefaultStop?.let { -- val mapView = mMapView!! -+ val mapView = mMapView ?: return - val map = mapView.getMapboxMap() - - it.setDuration(0) diff --git a/patches/@rnmapbox+maps+10.1.12.patch b/patches/@rnmapbox+maps+10.1.12.patch new file mode 100644 index 000000000000..c8135280056c --- /dev/null +++ b/patches/@rnmapbox+maps+10.1.12.patch @@ -0,0 +1,127 @@ +diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt +index 0d876a7..cceed39 100644 +--- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt ++++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt +@@ -188,7 +188,7 @@ class RNMBXCamera(private val mContext: Context, private val mManager: RNMBXCame + + private fun setInitialCamera() { + mDefaultStop?.let { +- val mapView = mMapView!! ++ val mapView = mMapView ?: return + val map = mapView.getMapboxMap() + + it.setDuration(0) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m +index 1808393..ec00542 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @interface RCT_EXTERN_MODULE(RNMBXOfflineModule, RCTEventEmitter) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +index 550f67b..76da02d 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @interface RCT_EXTERN_MODULE(RNMBXOfflineModuleLegacy, RCTEventEmitter) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m +index a98e102..e43be8f 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @interface RCT_EXTERN_MODULE(RNMBXTileStoreModule, NSObject) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m +index 62205d5..1db2ac4 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m +index e23b10c..6a023fa 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m +index 8b89774..9f85c35 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @class RNMBXLocation; +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m +index d7c05de..f680b86 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + + @interface RCT_EXTERN_MODULE(RNMBXLogging, NSObject) + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m +index 72f9928..f4f5fe2 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m +index c0ab14d..6177811 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m +index 3b0af79..e00b508 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + + @interface RCT_EXTERN_MODULE(RNMBXModule, NSObject) + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m +index 6fa19e5..54d0ff9 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.mm +similarity index 100% +rename from node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.m +rename to node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.mm diff --git a/patches/@shopify+flash-list+1.6.3.patch b/patches/@shopify+flash-list+1.6.3.patch index 4910bb20b4ec..ab347fbb4e9c 100644 --- a/patches/@shopify+flash-list+1.6.3.patch +++ b/patches/@shopify+flash-list+1.6.3.patch @@ -284,10 +284,24 @@ index 9c6bc58..0000000 - -- Initial release diff --git a/node_modules/@shopify/flash-list/RNFlashList.podspec b/node_modules/@shopify/flash-list/RNFlashList.podspec -index 38ff029..a749f6c 100644 +index 38ff029..f5f6c80 100644 --- a/node_modules/@shopify/flash-list/RNFlashList.podspec +++ b/node_modules/@shopify/flash-list/RNFlashList.podspec -@@ -9,14 +9,20 @@ Pod::Spec.new do |s| +@@ -2,6 +2,13 @@ require 'json' + + package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + ++default_config = { 'OTHER_SWIFT_FLAGS' => '-D RCT_NEW_ARCH_ENABLED', } ++ ++frameworks_flags = { ++ "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", ++ "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", ++} ++ + Pod::Spec.new do |s| + s.name = 'RNFlashList' + s.version = package['version'] +@@ -9,14 +16,24 @@ Pod::Spec.new do |s| s.homepage = package['homepage'] s.license = package['license'] s.author = package['author'] @@ -296,10 +310,14 @@ index 38ff029..a749f6c 100644 s.source_files = 'ios/Sources/**/*' s.requires_arc = true s.swift_version = '5.0' -+ s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-D RCT_NEW_ARCH_ENABLED', } ++ s.pod_target_xcconfig = default_config - # Dependencies - s.dependency 'React-Core' ++ if ENV['USE_FRAMEWORKS'] == '1' ++ s.pod_target_xcconfig = default_config.merge(frameworks_flags) ++ end ++ + if defined?(install_modules_dependencies()) != nil + install_modules_dependencies(s) + s.ios.deployment_target = "12.4" @@ -849,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..af1903c 100644 +index f18e92c..71b63dc 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 @@ -898,7 +916,7 @@ index f18e92c..af1903c 100644 @@ -46,7 +50,15 @@ import UIKit /// Tracks where first pixel is drawn in the visible window private var lastMinBound: CGFloat = 0 - + - override func layoutSubviews() { + private var viewsToLayout: [UIView] { + #if RCT_NEW_ARCH_ENABLED @@ -1035,10 +1053,10 @@ index 0000000..1ae0b66 +#endif /* AutoLayoutViewComponentView_h */ diff --git a/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutViewComponentView.mm b/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutViewComponentView.mm new file mode 100644 -index 0000000..2d4295c +index 0000000..6ef6a41 --- /dev/null +++ b/node_modules/@shopify/flash-list/ios/Sources/AutoLayoutViewComponentView.mm -@@ -0,0 +1,80 @@ +@@ -0,0 +1,86 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import "AutoLayoutViewComponentView.h" +#import @@ -1050,7 +1068,13 @@ index 0000000..2d4295c +#import + +#import "RCTFabricComponentsPlugins.h" ++ ++#ifdef USE_FRAMEWORKS ++#import ++#else +#import ++#endif ++ + +using namespace facebook::react; + @@ -1164,10 +1188,10 @@ index 0000000..ca1cbfe +#endif /* CellContainer_h */ diff --git a/node_modules/@shopify/flash-list/ios/Sources/CellContainerComponentView.mm b/node_modules/@shopify/flash-list/ios/Sources/CellContainerComponentView.mm new file mode 100644 -index 0000000..3a1c57e +index 0000000..ae489b8 --- /dev/null +++ b/node_modules/@shopify/flash-list/ios/Sources/CellContainerComponentView.mm -@@ -0,0 +1,57 @@ +@@ -0,0 +1,62 @@ +#import "CellContainerComponentView.h" + +#ifdef RCT_NEW_ARCH_ENABLED @@ -1179,7 +1203,12 @@ index 0000000..3a1c57e +#import + +#import "RCTFabricComponentsPlugins.h" ++ ++#ifdef USE_FRAMEWORKS ++#import ++#else +#import ++#endif + +using namespace facebook::react; + diff --git a/patches/react-native+0.73.4+014+fixPath.patch b/patches/react-native+0.73.4+014+fixPath.patch new file mode 100644 index 000000000000..ac49cca8621c --- /dev/null +++ b/patches/react-native+0.73.4+014+fixPath.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native/scripts/codegen/generate-artifacts-executor.js b/node_modules/react-native/scripts/codegen/generate-artifacts-executor.js +index 025f80c..d276c69 100644 +--- a/node_modules/react-native/scripts/codegen/generate-artifacts-executor.js ++++ b/node_modules/react-native/scripts/codegen/generate-artifacts-executor.js +@@ -454,7 +454,7 @@ function findCodegenEnabledLibraries( + codegenConfigFilename, + codegenConfigKey, + ) { +- const pkgJson = readPackageJSON(appRootDir); ++ const pkgJson = readPackageJSON(path.join(appRootDir, process.env.REACT_NATIVE_DIR ? 'react-native' : '')); + const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; + const libraries = []; + diff --git a/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch b/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch new file mode 100644 index 000000000000..7c4244f3a811 --- /dev/null +++ b/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/react-native/scripts/cocoapods/new_architecture.rb b/node_modules/react-native/scripts/cocoapods/new_architecture.rb +index ba75b019a9b9b2..c9999beb82b7ea 100644 +--- a/node_modules/react-native/scripts/cocoapods/new_architecture.rb ++++ b/node_modules/react-native/scripts/cocoapods/new_architecture.rb +@@ -105,6 +105,10 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version) + current_headers = current_config["HEADER_SEARCH_PATHS"] != nil ? current_config["HEADER_SEARCH_PATHS"] : "" + current_cpp_flags = current_config["OTHER_CPLUSPLUSFLAGS"] != nil ? current_config["OTHER_CPLUSPLUSFLAGS"] : "" + ++ flags_to_add = new_arch_enabled ? ++ "#{@@folly_compiler_flags} -DRCT_NEW_ARCH_ENABLED=1" : ++ "#{@@folly_compiler_flags}" ++ + header_search_paths = ["\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\""] + if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_ROOT)/DoubleConversion\"" +@@ -124,7 +128,7 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version) + } + end + header_search_paths_string = header_search_paths.join(" ") +- spec.compiler_flags = compiler_flags.empty? ? @@folly_compiler_flags : "#{compiler_flags} #{@@folly_compiler_flags}" ++ spec.compiler_flags = compiler_flags.empty? ? "$(inherited) #{flags_to_add}" : "$(inherited) #{compiler_flags} #{flags_to_add}" + current_config["HEADER_SEARCH_PATHS"] = current_headers.empty? ? + header_search_paths_string : + "#{current_headers} #{header_search_paths_string}" diff --git a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch deleted file mode 100644 index b5810c903873..000000000000 --- a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-quick-sqlite/android/build.gradle b/node_modules/react-native-quick-sqlite/android/build.gradle -index 323d34e..c2d0c44 100644 ---- a/node_modules/react-native-quick-sqlite/android/build.gradle -+++ b/node_modules/react-native-quick-sqlite/android/build.gradle -@@ -90,7 +90,6 @@ android { - externalNativeBuild { - cmake { - cppFlags "-O2", "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID" -- abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' - arguments '-DANDROID_STL=c++_shared', - "-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}", - "-DSQLITE_FLAGS='${SQLITE_FLAGS ? SQLITE_FLAGS : ''}'" diff --git a/patches/react-native-reanimated+3.7.2+002+copy-state.patch b/patches/react-native-reanimated+3.7.2+002+copy-state.patch new file mode 100644 index 000000000000..bd5899977ea9 --- /dev/null +++ b/patches/react-native-reanimated+3.7.2+002+copy-state.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/react-native-reanimated/Common/cpp/Fabric/ShadowTreeCloner.cpp b/node_modules/react-native-reanimated/Common/cpp/Fabric/ShadowTreeCloner.cpp +index f913ceb..3f58247 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/Fabric/ShadowTreeCloner.cpp ++++ b/node_modules/react-native-reanimated/Common/cpp/Fabric/ShadowTreeCloner.cpp +@@ -51,6 +51,7 @@ ShadowNode::Unshared cloneShadowTreeWithNewProps( + newChildNode = parentNode.clone({ + ShadowNodeFragment::propsPlaceholder(), + std::make_shared(children), ++ parentNode.getState() + }); + } + diff --git a/patches/react-native-vision-camera+4.0.0-beta.13.patch b/patches/react-native-vision-camera+4.0.0-beta.13.patch index 6a1cd1c74576..ecb635b7c195 100644 --- a/patches/react-native-vision-camera+4.0.0-beta.13.patch +++ b/patches/react-native-vision-camera+4.0.0-beta.13.patch @@ -1,8 +1,50 @@ diff --git a/node_modules/react-native-vision-camera/VisionCamera.podspec b/node_modules/react-native-vision-camera/VisionCamera.podspec -index 3a0e313..357a282 100644 +index 3a0e313..e983153 100644 --- a/node_modules/react-native-vision-camera/VisionCamera.podspec +++ b/node_modules/react-native-vision-camera/VisionCamera.podspec -@@ -40,6 +40,7 @@ Pod::Spec.new do |s| +@@ -2,7 +2,13 @@ require "json" + + package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +-nodeModules = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') ++pkgJsonPath = ENV['REACT_NATIVE_DIR'] ? '../react-native/package.json' : 'react-native/package.json' ++nodeModules = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('#{pkgJsonPath}')"`), '..') ++ ++frameworks_flags = { ++ "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", ++ "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", ++} + + forceDisableFrameProcessors = false + if defined?($VCDisableFrameProcessors) +@@ -15,6 +21,13 @@ workletsPath = File.join(nodeModules, "react-native-worklets-core") + hasWorklets = File.exist?(workletsPath) && !forceDisableFrameProcessors + Pod::UI.puts("[VisionCamera] react-native-worklets-core #{hasWorklets ? "found" : "not found"}, Frame Processors #{hasWorklets ? "enabled" : "disabled"}!") + ++default_config = { ++ "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) SK_METAL=1 SK_GANESH=1 VISION_CAMERA_ENABLE_FRAME_PROCESSORS=#{hasWorklets}", ++ "OTHER_SWIFT_FLAGS" => "$(inherited) -DRCT_NEW_ARCH_ENABLED #{hasWorklets ? "-D VISION_CAMERA_ENABLE_FRAME_PROCESSORS" : ""}", ++ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", ++ "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp/\"/** " ++} ++ + Pod::Spec.new do |s| + s.name = "VisionCamera" + s.version = package["version"] +@@ -27,19 +40,13 @@ Pod::Spec.new do |s| + s.platforms = { :ios => "12.4" } + s.source = { :git => "https://github.com/mrousavy/react-native-vision-camera.git", :tag => "#{s.version}" } + +- s.pod_target_xcconfig = { +- "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) SK_METAL=1 SK_GANESH=1 VISION_CAMERA_ENABLE_FRAME_PROCESSORS=#{hasWorklets}", +- "OTHER_SWIFT_FLAGS" => "$(inherited) #{hasWorklets ? "-D VISION_CAMERA_ENABLE_FRAME_PROCESSORS" : ""}", +- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", +- "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp/\"/** " +- } +- + s.requires_arc = true + + # All source files that should be publicly visible # Note how this does not include headers, since those can nameclash. s.source_files = [ # Core @@ -10,7 +52,7 @@ index 3a0e313..357a282 100644 "ios/*.{m,mm,swift}", "ios/Core/*.{m,mm,swift}", "ios/Extensions/*.{m,mm,swift}", -@@ -47,6 +48,7 @@ Pod::Spec.new do |s| +@@ -47,6 +54,7 @@ Pod::Spec.new do |s| "ios/React Utils/*.{m,mm,swift}", "ios/Types/*.{m,mm,swift}", "ios/CameraBridge.h", @@ -18,16 +60,18 @@ index 3a0e313..357a282 100644 # Frame Processors hasWorklets ? "ios/Frame Processor/*.{m,mm,swift}" : "", -@@ -66,9 +68,10 @@ Pod::Spec.new do |s| +@@ -66,9 +74,12 @@ Pod::Spec.new do |s| "ios/**/*.h" ] - + - s.dependency "React" - s.dependency "React-Core" - s.dependency "React-callinvoker" -+ s.pod_target_xcconfig = { -+ "OTHER_SWIFT_FLAGS" => "-DRCT_NEW_ARCH_ENABLED" -+ } ++ if ENV['USE_FRAMEWORKS'] == '1' ++ s.pod_target_xcconfig = default_config.merge(frameworks_flags) ++ else ++ s.pod_target_xcconfig = default_config ++ end + install_modules_dependencies(s) if hasWorklets @@ -609,8 +653,7 @@ index 0000000..56a6c3d +include ':VisionCamera' diff --git a/node_modules/react-native-vision-camera/android/src/main/.DS_Store b/node_modules/react-native-vision-camera/android/src/main/.DS_Store new file mode 100644 -index 0000000..63b728b -Binary files /dev/null and b/node_modules/react-native-vision-camera/android/src/main/.DS_Store differ +index 0000000..e69de29 diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraDevicesManager.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraDevicesManager.kt index a7c8358..a935ef6 100644 --- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraDevicesManager.kt @@ -1273,13 +1316,12 @@ index 0000000..46c2c2c +#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/node_modules/react-native-vision-camera/ios/RNCameraView.mm b/node_modules/react-native-vision-camera/ios/RNCameraView.mm new file mode 100644 -index 0000000..63fb00f +index 0000000..019be20 --- /dev/null +++ b/node_modules/react-native-vision-camera/ios/RNCameraView.mm -@@ -0,0 +1,373 @@ +@@ -0,0 +1,377 @@ +// This guard prevent the code from being compiled in the old architecture +#ifdef RCT_NEW_ARCH_ENABLED -+//#import "RNCameraView.h" +#import + +#import @@ -1292,7 +1334,12 @@ index 0000000..63fb00f +#import +#import +#import ++ ++#ifdef USE_FRAMEWORKS ++#import ++#else +#import "VisionCamera-Swift.h" ++#endif + +@interface RNCameraView : RCTViewComponentView +@end diff --git a/patches/react-native-web+0.19.9+007+osr-improvement.patch b/patches/react-native-web+0.19.9+007+osr-improvement.patch new file mode 100644 index 000000000000..074cac3d0e6f --- /dev/null +++ b/patches/react-native-web+0.19.9+007+osr-improvement.patch @@ -0,0 +1,70 @@ +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 b05da08..80aea85 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 +@@ -332,7 +332,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1 + }; + this._scrollRef = null; +- this._sentStartForContentLength = 0; ++ this._sentStartForFirstVisibleItemKey = null; + this._sentEndForContentLength = 0; + this._totalCellLength = 0; + this._totalCellsMeasured = 0; +@@ -1397,8 +1397,8 @@ class VirtualizedList extends StateSafePureComponent { + // Next check if the user just scrolled within the start threshold + // 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) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey) { ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({ + distanceFromStart + }); +@@ -1407,7 +1407,7 @@ class VirtualizedList extends StateSafePureComponent { + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { +- this._sentStartForContentLength = isWithinStartThreshold ? this._sentStartForContentLength : 0; ++ this._sentStartForFirstVisibleItemKey = isWithinStartThreshold ? this._sentStartForFirstVisibleItemKey : null; + this._sentEndForContentLength = isWithinEndThreshold ? this._sentEndForContentLength : 0; + } + } +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 459f017..799a6ee 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 +@@ -1325,7 +1325,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; +- _sentStartForContentLength = 0; ++ _sentStartForFirstVisibleItemKey: ?string = null; + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; +@@ -1675,18 +1675,18 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +- this._scrollMetrics.contentLength !== this._sentStartForContentLength ++ this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey + ) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { +- this._sentStartForContentLength = isWithinStartThreshold +- ? this._sentStartForContentLength +- : 0; ++ this._sentStartForFirstVisibleItemKey = isWithinStartThreshold ++ ? this._sentStartForFirstVisibleItemKey ++ : null; + this._sentEndForContentLength = isWithinEndThreshold + ? this._sentEndForContentLength + : 0; diff --git a/patches/react-pdf+7.3.3.patch b/patches/react-pdf+7.7.1.patch similarity index 81% rename from patches/react-pdf+7.3.3.patch rename to patches/react-pdf+7.7.1.patch index 6b3b4be22b6e..f6ec8d8c1685 100644 --- a/patches/react-pdf+7.3.3.patch +++ b/patches/react-pdf+7.7.1.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-pdf/dist/esm/Document.js b/node_modules/react-pdf/dist/esm/Document.js -index ac7ca31..56bc766 100644 +index 493ff15..8d5e734 100644 --- a/node_modules/react-pdf/dist/esm/Document.js +++ b/node_modules/react-pdf/dist/esm/Document.js -@@ -240,6 +240,7 @@ const Document = forwardRef(function Document(_a, ref) { +@@ -261,6 +261,7 @@ const Document = forwardRef(function Document(_a, ref) { pdfDispatch({ type: 'REJECT', error }); }); return () => { diff --git a/src/App.tsx b/src/App.tsx index 006028271c80..6316fa80fba1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,8 +32,8 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; +import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; -import {SuggestionsContextProvider} from './pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; @@ -79,8 +79,8 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, + ReportIDsContextProvider, PlaybackContextProvider, - SuggestionsContextProvider, FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, diff --git a/src/CONST.ts b/src/CONST.ts index c2299f242b6b..566d5179f86a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4,8 +4,10 @@ import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; import type {ValueOf} from 'type-fest'; +import BankAccount from './libs/models/BankAccount'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; +import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; import type {Unit} from './types/onyx/Policy'; type RateAndUnit = { @@ -53,16 +55,17 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', + INVOICE: 'invoice', + SYSTEM: 'system', } as const; // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; const onboardingChoices = { - TRACK: 'newDotTrack', - EMPLOYER: 'newDotEmployer', - MANAGE_TEAM: 'newDotManageTeam', PERSONAL_SPEND: 'newDotPersonalSpend', + MANAGE_TEAM: 'newDotManageTeam', + EMPLOYER: 'newDotEmployer', CHAT_SPLIT: 'newDotSplitChat', LOOKING_AROUND: 'newDotLookingAround', }; @@ -72,7 +75,6 @@ type OnboardingPurposeType = ValueOf; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], - DEFAULT_COMPOSER_PORTAL_HOST_NAME: 'suggestions_0', // 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], @@ -355,7 +357,9 @@ const CONST = { TRACK_EXPENSE: 'trackExpense', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', + SPOTNANA_TRAVEL: 'spotnanaTravel', ACCOUNTING_ON_NEW_EXPENSIFY: 'accountingOnNewExpensify', + XERO_ON_NEW_EXPENSIFY: 'xeroOnNewExpensify', }, BUTTON_STATES: { DEFAULT: 'default', @@ -580,6 +584,7 @@ const CONST = { ONFIDO_PRIVACY_POLICY_URL: 'https://onfido.com/privacy/', ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', LIST_OF_RESTRICTED_BUSINESSES: 'https://community.expensify.com/discussion/6191/list-of-restricted-businesses', + TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`, // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', @@ -632,7 +637,6 @@ const CONST = { MEMBER: 'member', }, MAX_COUNT_BEFORE_FOCUS_UPDATE: 30, - MAXIMUM_PARTICIPANTS: 8, SPLIT_REPORTID: '-2', ACTIONS: { LIMIT: 50, @@ -651,6 +655,7 @@ const CONST = { CREATED: 'CREATED', DELEGATE_SUBMIT: 'DELEGATESUBMIT', // OldDot Action DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action + DISMISSED_VIOLATION: 'DISMISSEDVIOLATION', DONATION: 'DONATION', // OldDot Action EXPORTED_TO_CSV: 'EXPORTEDTOCSV', // OldDot Action EXPORTED_TO_INTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action @@ -664,6 +669,7 @@ const CONST = { MANAGER_DETACH_RECEIPT: 'MANAGERDETACHRECEIPT', // OldDot Action MARKED_REIMBURSED: 'MARKEDREIMBURSED', // OldDot Action MARK_REIMBURSED_FROM_INTEGRATION: 'MARKREIMBURSEDFROMINTEGRATION', // OldDot Action + MERGED_WITH_CASH_TRANSACTION: 'MERGEDWITHCASHTRANSACTION', MODIFIED_EXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', OUTDATED_BANK_ACCOUNT: 'OUTDATEDBANKACCOUNT', // OldDot Action @@ -799,6 +805,7 @@ const CONST = { EXPENSE: 'expense', IOU: 'iou', TASK: 'task', + INVOICE: 'invoice', }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { @@ -837,11 +844,21 @@ const CONST = { }, RESERVED_ROOM_NAMES: ['#admins', '#announce'], MAX_PREVIEW_AVATARS: 4, - MAX_ROOM_NAME_LENGTH: 79, + MAX_ROOM_NAME_LENGTH: 99, LAST_MESSAGE_TEXT_MAX_LENGTH: 200, OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + PERMISSIONS: { + READ: 'read', + WRITE: 'write', + SHARE: 'share', + OWN: 'own', + }, + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + BUSINESS: 'policy', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -850,9 +867,8 @@ const CONST = { MAX_LINES: 16, MAX_LINES_SMALL_SCREEN: 6, MAX_LINES_FULL: -1, - - // The minimum number of typed lines needed to enable the full screen composer - FULL_COMPOSER_MIN_LINES: 3, + // The minimum height needed to enable the full screen composer + FULL_COMPOSER_MIN_HEIGHT: 60, }, MODAL: { MODAL_TYPE: { @@ -903,6 +919,15 @@ const CONST = { SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, }, + SEARCH_TABLE_COLUMNS: { + DATE: 'date', + MERCHANT: 'merchant', + FROM: 'from', + TO: 'to', + TOTAL: 'total', + TYPE: 'type', + ACTION: 'action', + }, PRIORITY_MODE: { GSD: 'gsd', DEFAULT: 'default', @@ -1008,6 +1033,11 @@ const CONST = { PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, MAX_REQUEST_RETRIES: 10, + NETWORK_STATUS: { + ONLINE: 'online', + OFFLINE: 'offline', + UNKNOWN: 'unknown', + }, }, WEEK_STARTS_ON: 1, // Monday DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, @@ -1172,7 +1202,6 @@ const CONST = { EMOJI_PICKER_HEADER_HEIGHT: 32, RECIPIENT_LOCAL_TIME_HEIGHT: 25, AUTO_COMPLETE_SUGGESTER: { - EDIT_SUGGESTER_PADDING: 8, SUGGESTER_PADDING: 6, SUGGESTER_INNER_PADDING: 8, SUGGESTION_ROW_HEIGHT: 40, @@ -1233,25 +1262,45 @@ const CONST = { ENABLE_NEW_CATEGORIES: 'enableNewCategories', SYNC_CUSTOMERS: 'syncCustomers', SYNC_LOCATIONS: 'syncLocations', - SYNC_TAXES: 'syncTaxes', - PREFERRED_EXPORTER: 'exporter', + SYNC_TAX: 'syncTax', + EXPORT: 'export', EXPORT_DATE: 'exportDate', - OUT_OF_POCKET_EXPENSES: 'outOfPocketExpenses', - EXPORT_INVOICE: 'exportInvoice', - EXPORT_ENTITY: 'exportEntity', - EXPORT_ACCOUNT: 'exportAccount', - EXPORT_COMPANY_CARD: 'exportCompanyCard', + NON_REIMBURSABLE_EXPENSES_ACCOUNT: 'nonReimbursableExpensesAccount', + NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'nonReimbursableExpensesExportDestination', + REIMBURSABLE_EXPENSES_ACCOUNT: 'reimbursableExpensesAccount', + REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'reimbursableExpensesExportDestination', + NON_REIMBURSABLE_BILL_DEFAULT_VENDOR: 'nonReimbursableBillDefaultVendor', + RECEIVABLE_ACCOUNT: 'receivableAccount', AUTO_SYNC: 'autoSync', - SYNCE_PEOPLE: 'syncPeople', + SYNC_PEOPLE: 'syncPeople', AUTO_CREATE_VENDOR: 'autoCreateVendor', REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID', COLLECTION_ACCOUNT_ID: 'collectionAccountID', }, - QUICKBOOKS_EXPORT_ENTITY: { - VENDOR_BILL: 'vendorBill', + XERO_CONFIG: { + AUTO_SYNC: 'autoSync', + SYNC: 'sync', + IMPORT_CUSTOMERS: 'importCustomers', + IMPORT_TAX_RATES: 'importTaxRates', + }, + + QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: { + VENDOR_BILL: 'bill', CHECK: 'check', - JOURNAL_ENTRY: 'journalEntry', + JOURNAL_ENTRY: 'journal_entry', + }, + + QUICKBOOKS_EXPORT_DATE: { + LAST_EXPENSE: 'LAST_EXPENSE', + REPORT_EXPORTED: 'REPORT_EXPORTED', + REPORT_SUBMITTED: 'REPORT_SUBMITTED', + }, + + QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE: { + CREDIT_CARD: 'credit_card', + DEBIT_CARD: 'debit_card', + VENDOR_BILL: 'bill', }, ACCOUNT_ID: { @@ -1346,8 +1395,9 @@ const CONST = { PERSONAL_INFO: { LEGAL_NAME: 0, DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, + ADDRESS: 2, + PHONE_NUMBER: 3, + SSN: 4, }, }, TIER_NAME: { @@ -1372,6 +1422,13 @@ const CONST = { ERROR: 'ERROR', EXIT: 'EXIT', }, + DEFAULT_DATA: { + bankName: '', + plaidAccessToken: '', + bankAccounts: [] as PlaidBankAccount[], + isLoading: false, + errors: {}, + }, }, ONFIDO: { @@ -1450,6 +1507,7 @@ const CONST = { PAY: 'pay', SPLIT: 'split', REQUEST: 'request', + INVOICE: 'invoice', SUBMIT: 'submit', TRACK: 'track', }, @@ -1619,6 +1677,27 @@ const CONST = { DISABLE: 'disable', ENABLE: 'enable', }, + DEFAULT_CATEGORIES: [ + 'Advertising', + 'Benefits', + 'Car', + 'Equipment', + 'Fees', + 'Home Office', + 'Insurance', + 'Interest', + 'Labor', + 'Maintenance', + 'Materials', + 'Meals and Entertainment', + 'Office Supplies', + 'Other', + 'Professional Services', + 'Rent', + 'Taxes', + 'Travel', + 'Utilities', + ], OWNERSHIP_ERRORS: { NO_BILLING_CARD: 'noBillingCard', AMOUNT_OWED: 'amountOwed', @@ -1647,6 +1726,7 @@ const CONST = { NAME: { // Here we will add other connections names when we add support for them QBO: 'quickbooksOnline', + XERO: 'xero', }, SYNC_STAGE_NAME: { STARTING_IMPORT: 'startingImport', @@ -1660,9 +1740,28 @@ const CONST = { QBO_SYNC_PAYMENTS: 'quickbooksOnlineSyncBillPayments', QBO_IMPORT_TAX_CODES: 'quickbooksOnlineSyncTaxCodes', QBO_CHECK_CONNECTION: 'quickbooksOnlineCheckConnection', + QBO_SYNC_TITLE: 'quickbooksOnlineSyncTitle', + QBO_SYNC_LOAD_DATA: 'quickbooksOnlineSyncLoadData', + QBO_SYNC_APPLY_CATEGORIES: 'quickbooksOnlineSyncApplyCategories', + QBO_SYNC_APPLY_CUSTOMERS: 'quickbooksOnlineSyncApplyCustomers', + QBO_SYNC_APPLY_PEOPLE: 'quickbooksOnlineSyncApplyEmployees', + QBO_SYNC_APPLY_CLASSES_LOCATIONS: 'quickbooksOnlineSyncApplyClassesLocations', JOB_DONE: 'jobDone', + XERO_SYNC_STEP: 'xeroSyncStep', + XERO_SYNC_XERO_REIMBURSED_REPORTS: 'xeroSyncXeroReimbursedReports', + XERO_SYNC_EXPENSIFY_REIMBURSED_REPORTS: 'xeroSyncExpensifyReimbursedReports', + XERO_SYNC_IMPORT_CHART_OF_ACCOUNTS: 'xeroSyncImportChartOfAccounts', + XERO_SYNC_IMPORT_CATEGORIES: 'xeroSyncImportCategories', + XERO_SYNC_IMPORT_TRACKING_CATEGORIES: 'xeroSyncImportTrackingCategories', + XERO_SYNC_IMPORT_CUSTOMERS: 'xeroSyncImportCustomers', + XERO_SYNC_IMPORT_BANK_ACCOUNTS: 'xeroSyncImportBankAccounts', + XERO_SYNC_IMPORT_TAX_RATES: 'xeroSyncImportTaxRates', }, }, + ACCESS_VARIANTS: { + PAID: 'paid', + ADMIN: 'admin', + }, }, CUSTOM_UNITS: { @@ -1758,7 +1857,7 @@ const CONST = { CARD_NUMBER: /^[0-9]{15,16}$/, CARD_SECURITY_CODE: /^[0-9]{3,4}$/, CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/, - ROOM_NAME: /^#[\p{Ll}0-9-]{1,80}$/u, + ROOM_NAME: /^#[\p{Ll}0-9-]{1,100}$/u, // eslint-disable-next-line max-len, no-misleading-character-class EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, @@ -1938,7 +2037,6 @@ const CONST = { INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { - SHARE_CODE: 'shareCode', MEMBERS: 'member', INVITE: 'invite', SETTINGS: 'settings', @@ -3331,10 +3429,6 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', - SENT: 'sent', - DRAFTS: 'drafts', - WAITING_ON_YOU: 'waitingOnYou', - FINISHED: 'finished', }, STATUS_TEXT_MAX_LENGTH: 100, @@ -3419,6 +3513,16 @@ const CONST = { LINK: 'https://join.my.expensify.com', }, + FEATURE_TRAINING: { + CONTENT_TYPES: { + TRACK_EXPENSE: 'track-expenses', + }, + 'track-expenses': { + VIDEO_URL: `${CLOUDFRONT_URL}/videos/guided-setup-track-business.mp4`, + LEARN_MORE_LINK: `${USE_EXPENSIFY_URL}/track-expenses`, + }, + }, + /** * native IDs for close buttons in Overlay component */ @@ -3558,7 +3662,6 @@ const CONST = { }, INTRO_CHOICES: { - TRACK: 'newDotTrack', SUBMIT: 'newDotSubmit', MANAGE_TEAM: 'newDotManageTeam', CHAT_SPLIT: 'newDotSplitChat', @@ -3585,19 +3688,6 @@ const CONST = { ONBOARDING_CHOICES: {...onboardingChoices}, ONBOARDING_CONCIERGE: { - [onboardingChoices.TRACK]: - "# Let's start tracking your expenses!\n" + - '\n' + - "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > *New Workspace*\n' + - '2. Give your workspace a name (e.g. "My business expenses").\n' + - '\n' + - 'Then, add expenses to your workspace:\n' + - '1. Find your workspace using the search field.\n' + - '2. Click the gray + button next to the message field.\n' + - '3. Click Request money, then add your expense type.\n' + - '\n' + - "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", [onboardingChoices.EMPLOYER]: '# Expensify is the fastest way to get paid back!\n' + '\n' + @@ -3638,51 +3728,11 @@ const CONST = { }, ONBOARDING_MESSAGES: { - [onboardingChoices.TRACK]: { - message: 'Here are some essential tasks to keep your business spend in shape for tax season.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, - tasks: [ - { - type: 'createWorkspace', - autoCompleted: true, - title: 'Create a workspace', - subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more.', - message: - 'Here’s how to create a workspace:\n' + - '\n' + - '1. Click your profile picture.\n' + - '2. Click Workspaces > New workspace.\n' + - '\n' + - 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', - }, - { - type: 'trackExpense', - autoCompleted: false, - title: 'Track an expense', - subtitle: 'Track an expense in any currency, in just a few clicks.', - message: - 'Here’s how to track an expense:\n' + - '\n' + - '1. Click the green + button.\n' + - '2. Choose Track expense.\n' + - '3. Enter an amount or scan a receipt.\n' + - '4. Click Track.\n' + - '\n' + - 'And you’re done! Yep, it’s that easy.', - }, - ], - }, [onboardingChoices.EMPLOYER]: { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, duration: 55, width: 1280, height: 960, @@ -3692,12 +3742,13 @@ const CONST = { type: 'submitExpense', autoCompleted: false, title: 'Submit an expense', - subtitle: 'Submit an expense by entering an amount or scanning a receipt.', - message: + description: + '*Submit an expense* by entering an amount or scanning a receipt.\n' + + '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green + button.\n' + - '2. Choose Submit expense.\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Submit expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + '\n' + @@ -3707,12 +3758,13 @@ const CONST = { type: 'enableWallet', autoCompleted: false, title: 'Enable your wallet', - subtitle: 'You’ll need to enable your Expensify Wallet to get paid back. Don’t worry, it’s easy!', - message: + description: + 'You’ll need to *enable your Expensify Wallet* to get paid back. Don’t worry, it’s easy!\n' + + '\n' + 'Here’s how to set up your wallet:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Click Wallet > Enable wallet.\n' + + '2. Click *Wallet* > *Enable wallet*.\n' + '3. Connect your bank account.\n' + '\n' + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', @@ -3722,8 +3774,8 @@ const CONST = { [onboardingChoices.MANAGE_TEAM]: { message: 'Here are some important tasks to help get your team’s expenses under control.', video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-manage-team.jpg`, duration: 55, width: 1280, height: 960, @@ -3733,21 +3785,21 @@ const CONST = { type: 'createWorkspace', autoCompleted: true, title: 'Create a workspace', - subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more.', - message: + description: + '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' + + '\n' + 'Here’s how to create a workspace:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Click Workspaces > New workspace.\n' + + '2. Click *Workspaces* > *New workspace*.\n' + '\n' + - 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + '*Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.*', }, { type: 'meetGuide', autoCompleted: false, title: 'Meet your setup specialist', - subtitle: '', - message: ({adminsRoomLink, guideCalendarLink}: {adminsRoomLink: string; guideCalendarLink: string}) => + description: ({adminsRoomLink, guideCalendarLink}: {adminsRoomLink: string; guideCalendarLink: string}) => `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + '\n' + `Chat with the specialist in your [#admins room](${adminsRoomLink}) or [schedule a call](${guideCalendarLink}) today.`, @@ -3756,31 +3808,33 @@ const CONST = { type: 'setupCategories', autoCompleted: false, title: 'Set up categories', - subtitle: 'Set up categories so your team can code expenses for easy reporting.', - message: + description: + '*Set up categories* so your team can code expenses for easy reporting.\n' + + '\n' + 'Here’s how to set up categories:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Go to Workspaces > [your workspace].\n' + - '3. Click Categories.\n' + + '2. Go to *Workspaces* > [your workspace].\n' + + '3. Click *Categories*.\n' + '4. Enable and disable default categories.\n' + - '5. Click Add categories to make your own.\n' + + '5. Click *Add categories* to make your own.\n' + '\n' + - 'For more controls like requiring a category for every expense, click Settings.', + 'For more controls like requiring a category for every expense, click *Settings*.', }, { type: 'addExpenseApprovals', autoCompleted: false, title: 'Add expense approvals', - subtitle: 'Add expense approvals to review your team’s spend and keep it under control.', - message: + description: + '*Add expense approvals* to review your team’s spend and keep it under control.\n' + + '\n' + 'Here’s how to add expense approvals:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Go to Workspaces > [your workspace].\n' + - '3. Click More features.\n' + - '4. Enable Workflows.\n' + - '5. In Workflows, enable Add approvals.\n' + + '2. Go to *Workspaces* > [your workspace].\n' + + '3. Click *More features*.\n' + + '4. Enable *Workflows*.\n' + + '5. In *Workflows*, enable *Add approvals*.\n' + '\n' + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team.', }, @@ -3788,13 +3842,14 @@ const CONST = { type: 'inviteTeam', autoCompleted: false, title: 'Invite your team', - subtitle: 'Invite your team to Expensify so they can start tracking expenses today.', - message: + description: + '*Invite your team* to Expensify so they can start tracking expenses today.\n' + + '\n' + 'Here’s how to invite your team:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Go to Workspaces > [your workspace].\n' + - '3. Click Members > Invite member.\n' + + '2. Go to *Workspaces* > [your workspace].\n' + + '3. Click *Members* > *Invite member*.\n' + '4. Enter emails or phone numbers. \n' + '5. Add an invite message if you want.\n' + '\n' + @@ -3805,8 +3860,8 @@ const CONST = { [onboardingChoices.PERSONAL_SPEND]: { message: 'Here’s how to track your spend in a few clicks.', video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, duration: 55, width: 1280, height: 960, @@ -3816,14 +3871,15 @@ const CONST = { type: 'trackExpense', autoCompleted: false, title: 'Track an expense', - subtitle: 'Track an expense in any currency, whether you have a receipt or not.', - message: + description: + '*Track an expense* in any currency, whether you have a receipt or not.\n' + + '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Click the green + button.\n' + - '2. Choose Track expense.\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Track expense*.\n' + '3. Enter an amount or scan a receipt.\n' + - '4. Click Track.\n' + + '4. Click *Track*.\n' + '\n' + 'And you’re done! Yep, it’s that easy.', }, @@ -3832,8 +3888,8 @@ const CONST = { [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-chat-split-bills.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-chat-split-bills.jpg`, duration: 55, width: 1280, height: 960, @@ -3843,15 +3899,16 @@ const CONST = { type: 'startChat', autoCompleted: false, title: 'Start a chat', - subtitle: 'Start a chat with a friend or group using their email or phone number.', - message: + description: + '*Start a chat* with a friend or group using their email or phone number.\n' + + '\n' + 'Here’s how to start a chat:\n' + '\n' + - '1. Click the green + button.\n' + - '2. Choose Start chat.\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Start chat*.\n' + '3. Enter emails or phone numbers.\n' + '\n' + - 'If any of your friends aren’t using Expensify already, they’ll be invited automatically. \n' + + 'If any of your friends aren’t using Expensify already, they’ll be invited automatically.\n' + '\n' + 'Every chat will also turn into an email or text that they can respond to directly.', }, @@ -3859,12 +3916,13 @@ const CONST = { type: 'splitExpense', autoCompleted: false, title: 'Split an expense', - subtitle: 'Split an expense right in your chat with one or more friends.', - message: + description: + '*Split an expense* right in your chat with one or more friends.\n' + + '\n' + 'Here’s how to request money:\n' + '\n' + - '1. Click the green + button.\n' + - '2. Choose Split expense.\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Split expense*.\n' + '3. Scan a receipt or enter an amount.\n' + '4. Add your friend(s) to the request.\n' + '\n' + @@ -3874,12 +3932,13 @@ const CONST = { type: 'enableWallet', autoCompleted: false, title: 'Enable your wallet', - subtitle: 'You’ll need to enable your Expensify Wallet to get paid back. Don’t worry, it’s easy!', - message: + description: + 'You’ll need to *enable your Expensify Wallet* to get paid back. Don’t worry, it’s easy!\n' + + '\n' + 'Here’s how to enable your wallet:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Click Wallet > Enable wallet.\n' + + '2. *Click Wallet* > *Enable wallet*.\n' + '3. Add your bank account.\n' + '\n' + 'Once that’s done, you can request money from anyone and get paid right into your personal bank account.', @@ -3889,13 +3948,6 @@ const CONST = { [onboardingChoices.LOOKING_AROUND]: { message: "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, tasks: [], }, }, @@ -3919,31 +3971,43 @@ const CONST = { DEBUG: 'DEBUG', }, }, - REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX: { - BANK_ACCOUNT: { - ACCOUNT_NUMBERS: 0, - }, - PERSONAL_INFO: { - LEGAL_NAME: 0, - DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, - }, - BUSINESS_INFO: { - BUSINESS_NAME: 0, - TAX_ID_NUMBER: 1, - COMPANY_WEBSITE: 2, - PHONE_NUMBER: 3, - COMPANY_ADDRESS: 4, - COMPANY_TYPE: 5, - INCORPORATION_DATE: 6, - INCORPORATION_STATE: 7, + REIMBURSEMENT_ACCOUNT: { + DEFAULT_DATA: { + achData: { + state: BankAccount.STATE.SETUP, + }, + isLoading: false, + errorFields: {}, + errors: {}, + maxAttemptsReached: false, + shouldShowResetModal: false, }, - UBO: { - LEGAL_NAME: 0, - DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, + SUBSTEP_INDEX: { + BANK_ACCOUNT: { + ACCOUNT_NUMBERS: 0, + }, + PERSONAL_INFO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, + BUSINESS_INFO: { + BUSINESS_NAME: 0, + TAX_ID_NUMBER: 1, + COMPANY_WEBSITE: 2, + PHONE_NUMBER: 3, + COMPANY_ADDRESS: 4, + COMPANY_TYPE: 5, + INCORPORATION_DATE: 6, + INCORPORATION_STATE: 7, + }, + UBO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, }, }, CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{ @@ -4613,12 +4677,6 @@ const CONST = { }, }, - QUICKBOOKS_EXPORT_DATE: { - LAST_EXPENSE: 'lastExpense', - EXPORTED_DATE: 'exportedDate', - SUBMITTED_DATA: 'submittedData', - }, - SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', }, @@ -4644,6 +4702,20 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + + DOWNLOADS_PATH: '/Downloads', + NEW_EXPENSIFY_PATH: '/New Expensify', + + ENVIRONMENT_SUFFIX: { + DEV: ' Dev', + ADHOC: ' AdHoc', + }, + + SEARCH_TRANSACTION_TYPE: { + CASH: 'cash', + CARD: 'card', + DISTANCE: 'distance', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 6a57d6fdcc10..c67f1400fc4b 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -41,6 +41,8 @@ Onyx.registerLogger(({level, message}) => { if (level === 'alert') { Log.alert(message); console.error(message); + } else if (level === 'hmmm') { + Log.hmmm(message); } else { Log.info(message); } diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index f199d2841ec0..eea357322075 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -8,6 +8,7 @@ export default { LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator', + FEATURE_TRANING_MODAL_NAVIGATOR: 'FeatureTrainingModalNavigator', WELCOME_VIDEO_MODAL_NAVIGATOR: 'WelcomeVideoModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 95d383345ec6..88818c3eb7c6 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -2,6 +2,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; +import type Onboarding from './types/onyx/Onboarding'; import type AssertTypesEqual from './types/utils/AssertTypesEqual'; import type DeepValueOf from './types/utils/DeepValueOf'; @@ -38,9 +39,6 @@ const ONYXKEYS = { CREDENTIALS: 'credentials', STASHED_CREDENTIALS: 'stashedCredentials', - // Contains loading data for the IOU feature (MoneyRequestModal, IOUDetail, & MoneyRequestPreview Components) - IOU: 'iou', - /** Keeps track if there is modal currently visible or not */ MODAL: 'modal', @@ -114,6 +112,9 @@ const ONYXKEYS = { /** Boolean flag only true when first set */ NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', + /** This NVP contains information about whether the onboarding flow was completed or not */ + NVP_ONBOARDING: 'nvp_onboarding', + /** Contains the user preference for the LHN priority mode */ NVP_PRIORITY_MODE: 'nvp_priorityMode', @@ -141,6 +142,9 @@ const ONYXKEYS = { /** This NVP contains the referral banners the user dismissed */ NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners', + /** This NVP contains the training modals the user denied showing again */ + NVP_HAS_SEEN_TRACK_TRAINING: 'nvp_hasSeenTrackTraining', + /** Indicates which locale should be used */ NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', @@ -278,6 +282,9 @@ const ONYXKEYS = { /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + /** Onboarding Purpose selected by the user during Onboarding flow */ + ONBOARDING_ADMINS_CHAT_REPORT_ID: 'onboardingAdminsChatReportID', + // Max width supported for HTML element MAX_CANVAS_WIDTH: 'maxCanvasWidth', @@ -336,7 +343,6 @@ const ONYXKEYS = { REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', - REPORT_DRAFT_COMMENT_NUMBER_OF_LINES: 'reportDraftCommentNumberOfLines_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', @@ -355,6 +361,9 @@ const ONYXKEYS = { /** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */ DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_', + + // Search Page related + SNAPSHOT: 'snapshot_', }, /** List of Form ids */ @@ -541,7 +550,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; - [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; @@ -559,12 +567,14 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; + [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; }; type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; + [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; @@ -572,7 +582,6 @@ type OnyxValuesMapping = { [ONYXKEYS.CURRENT_DATE]: string; [ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.STASHED_CREDENTIALS]: OnyxTypes.Credentials; - [ONYXKEYS.IOU]: OnyxTypes.IOU; [ONYXKEYS.MODAL]: OnyxTypes.Modal; [ONYXKEYS.NETWORK]: OnyxTypes.Network; [ONYXKEYS.NEW_GROUP_CHAT_DRAFT]: OnyxTypes.NewGroupChatDraft; @@ -614,6 +623,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale; [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; + [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; @@ -648,6 +658,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a8cb1847ccc1..341d058d2cd1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -28,6 +28,11 @@ const ROUTES = { getRoute: (query: string) => `search/${query}` as const, }, + SEARCH_REPORT: { + route: '/search/:query/view/:reportID', + getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, + }, + // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', FLAG_COMMENT: { @@ -50,6 +55,7 @@ const ROUTES = { 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), @@ -92,12 +98,12 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: 'settings/wallet/card/:domain', - getRoute: (domain: string) => `settings/wallet/card/${domain}` as const, + route: 'settings/wallet/card/:cardID?', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const, }, SETTINGS_REPORT_FRAUD: { - route: 'settings/wallet/card/:domain/report-virtual-fraud', - getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:cardID/report-virtual-fraud', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { route: 'settings/wallet/card/:domain/get-physical/name', @@ -119,6 +125,7 @@ const ROUTES = { SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', SETTINGS_ADD_BANK_ACCOUNT_REFACTOR: 'settings/wallet/add-bank-account-refactor', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_ENABLE_PAYMENTS_REFACTOR: 'settings/wallet/enable-payments-refactor', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, @@ -126,12 +133,12 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: 'settings/wallet/card/:domain/report-card-lost-or-damaged', - getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:cardID/report-card-lost-or-damaged', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { - route: 'settings/wallet/card/:domain/activate', - getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const, + route: 'settings/wallet/card/:cardID/activate', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const, }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', @@ -272,11 +279,6 @@ const ROUTES = { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const, }, - EDIT_SPLIT_BILL: { - route: `r/:reportID/split/:reportActionID/edit/:field/:tagIndex?`, - getRoute: (reportID: string, reportActionID: string, field: ValueOf, tagIndex?: number) => - `r/${reportID}/split/${reportActionID}/edit/${field as string}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const, - }, TASK_TITLE: { route: 'r/:reportID/title', getRoute: (reportID: string) => `r/${reportID}/title` as const, @@ -310,18 +312,15 @@ const ROUTES = { getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string) => `${type}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const, }, - MONEY_REQUEST_MERCHANT: { - route: ':iouType/new/merchant/:reportID?', - getRoute: (iouType: IOUType, reportID = '') => `${iouType}/new/merchant/${reportID}` as const, - }, - MONEY_REQUEST_RECEIPT: { - route: ':iouType/new/receipt/:reportID?', - getRoute: (iouType: IOUType, reportID = '') => `${iouType}/new/receipt/${reportID}` as const, - }, MONEY_REQUEST_CREATE: { route: ':action/:iouType/start/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const, }, + MONEY_REQUEST_STEP_SEND_FROM: { + route: 'create/:iouType/from/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType as string}/from/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => @@ -382,6 +381,11 @@ const ROUTES = { getRoute: (iouType: IOUType, transactionID: string, reportID: string, backTo = '', action: IOUAction = 'create') => getUrlWithBackToParam(`${action as string}/${iouType as string}/participants/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_SPLIT_PAYER: { + route: ':action/:iouType/confirmation/:transactionID/:reportID/payer', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer`, backTo), + }, MONEY_REQUEST_STEP_SCAN: { route: ':action/:iouType/scan/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => @@ -440,10 +444,6 @@ const ROUTES = { NEW_TASK_TITLE: 'new/task/title', NEW_TASK_DESCRIPTION: 'new/task/description', - ONBOARD: 'onboard', - ONBOARD_MANAGE_EXPENSES: 'onboard/manage-expenses', - ONBOARD_EXPENSIFY_CLASSIC: 'onboard/expensify-classic', - TEACHERS_UNITE: 'settings/teachersunite', I_KNOW_A_TEACHER: 'settings/teachersunite/i-know-a-teacher', I_AM_A_TEACHER: 'settings/teachersunite/i-am-a-teacher', @@ -472,6 +472,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile', getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const, }, + WORKSPACE_PROFILE_ADDRESS: { + route: 'settings/workspaces/:policyID/profile/address', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/address` as const, + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -484,17 +488,33 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export` as const, }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: { - route: 'settings/workspaces/:policyID/accounting/quickbooks-online/company-card-expense-account-select', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/company-card-expense-account-select` as const, + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/account-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/account-select` as const, + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/default-vendor-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/default-vendor-select` as const, + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_SELECT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/card-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/card-select` as const, }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/invoice-account-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/invoice-account-select` as const, }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/account-payable-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/account-payable-select` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER: { - route: 'settings/workspaces/:policyID/accounting/quickbooks-online/preferred-exporter', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/preferred-exporter` as const, + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/preferred-exporter', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/preferred-exporter` as const, }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/out-of-pocket-expense', @@ -739,6 +759,9 @@ const ROUTES = { getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, PROCESS_MONEY_REQUEST_HOLD: 'hold-expense-educational', + TRAVEL_MY_TRIPS: 'travel', + TRAVEL_TCS: 'travel/terms', + TRACK_TRAINING_MODAL: 'track-training', ONBOARDING_ROOT: 'onboarding', ONBOARDING_PERSONAL_DETAILS: 'onboarding/personal-details', ONBOARDING_WORK: 'onboarding/work', @@ -749,6 +772,26 @@ const ROUTES = { route: 'r/:reportID/transaction/:transactionID/receipt', getRoute: (reportID: string, transactionID: string) => `r/${reportID}/transaction/${transactionID}/receipt` as const, }, + POLICY_ACCOUNTING_XERO_IMPORT: { + route: 'settings/workspaces/:policyID/accounting/xero/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import` as const, + }, + POLICY_ACCOUNTING_XERO_ORGANIZATION: { + route: 'settings/workspaces/:policyID/accounting/xero/organization/:currentOrganizationID', + getRoute: (policyID: string, currentOrganizationID: string) => `settings/workspaces/${policyID}/accounting/xero/organization/${currentOrganizationID}` 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, + }, + POLICY_ACCOUNTING_XERO_TAXES: { + route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/taxes` as const, + }, + POLICY_ACCOUNTING_XERO_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/xero/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c6658c2f6578..45c9906e8ca3 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -20,11 +20,17 @@ const SCREENS = { NOT_FOUND: 'not-found', TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps', VALIDATE_LOGIN: 'ValidateLogin', + CONNECTION_COMPLETE: 'ConnectionComplete', UNLINK_LOGIN: 'UnlinkLogin', SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', + TRAVEL: { + MY_TRIPS: 'Travel_MyTrips', + TCS: 'Travel_TCS', + }, WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane', SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', + REPORT_RHP: 'Search_Report_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, SETTINGS: { @@ -84,6 +90,7 @@ const SCREENS = { TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', + ENABLE_PAYMENTS_REFACTOR: 'Settings_Wallet_EnablePayments_Refactor', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', @@ -116,7 +123,6 @@ const SCREENS = { PARTICIPANTS: 'Participants', MONEY_REQUEST: 'MoneyRequest', NEW_TASK: 'NewTask', - ONBOARD_ENGAGEMENT: 'Onboard_Engagement', TEACHERS_UNITE: 'TeachersUnite', TASK_DETAILS: 'Task_Details', ENABLE_PAYMENTS: 'EnablePayments', @@ -131,6 +137,8 @@ const SCREENS = { ROOM_INVITE: 'RoomInvite', REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', + TRAVEL: 'Travel', + SEARCH_REPORT: 'SearchReport', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -160,6 +168,8 @@ const SCREENS = { STEP_WAYPOINT: 'Money_Request_Step_Waypoint', STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', + STEP_SPLIT_PAYER: 'Money_Request_Step_Split_Payer', + STEP_SEND_FROM: 'Money_Request_Step_Send_From', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', @@ -218,7 +228,11 @@ const SCREENS = { QUICKBOOKS_ONLINE_EXPORT: 'Workspace_Accounting_Quickbooks_Online_Export', QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Date_Select', QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Invoice_Account_Select', - QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Select', + QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense', + QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Select', + QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Payable_Select', + QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Non_Reimbursable_Default_Vendor_Select', + QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Select', QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER: 'Workspace_Accounting_Quickbooks_Online_Export_Preferred_Exporter', QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES: 'Workspace_Accounting_Quickbooks_Online_Export_Out_Of_Pocket_Expenses', QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Out_Of_Pocket_Expenses_Select', @@ -226,6 +240,11 @@ const SCREENS = { QUICKBOOKS_ONLINE_ADVANCED: 'Policy_Accounting_Quickbooks_Online_Advanced', QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Account_Selector', QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector', + XERO_IMPORT: 'Policy_Accounting_Xero_Import', + XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers', + XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', + XERO_TAXES: 'Policy_Accounting_Xero_Taxes', + XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', @@ -257,6 +276,7 @@ const SCREENS = { TAG_CREATE: 'Tag_Create', TAG_SETTINGS: 'Tag_Settings', CURRENCY: 'Workspace_Profile_Currency', + ADDRESS: 'Workspace_Profile_Address', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', @@ -306,12 +326,6 @@ const SCREENS = { WORK: 'Onboarding_Work', }, - ONBOARD_ENGAGEMENT: { - ROOT: 'Onboard_Engagement_Root', - MANAGE_TEAMS_EXPENSES: 'Manage_Teams_Expenses', - EXPENSIFY_CLASSIC: 'Expenisfy_Classic', - }, - WELCOME_VIDEO: { ROOT: 'Welcome_Video_Root', }, @@ -343,6 +357,7 @@ const SCREENS = { REFERRAL_DETAILS: 'Referral_Details', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', TRANSACTION_RECEIPT: 'TransactionReceipt', + FEATURE_TRAINING_ROOT: 'FeatureTraining_Root', } as const; type Screen = DeepValueOf; diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index a11fb2a01cc8..ccd0f21626a0 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -10,7 +10,6 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {useSuggestionsContext} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import viewForwardedRef from '@src/types/utils/viewForwardedRef'; @@ -40,7 +39,6 @@ function BaseAutoCompleteSuggestions( suggestions, isSuggestionPickerLarge, keyExtractor, - shouldBeDisplayedBelowParentContainer = false, }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { @@ -49,7 +47,6 @@ function BaseAutoCompleteSuggestions( const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const scrollRef = useRef>(null); - const {activeID} = useSuggestionsContext(); /** * Render a suggestion menu item component. */ @@ -71,7 +68,7 @@ function BaseAutoCompleteSuggestions( ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBeDisplayedBelowParentContainer, Boolean(activeID))); + const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); const estimatedListSize = useMemo( () => ({ height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, diff --git a/src/components/AutoCompleteSuggestions/index.native.tsx b/src/components/AutoCompleteSuggestions/index.native.tsx index 863e806b143e..fbfa7d953581 100644 --- a/src/components/AutoCompleteSuggestions/index.native.tsx +++ b/src/components/AutoCompleteSuggestions/index.native.tsx @@ -1,14 +1,11 @@ import {Portal} from '@gorhom/portal'; import React from 'react'; -import {useSuggestionsContext} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; -import CONST from '@src/CONST'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { - const {activeID} = useSuggestionsContext(); return ( - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} {...props} /> diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 66ea0de6f9f3..baca4011a177 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -4,7 +4,6 @@ import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import {measureHeightOfSuggestionsContainer} from '@libs/SuggestionUtils'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; @@ -19,13 +18,11 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); - const suggestionsContainerHeight = measureHeightOfSuggestionsContainer(props.suggestions.length, props.isSuggestionPickerLarge); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, left: 0, bottom: 0, }); - const [shouldShowBelowContainer, setShouldShowBelowContainer] = React.useState(false); React.useEffect(() => { const container = containerRef.current; if (!container) { @@ -44,19 +41,13 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} if (!measureParentContainer) { return; } - - measureParentContainer((x, y, w, h) => { - const currentBottom = y < suggestionsContainerHeight ? windowHeight - y - suggestionsContainerHeight - h : windowHeight - y; - setShouldShowBelowContainer(y < suggestionsContainerHeight); - setContainerState({left: x, bottom: currentBottom, width: w}); - }); - }, [measureParentContainer, windowHeight, windowWidth, suggestionsContainerHeight]); + measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); + }, [measureParentContainer, windowHeight, windowWidth]); const componentToRender = ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} - shouldBeDisplayedBelowParentContainer={shouldShowBelowContainer} ref={containerRef} /> ); diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index d9824db1988d..61d614dcf2e4 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -1,6 +1,6 @@ import type {ReactElement} from 'react'; -type MeasureParentContainerCallback = (x: number, y: number, width: number, height: number) => void; +type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; type RenderSuggestionMenuItemProps = { item: TSuggestion; @@ -33,9 +33,6 @@ type AutoCompleteSuggestionsProps = { /** Meaures the parent container's position and dimensions. */ measureParentContainer?: (callback: MeasureParentContainerCallback) => void; - - /** Whether suggestion should be displayed below the parent container or not */ - shouldBeDisplayedBelowParentContainer?: boolean; }; export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index bf48894beaab..4acf197ba178 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -7,6 +7,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; +import * as UserUtils from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; import type {AvatarType} from '@src/types/onyx/OnyxCommon'; @@ -49,10 +50,13 @@ type AvatarProps = { /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; + + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ + accountID?: number | string; }; function Avatar({ - source, + source: originalSource, imageStyles, iconAdditionalStyles, containerStyles, @@ -62,6 +66,7 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', + accountID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -72,7 +77,7 @@ function Avatar({ useEffect(() => { setImageError(false); - }, [source]); + }, [originalSource]); const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); @@ -81,14 +86,15 @@ function Avatar({ const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; // We pass the color styles down to the SVG for the workspace and fallback avatar. - const useFallBackAvatar = imageError || source === Expensicons.FallbackAvatar || !source; + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, Number(accountID)); + const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; const avatarSource = useFallBackAvatar ? fallbackAvatar : source; let iconColors; if (isWorkspace) { - iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(name); + iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(accountID?.toString() ?? ''); } else if (useFallBackAvatar) { iconColors = StyleUtils.getBackgroundColorAndFill(theme.buttonHoveredBG, theme.icon); } else { diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index c7a4ece0de97..8942bf97a7dd 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -60,7 +60,8 @@ function AvatarWithDisplayName({ const title = ReportUtils.getReportName(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); - const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); + const isMoneyRequestOrReport = + ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index a46b37c986ba..72dc53cceb39 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -7,6 +7,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -17,7 +18,13 @@ import Tooltip from './Tooltip'; type BannerProps = { /** Text to display in the banner. */ - text: string; + text?: string; + + /** Content to display in the banner. */ + content?: React.ReactNode; + + /** The icon asset to display to the left of the text */ + icon?: IconAsset | null; /** Should this component render the left-aligned exclamation icon? */ shouldShowIcon?: boolean; @@ -41,7 +48,18 @@ type BannerProps = { textStyles?: StyleProp; }; -function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { +function Banner({ + text, + content, + icon = Expensicons.Exclamation, + onClose, + onPress, + containerStyles, + textStyles, + shouldRenderHTML = false, + shouldShowIcon = false, + shouldShowCloseButton = false, +}: BannerProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -65,15 +83,17 @@ function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRend ]} > - {shouldShowIcon && ( + {shouldShowIcon && icon && ( )} - {shouldRenderHTML ? ( + {content && content} + + {shouldRenderHTML && text ? ( ) : ( & { /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; + + /** Whether the button should use split style or not */ + isSplitButton?: boolean; }; type KeyboardShortcutComponentProps = Pick; @@ -198,6 +201,7 @@ function Button( id = '', accessibilityLabel = '', + isSplitButton = false, ...rest }: ButtonProps, ref: ForwardedRef, @@ -253,13 +257,22 @@ function Button( {shouldShowRightIcon && ( - + {!isSplitButton ? ( + + ) : ( + + )} )} diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index a4e6e2c87fec..a28b7ebf0864 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -14,6 +14,7 @@ import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu({ success = false, + isSplitButton = true, isLoading = false, isDisabled = false, pressOnEnter = false, @@ -40,7 +41,7 @@ function ButtonWithDropdownMenu({ const [isMenuVisible, setIsMenuVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); - const caretButton = useRef(null); + const caretButton = useRef(null); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; @@ -64,7 +65,6 @@ function ButtonWithDropdownMenu({ }); } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); - return ( {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -72,8 +72,10 @@ function ButtonWithDropdownMenu({ + + )} ) : ( - + ); } diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx deleted file mode 100644 index 64391909b197..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {Animated, View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx deleted file mode 100644 index 8757d66160c4..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({accessibilityHint, containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts deleted file mode 100644 index cfe791eed79c..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {StyleProp, ViewStyle} from 'react-native'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type FloatingMessageCounterContainerProps = ChildrenProps & { - /** Styles to be assigned to Container */ - containerStyles?: StyleProp; - - /** Specifies the accessibility hint for the component */ - accessibilityHint?: string; -}; - -export default FloatingMessageCounterContainerProps; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 09f6e8598e6c..07de62b1eabd 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -11,6 +11,7 @@ import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; +import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; @@ -18,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import getIconForAction from '@libs/getIconForAction'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; @@ -115,8 +117,11 @@ function AttachmentPickerWithMenuItems({ const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {windowHeight} = useWindowDimensions(); + const {windowHeight, windowWidth} = useWindowDimensions(); const {canUseTrackExpense} = usePermissions(); + const {isSmallScreenWidth} = useWindowDimensions(); + const isReportOpenInRHP = useIsReportOpenInRHP(); + const shouldUseNarrowLayout = isReportOpenInRHP || isSmallScreenWidth; /** * Returns the list of IOU Options @@ -124,25 +129,30 @@ function AttachmentPickerWithMenuItems({ const moneyRequestOptions = useMemo(() => { const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { - icon: Expensicons.Receipt, + icon: Expensicons.Transfer, text: translate('iou.splitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.SUBMIT]: { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.PAY]: { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? ''), }, [CONST.IOU.TYPE.TRACK]: { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? ''), }, + [CONST.IOU.TYPE.INVOICE]: { + icon: Expensicons.Invoice, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? ''), + }, }; return ReportUtils.temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ @@ -302,7 +312,7 @@ function AttachmentPickerWithMenuItems({ triggerAttachmentPicker(); } }} - anchorPosition={styles.createMenuPositionReportActionCompose(windowHeight)} + anchorPosition={styles.createMenuPositionReportActionCompose(shouldUseNarrowLayout, windowHeight, windowWidth)} anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM}} menuItems={menuItems} withoutOverlay diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8f42da5a1575..469a7300a84f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -44,6 +44,7 @@ import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutsi import type {ComposerRef, SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; +import variables from '@styles/variables'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; @@ -63,9 +64,6 @@ type AnimatedRef = ReturnType; type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsOnyxProps = { - /** The number of lines the comment should take up */ - numberOfLines: OnyxEntry; - /** The parent report actions for the report */ parentReportActions: OnyxEntry; @@ -215,7 +213,6 @@ function ComposerWithSuggestions( modal, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, parentReportActions, - numberOfLines, // Props: Report reportID, @@ -459,22 +456,9 @@ function ComposerWithSuggestions( ], ); - /** - * Update the number of lines for a comment in Onyx - */ - const updateNumberOfLines = useCallback( - (newNumberOfLines: number) => { - if (newNumberOfLines === numberOfLines) { - return; - } - Report.saveReportCommentNumberOfLines(reportID, newNumberOfLines); - }, - [reportID, numberOfLines], - ); - const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); - const commentLength = ReportUtils.getCommentLength(trimmedComment); + const commentLength = ReportUtils.getCommentLength(trimmedComment, {reportID}); // Don't submit empty comments or comments that exceed the character limit if (!commentLength || commentLength > CONST.MAX_COMMENT_LENGTH) { @@ -730,6 +714,11 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isOnlyEmojiLineHeight = useMemo(() => { + const isOnlyEmoji = EmojiUtils.containsOnlyEmojis(value); + return isOnlyEmoji ? {lineHeight: variables.fontSizeOnlyEmojisHeight} : {}; + }, [value]); + return ( <> @@ -743,7 +732,7 @@ function ComposerWithSuggestions( onChangeText={onChangeText} onKeyPress={triggerHotkeyActions} textAlignVertical="top" - style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} + style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose, isOnlyEmojiLineHeight]} maxLines={maxComposerLines} onFocus={onFocus} onBlur={onBlur} @@ -760,8 +749,6 @@ function ComposerWithSuggestions( isComposerFullSize={isComposerFullSize} value={value} testID="composer" - numberOfLines={numberOfLines ?? undefined} - onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition onLayout={onLayout} onScroll={hideSuggestionMenu} @@ -808,12 +795,6 @@ ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); export default withOnyx, ComposerWithSuggestionsOnyxProps>({ - numberOfLines: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, - // We might not have number of lines in onyx yet, for which the composer would be rendered as null - // during the first render, which we want to avoid: - initWithStoredValues: false, - }, modal: { key: ONYXKEYS.MODAL, }, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx deleted file mode 100644 index c7510710828d..000000000000 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import type {Dispatch, ForwardedRef, RefObject, SetStateAction} from 'react'; -import React, {useState} from 'react'; -import type {MeasureInWindowOnSuccessCallback, TextInput} from 'react-native'; -import Composer from '@components/Composer'; -import type {ComposerProps} from '@components/Composer/types'; -import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; -import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; - -type Selection = { - start: number; - end: number; -}; - -type ComposerWithSuggestionsEditProps = ComposerProps & { - setValue: Dispatch>; - setSelection: Dispatch>; - resetKeyboardInput: () => void; - isComposerFocused: boolean; - suggestionsRef: RefObject; - updateDraft: (newValue: string) => void; - measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; - value: string; - selection: Selection; - isGroupPolicyReport: boolean; -}; - -function ComposerWithSuggestionsEdit( - { - value, - maxLines = -1, - onKeyPress = () => {}, - style, - onSelectionChange = () => {}, - selection = { - start: 0, - end: 0, - }, - onBlur = () => {}, - onFocus = () => {}, - onChangeText = () => {}, - setValue = () => {}, - setSelection = () => {}, - resetKeyboardInput = () => {}, - isComposerFocused, - suggestionsRef, - updateDraft, - measureParentContainer, - id = undefined, - isGroupPolicyReport, - }: ComposerWithSuggestionsEditProps, - ref: ForwardedRef, -) { - const [composerHeight, setComposerHeight] = useState(0); - - return ( - <> - { - const composerLayoutHeight = e.nativeEvent.layout.height; - if (composerHeight === composerLayoutHeight) { - return; - } - setComposerHeight(composerLayoutHeight); - }} - /> - - - - ); -} - -export default React.forwardRef(ComposerWithSuggestionsEdit); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx deleted file mode 100644 index ceecb56af450..000000000000 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type {MutableRefObject, ReactNode} from 'react'; -import React, {createContext, useCallback, useContext, useMemo, useRef, useState} from 'react'; -import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; - -type SuggestionsContextProviderProps = { - children?: ReactNode; -}; - -type SuggestionsContextProps = { - activeID: string | null; - currentActiveSuggestionsRef: MutableRefObject; - updateCurrentActiveSuggestionsRef: (ref: SuggestionsRef | null, id: string) => void; - clearActiveSuggestionsRef: () => void; - isActiveSuggestions: (id: string) => boolean; -}; - -const SuggestionsContext = createContext({ - activeID: null, - currentActiveSuggestionsRef: {current: null}, - updateCurrentActiveSuggestionsRef: () => {}, - clearActiveSuggestionsRef: () => {}, - isActiveSuggestions: () => false, -}); - -function SuggestionsContextProvider({children}: SuggestionsContextProviderProps) { - const currentActiveSuggestionsRef = useRef(null); - const [activeID, setActiveID] = useState(null); - - const updateCurrentActiveSuggestionsRef = useCallback((ref: SuggestionsRef | null, id: string) => { - currentActiveSuggestionsRef.current = ref; - setActiveID(id); - }, []); - - const clearActiveSuggestionsRef = useCallback(() => { - currentActiveSuggestionsRef.current = null; - setActiveID(null); - }, []); - - const isActiveSuggestions = useCallback((id: string) => id === activeID, [activeID]); - - const contextValue = useMemo( - () => ({activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions}), - [activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions], - ); - - return {children}; -} - -function useSuggestionsContext() { - const context = useContext(SuggestionsContext); - return context; -} - -SuggestionsContextProvider.displayName = 'SuggestionsContextProvider'; - -export {SuggestionsContextProvider, useSuggestionsContext}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 838e2466c6f3..5bfa2475ee23 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -61,7 +61,6 @@ type SuggestionsRef = { updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; getSuggestions: () => Mention[] | Emoji[]; - updateShouldShowSuggestionMenuAfterScrolling: () => void; }; type ReportActionComposeOnyxProps = { @@ -343,7 +342,8 @@ function ReportActionCompose({ [], ); - const isGroupPolicyReport = useMemo(() => ReportUtils.isGroupPolicy(report), [report]); + // When we invite someone to a room they don't have the policy object, but we still want them to be able to mention other reports they are members of, so we only check if the policyID in the report is from a workspace + const isGroupPolicyReport = useMemo(() => !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE, [report]); const reportRecipientAcountIDs = ReportUtils.getReportRecipientAccountIDs(report, currentUserPersonalDetails.accountID); const reportRecipient = personalDetails[reportRecipientAcountIDs[0]]; const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; @@ -379,7 +379,7 @@ function ReportActionCompose({ {shouldShowReportRecipientLocalTime && hasReportRecipient && } - + { - setSuggestionValues((prevState) => ({...prevState, shouldShowSuggestionMenu: !!prevState.suggestedEmojis.length})); - }, []); - /** * Listens for keyboard shortcuts and applies the action */ @@ -219,17 +215,8 @@ function SuggestionEmoji( setShouldBlockSuggestionCalc, updateShouldShowSuggestionMenuToFalse, getSuggestions, - updateShouldShowSuggestionMenuAfterScrolling, }), - [ - onSelectionChange, - resetSuggestions, - setShouldBlockSuggestionCalc, - triggerHotkeyActions, - updateShouldShowSuggestionMenuToFalse, - getSuggestions, - updateShouldShowSuggestionMenuAfterScrolling, - ], + [onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], ); if (!isEmojiSuggestionsMenuVisible) { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 37032a2550fe..05e1163da200 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -86,10 +86,10 @@ function SuggestionMention( const debouncedSearchInServer = useDebounce( useCallback(() => { const foundSuggestionsCount = suggestionValues.suggestedMentions.length; - if (suggestionValues.prefixType === '#' && foundSuggestionsCount < 5) { + if (suggestionValues.prefixType === '#' && foundSuggestionsCount < 5 && isGroupPolicyReport) { ReportUserActions.searchInServer(value, policyID); } - }, [suggestionValues.suggestedMentions.length, suggestionValues.prefixType, policyID, value]), + }, [suggestionValues.suggestedMentions.length, suggestionValues.prefixType, policyID, value, isGroupPolicyReport]), CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME, ); @@ -360,10 +360,6 @@ function SuggestionMention( }); }, []); - const updateShouldShowSuggestionMenuAfterScrolling = useCallback(() => { - setSuggestionValues((prevState) => ({...prevState, shouldShowSuggestionMenu: !!prevState.suggestedMentions.length})); - }, []); - const setShouldBlockSuggestionCalc = useCallback( (shouldBlockSuggestionCalc: boolean) => { shouldBlockCalc.current = shouldBlockSuggestionCalc; @@ -381,9 +377,8 @@ function SuggestionMention( setShouldBlockSuggestionCalc, updateShouldShowSuggestionMenuToFalse, getSuggestions, - updateShouldShowSuggestionMenuAfterScrolling, }), - [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions, updateShouldShowSuggestionMenuAfterScrolling], + [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], ); if (!isMentionSuggestionsMenuVisible) { diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 288a8b1a6d81..8ebd52f62428 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -127,11 +127,6 @@ function Suggestions( suggestionMentionRef.current?.updateShouldShowSuggestionMenuToFalse(); }, []); - const updateShouldShowSuggestionMenuAfterScrolling = useCallback(() => { - suggestionEmojiRef.current?.updateShouldShowSuggestionMenuAfterScrolling(); - suggestionMentionRef.current?.updateShouldShowSuggestionMenuAfterScrolling(); - }, []); - const setShouldBlockSuggestionCalc = useCallback((shouldBlock: boolean) => { suggestionEmojiRef.current?.setShouldBlockSuggestionCalc(shouldBlock); suggestionMentionRef.current?.setShouldBlockSuggestionCalc(shouldBlock); @@ -146,17 +141,8 @@ function Suggestions( updateShouldShowSuggestionMenuToFalse, setShouldBlockSuggestionCalc, getSuggestions, - updateShouldShowSuggestionMenuAfterScrolling, }), - [ - onSelectionChange, - resetSuggestions, - setShouldBlockSuggestionCalc, - triggerHotkeyActions, - updateShouldShowSuggestionMenuToFalse, - getSuggestions, - updateShouldShowSuggestionMenuAfterScrolling, - ], + [onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], ); useEffect(() => { diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index efb2d8ba73fb..5697807ca825 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -29,6 +29,7 @@ import TaskAction from '@components/ReportActionItem/TaskAction'; import TaskPreview from '@components/ReportActionItem/TaskPreview'; import TaskView from '@components/ReportActionItem/TaskView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; +import SpacerView from '@components/SpacerView'; import Text from '@components/Text'; import UnreadActionIndicator from '@components/UnreadActionIndicator'; import useLocalize from '@hooks/useLocalize'; @@ -66,6 +67,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {OriginalMessageActionableMentionWhisper, OriginalMessageActionableTrackedExpenseWhisper, OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; +import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; @@ -150,6 +152,12 @@ type ReportActionItemProps = { /** Callback to be called on onPress */ onPress?: () => void; + + /** If this is the first visible report action */ + isFirstVisibleReportAction: boolean; + + /** IF the thread divider line will be used */ + shouldUseThreadDividerLine?: boolean; } & ReportActionItemOnyxProps; const isIOUReport = (actionObj: OnyxEntry): actionObj is OnyxTypes.ReportActionBase & OnyxTypes.OriginalMessageIOU => @@ -174,6 +182,8 @@ function ReportActionItem({ policy, transaction, onPress = undefined, + isFirstVisibleReportAction = false, + shouldUseThreadDividerLine = false, }: ReportActionItemProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -459,6 +469,22 @@ function ReportActionItem({ ]; }, [action, report.reportID]); + const renderThreadDivider = useMemo( + () => + shouldHideThreadDividerLine ? ( + + ) : ( + + ), + [shouldHideThreadDividerLine, styles.reportHorizontalRule, report.reportID], + ); + /** * Get the content of ReportActionItem * @param hovered whether the ReportActionItem is hovered @@ -586,6 +612,10 @@ function ReportActionItem({ children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION) { + children = ; } else { const hasBeenFlagged = ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && @@ -631,7 +661,6 @@ function ReportActionItem({ action={action} draftMessage={draftMessage} reportID={report.reportID} - isGroupPolicyReport={ReportUtils.isGroupPolicy(report)} index={index} ref={textInputRef} // Avoid defining within component due to an existing Onyx bug @@ -765,11 +794,13 @@ function ReportActionItem({ } return ( - + + + {renderThreadDivider} + ); } @@ -797,40 +828,44 @@ function ReportActionItem({ - + + {renderThreadDivider} ); } - if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) { return ( {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( <> {transactionCurrency !== report.currency && ( - + <> + + {renderThreadDivider} + )} - + + + {renderThreadDivider} + ) : ( - + <> + + {renderThreadDivider} + )} ); @@ -907,7 +942,7 @@ function ReportActionItem({ > {(hovered) => ( - {shouldDisplayNewMarker && } + {shouldDisplayNewMarker && (!shouldUseThreadDividerLine || !isFirstVisibleReportAction) && } prevProps.policy?.name === nextProps.policy?.name && - prevProps.policy?.avatar === nextProps.policy?.avatar && + prevProps.policy?.avatarURL === nextProps.policy?.avatarURL && prevProps.report?.stateNum === nextProps.report?.stateNum && prevProps.report?.statusNum === nextProps.report?.statusNum && prevProps.report?.lastReadTime === nextProps.report?.lastReadTime && diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 421faae66bf0..de9e0b6a6ece 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -1,6 +1,5 @@ import React, {memo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; -import type {AvatarProps} from '@components/Avatar'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; @@ -39,7 +38,7 @@ type ReportActionItemFragmentProps = { delegateAccountID?: number; /** icon */ - actorIcon?: AvatarProps; + actorIcon?: OnyxCommon.Icon; /** Whether the comment is a thread parent message/the first message in a thread */ isThreadParentMessage?: boolean; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index aeb870406adf..5fb23be145fb 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -1,12 +1,12 @@ -import {PortalHost} from '@gorhom/portal'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter, findNodeHandle, Keyboard, NativeModules, View} from 'react-native'; -import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; +import {Keyboard, View} from 'react-native'; +import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; +import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; import Icon from '@components/Icon'; @@ -42,13 +42,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; -import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; -import {useSuggestionsContext} from './ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; -import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose'; import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection'; -const {RNTextInputReset} = NativeModules; - type ReportActionItemMessageEditProps = { /** All the data of the action */ action: OnyxTypes.ReportAction; @@ -59,9 +54,6 @@ type ReportActionItemMessageEditProps = { /** ReportID that holds the comment we're editing */ reportID: string; - /** If current composer is connected with report from group policy */ - isGroupPolicyReport: boolean; - /** Position index of the report action in the overall report FlatList view */ index: number; @@ -80,7 +72,7 @@ const isMobileSafari = Browser.isMobileSafari(); const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection(); function ReportActionItemMessageEdit( - {action, draftMessage, reportID, isGroupPolicyReport, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, + {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, forwardedRef: ForwardedRef<(TextInput & HTMLTextAreaElement) | undefined>, ) { const theme = useTheme(); @@ -90,7 +82,6 @@ function ReportActionItemMessageEdit( const {translate, preferredLocale} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); const {isSmallScreenWidth} = useWindowDimensions(); - const {updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions} = useSuggestionsContext(); const prevDraftMessage = usePrevious(draftMessage); const getInitialSelection = () => { @@ -121,8 +112,6 @@ function ReportActionItemMessageEdit( const isFocusedRef = useRef(false); const insertedEmojis = useRef([]); const draftRef = useRef(draft); - const containerRef = useRef(null); - const suggestionsRef = useRef(null); const emojiPickerSelectionRef = useRef(undefined); // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -186,7 +175,7 @@ function ReportActionItemMessageEdit( // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), // so we need to ensure that it is only updated after focus. if (isMobileSafari) { - setDraft((prevDraft: string) => { + setDraft((prevDraft) => { setSelection({ start: prevDraft.length, end: prevDraft.length, @@ -215,9 +204,6 @@ function ReportActionItemMessageEdit( if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.clearActiveReportAction(); } - if (isActiveSuggestions(action.reportActionID)) { - clearActiveSuggestionsRef(); - } // Show the main composer when the focused message is deleted from another client // to prevent the main composer stays hidden until we swtich to another chat. @@ -276,7 +262,6 @@ function ReportActionItemMessageEdit( if (emojis?.length > 0) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis?.length > 0) { - suggestionsRef.current?.resetSuggestions(); insertedEmojis.current = [...insertedEmojis.current, ...newEmojis]; debouncedUpdateFrequentlyUsedEmojis(); } @@ -333,7 +318,7 @@ function ReportActionItemMessageEdit( */ const publishDraft = useCallback(() => { // Do nothing if draft exceed the character limit - if (ReportUtils.getCommentLength(draft) > CONST.MAX_COMMENT_LENGTH) { + if (ReportUtils.getCommentLength(draft, {reportID}) > CONST.MAX_COMMENT_LENGTH) { return; } @@ -374,10 +359,6 @@ function ReportActionItemMessageEdit( */ const triggerSaveOrCancel = useCallback( (e: NativeSyntheticEvent | KeyboardEvent) => { - if (suggestionsRef.current?.triggerHotkeyActions(e as KeyboardEvent)) { - return; - } - if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } @@ -393,57 +374,18 @@ function ReportActionItemMessageEdit( [deleteDraft, isKeyboardShown, isSmallScreenWidth, publishDraft], ); - const resetKeyboardInput = useCallback(() => { - if (!RNTextInputReset) { - return; - } - RNTextInputReset.resetKeyboardInput(findNodeHandle(textInputRef.current)); - }, [textInputRef]); - - const measureContainer = useCallback((callback: MeasureInWindowOnSuccessCallback) => { - if (!containerRef.current) { - return; - } - containerRef.current.measureInWindow(callback); - }, []); - /** * Focus the composer text input */ const focus = focusComposerWithDelay(textInputRef.current); useEffect(() => { - validateCommentMaxLength(draft); - }, [draft, validateCommentMaxLength]); - - /** - * Listen scrolling event - */ - useEffect(() => { - if (!isFocused || !suggestionsRef.current) { - return () => {}; - } - const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { - if (scrolling) { - suggestionsRef?.current?.resetSuggestions(); - return; - } - // Reopen the suggestion after scroll has end - suggestionsRef?.current?.updateShouldShowSuggestionMenuAfterScrolling(); - }); - - return () => { - scrollingListener.remove(); - }; - }, [isFocused]); + validateCommentMaxLength(draft, {reportID}); + }, [draft, reportID, validateCommentMaxLength]); return ( <> - - + - { textInputRef.current = el; @@ -503,33 +445,18 @@ function ReportActionItemMessageEdit( if (!ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.clearActiveReportAction(); } - - updateCurrentActiveSuggestionsRef(suggestionsRef.current, action.reportActionID); }} onBlur={(event: NativeSyntheticEvent) => { setIsFocused(false); // @ts-expect-error TODO: TextInputFocusEventData doesn't contain relatedTarget. const relatedTargetId = event.nativeEvent?.relatedTarget?.id; - suggestionsRef.current?.resetSuggestions(); - clearActiveSuggestionsRef(); - if (relatedTargetId && [messageEditInput, emojiButtonID].includes(relatedTargetId)) { + if ((relatedTargetId && [messageEditInput, emojiButtonID].includes(relatedTargetId)) || EmojiPickerAction.isEmojiPickerVisible()) { return; } setShouldShowComposeInputKeyboardAware(true); }} selection={selection} - onSelectionChange={(e) => { - suggestionsRef.current?.onSelectionChange?.(e); - setSelection(e.nativeEvent.selection); - }} - setValue={setDraft} - setSelection={setSelection} - isComposerFocused={!!textInputRef.current && textInputRef.current.isFocused()} - resetKeyboardInput={resetKeyboardInput} - suggestionsRef={suggestionsRef} - updateDraft={updateDraft} - measureParentContainer={measureContainer} - isGroupPolicyReport={isGroupPolicyReport} + onSelectionChange={(e) => setSelection(e.nativeEvent.selection)} /> diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index a4592075aa0c..5de85eb5f43f 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -44,6 +44,12 @@ type ReportActionItemParentActionProps = { /** Whether we should display "Replies" divider */ shouldDisplayReplyDivider: boolean; + + /** If this is the first visible report action */ + isFirstVisibleReportAction: boolean; + + /** If the thread divider line will be used */ + shouldUseThreadDividerLine?: boolean; }; function ReportActionItemParentAction({ @@ -54,6 +60,8 @@ function ReportActionItemParentAction({ index = 0, shouldHideThreadDividerLine = false, shouldDisplayReplyDivider, + isFirstVisibleReportAction = false, + shouldUseThreadDividerLine = false, }: ReportActionItemParentActionProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -124,6 +132,8 @@ function ReportActionItemParentAction({ isMostRecentIOUReportAction={false} shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} index={index} + isFirstVisibleReportAction={isFirstVisibleReportAction} + shouldUseThreadDividerLine={shouldUseThreadDividerLine} /> ))} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index dda17e1e83d3..234147a30bd5 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -85,7 +85,8 @@ function ReportActionItemSingle({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); - const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); + const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? {}); + const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { @@ -107,10 +108,14 @@ function ReportActionItemSingle({ 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 - const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID; + const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport ? iouReport?.managerID : iouReport?.ownerAccountID; const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); - displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; + + if (!isInvoiceReport) { + displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; + } + secondaryAvatar = { source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, @@ -200,6 +205,7 @@ function ReportActionItemSingle({ source={icon.source} type={icon.type} name={icon.name} + accountID={icon.id} fallbackIcon={fallbackIcon} /> diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 8549af3ee483..fd41185242bb 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -33,7 +33,6 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; import FloatingMessageCounter from './FloatingMessageCounter'; import getInitialNumToRender from './getInitialNumReportActionsToRender'; import ListBoundaryLoader from './ListBoundaryLoader'; -import {useSuggestionsContext} from './ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; type LoadNewerChats = DebouncedFunc<(params: {distanceFromStart: number}) => void>; @@ -164,7 +163,6 @@ function ReportActionsList({ const reportScrollManager = useReportScrollManager(); const userActiveSince = useRef(null); const lastMessageTime = useRef(null); - const {currentActiveSuggestionsRef} = useSuggestionsContext(); const [isVisible, setIsVisible] = useState(false); const isFocused = useIsFocused(); @@ -202,7 +200,7 @@ function ReportActionsList({ ); const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); - const hasNewestReportAction = sortedReportActions?.[0].created === report.lastVisibleActionCreated; + const hasNewestReportAction = sortedVisibleReportActions?.[0]?.created === report.lastVisibleActionCreated; const hasNewestReportActionRef = useRef(hasNewestReportAction); hasNewestReportActionRef.current = hasNewestReportAction; const previousLastIndex = useRef(lastActionIndex); @@ -327,16 +325,14 @@ function ReportActionsList({ const scrollToBottomForCurrentUserAction = useCallback( (isFromCurrentUser: boolean) => { - // If a new comment is added and it's from the current user scroll to the bottom - // otherwise leave the user positioned where they are now in the list. - // Additionally, since the first report action could be a whisper message (new WS) -> - // hasNewestReportAction will be false, check isWhisperAction is false before returning early. - if (!isFromCurrentUser || (!hasNewestReportActionRef.current && !ReportActionsUtils.isWhisperAction(sortedReportActions?.[0]))) { + // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where + // they are now in the list. + if (!isFromCurrentUser || !hasNewestReportActionRef.current) { return; } InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); }, - [sortedReportActions, reportScrollManager], + [reportScrollManager], ); useEffect(() => { // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? @@ -426,6 +422,8 @@ function ReportActionsList({ [sortedReportActions, isOffline, currentUnreadMarker], ); + const firstVisibleReportActionID = useMemo(() => ReportActionsUtils.getFirstVisibleReportActionID(sortedReportActions, isOffline), [sortedReportActions, isOffline]); + /** * Evaluate new unread marker visibility for each of the report actions. */ @@ -455,6 +453,24 @@ function ReportActionsList({ [currentUnreadMarker, sortedVisibleReportActions, report.reportID, messageManuallyMarkedUnread], ); + const shouldUseThreadDividerLine = useMemo(() => { + const topReport = sortedVisibleReportActions.length > 0 ? sortedVisibleReportActions[sortedVisibleReportActions.length - 1] : null; + + if (topReport && topReport.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return false; + } + + if (ReportActionsUtils.isTransactionThread(parentReportAction)) { + return !ReportActionsUtils.isDeletedParentAction(parentReportAction) && !ReportActionsUtils.isReversedTransaction(parentReportAction); + } + + if (ReportUtils.isTaskReport(report)) { + return !ReportUtils.isCanceledTaskReport(report, parentReportAction); + } + + return ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report); + }, [parentReportAction, report, sortedVisibleReportActions]); + const calculateUnreadMarker = useCallback(() => { // Iterate through the report actions and set appropriate unread marker. // This is to avoid a warning of: @@ -540,6 +556,8 @@ function ReportActionsList({ shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} shouldDisplayReplyDivider={sortedReportActions.length > 1} + isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID} + shouldUseThreadDividerLine={shouldUseThreadDividerLine} /> ), [ @@ -554,6 +572,8 @@ function ReportActionsList({ reportActions, transactionThreadReport, parentReportActionForTransactionThread, + shouldUseThreadDividerLine, + firstVisibleReportActionID, ], ); @@ -650,18 +670,6 @@ function ReportActionsList({ onScrollToIndexFailed={onScrollToIndexFailed} extraData={extraData} key={listID} - onScrollBeginDrag={() => { - if (!currentActiveSuggestionsRef.current) { - return; - } - currentActiveSuggestionsRef.current.resetSuggestions(); - }} - onScrollEndDrag={() => { - if (!currentActiveSuggestionsRef.current) { - return; - } - currentActiveSuggestionsRef.current.updateShouldShowSuggestionMenuAfterScrolling(); - }} shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index e35b58dd9dae..8782d6dbce55 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -46,6 +46,12 @@ type ReportActionsListItemRendererProps = { /** Whether we should display "Replies" divider */ shouldDisplayReplyDivider: boolean; + + /** If this is the first visible report action */ + isFirstVisibleReportAction: boolean; + + /** If the thread divider line will be used */ + shouldUseThreadDividerLine?: boolean; }; function ReportActionsListItemRenderer({ @@ -61,6 +67,8 @@ function ReportActionsListItemRenderer({ shouldDisplayNewMarker, linkedReportActionID = '', shouldDisplayReplyDivider, + isFirstVisibleReportAction = false, + shouldUseThreadDividerLine = false, parentReportActionForTransactionThread, }: ReportActionsListItemRendererProps) { const shouldDisplayParentAction = @@ -144,6 +152,8 @@ function ReportActionsListItemRenderer({ reportActions={reportActions} transactionThreadReport={transactionThreadReport} index={index} + isFirstVisibleReportAction={isFirstVisibleReportAction} + shouldUseThreadDividerLine={shouldUseThreadDividerLine} /> ) : ( ); } diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 26f796b8bdc4..02008a464859 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -138,22 +138,18 @@ function ReportActionsView({ // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) // so that we display transaction-level and report-level report actions in order in the one-transaction view - const [combinedReportActions, parentReportActionForTransactionThread] = useMemo(() => { - if (isEmptyObject(transactionThreadReportActions)) { - return [allReportActions, null]; - } - - // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` - const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); - const moneyRequestAction = allReportActions.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID); + const combinedReportActions = useMemo( + () => ReportActionsUtils.getCombinedReportActions(allReportActions, transactionThreadReportActions), + [allReportActions, transactionThreadReportActions], + ); - // Filter out the expense actions because we don't want to show any preview actions for one-transaction reports - const filteredReportActions = [...allReportActions, ...filteredTransactionThreadReportActions].filter((action) => { - const actionType = (action as OnyxTypes.OriginalMessageIOU).originalMessage?.type ?? ''; - return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK && !ReportActionsUtils.isSentMoneyReportAction(action); - }); - return [ReportActionsUtils.getSortedReportActions(filteredReportActions, true), moneyRequestAction ?? null]; - }, [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID]); + const parentReportActionForTransactionThread = useMemo( + () => + isEmptyObject(transactionThreadReportActions) + ? null + : (allReportActions.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID) as OnyxEntry), + [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], + ); const indexOfLinkedAction = useMemo(() => { if (!reportActionID) { @@ -322,13 +318,13 @@ function ReportActionsView({ } if (!isEmptyObject(transactionThreadReport)) { - // Get newer actions based on the newest reportAction for the current report + // Get older actions based on the oldest reportAction for the current report const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID); - Report.getNewerActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0'); + Report.getOlderActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0'); - // Get newer actions based on the newest reportAction for the transaction thread report + // Get older actions based on the oldest reportAction for the transaction thread report const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID); - Report.getNewerActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0'); + Report.getOlderActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0'); } else { // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); @@ -348,10 +344,18 @@ function ReportActionsView({ newestReportAction: newestReportAction.pendingAction, firstReportActionID: newestReportAction?.reportActionID, isLoadingOlderReportsFirstNeeded, + reportActionID, })}`, ); - if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + if ( + !reportActionID || + !isFocused || + isLoadingInitialReportActions || + isLoadingOlderReportActions || + network.isOffline || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } @@ -368,6 +372,7 @@ function ReportActionsView({ network.isOffline, reportActions.length, newestReportAction, + isFocused, ]); /** diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 04fbd0308390..9a8ca2955127 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -21,6 +21,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; +import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; type ReportFooterOnyxProps = { /** Whether to show the compose input */ @@ -81,6 +82,8 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); + const canWriteInReport = ReportUtils.canWriteInReport(report); + const isSystemChat = ReportUtils.isSystemChat(report); const allPersonalDetails = usePersonalDetails(); @@ -131,7 +134,7 @@ function ReportFooter({ return ( <> {hideComposer && ( - + {isAnonymousUser && !isArchivedRoom && ( )} {isArchivedRoom && } + {!isAnonymousUser && !canWriteInReport && isSystemChat && } {!isSmallScreenWidth && {hideComposer && }} )} diff --git a/src/pages/home/report/SystemChatReportFooterMessage.tsx b/src/pages/home/report/SystemChatReportFooterMessage.tsx new file mode 100644 index 000000000000..c9ccac8f5c18 --- /dev/null +++ b/src/pages/home/report/SystemChatReportFooterMessage.tsx @@ -0,0 +1,91 @@ +import React, {useMemo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Banner from '@components/Banner'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import * as ReportInstance from '@userActions/Report'; +import type {OnboardingPurposeType} from '@src/CONST'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Policy as PolicyType} from '@src/types/onyx'; + +type SystemChatReportFooterMessageOnyxProps = { + /** Saved onboarding purpose selected by the user */ + choice: OnyxEntry; + + /** The list of this user's policies */ + policies: OnyxCollection; + + /** policyID for main workspace */ + activePolicyID: OnyxEntry>; +}; + +type SystemChatReportFooterMessageProps = SystemChatReportFooterMessageOnyxProps; + +function SystemChatReportFooterMessage({choice, policies, activePolicyID}: SystemChatReportFooterMessageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const adminChatReport = useMemo(() => { + const adminPolicy = activePolicyID + ? PolicyUtils.getPolicy(activePolicyID ?? '') + : Object.values(policies ?? {}).find((policy) => PolicyUtils.shouldShowPolicy(policy, false) && policy?.role === CONST.POLICY.ROLE.ADMIN && policy?.chatReportIDAdmins); + + return ReportUtils.getReport(String(adminPolicy?.chatReportIDAdmins)); + }, [activePolicyID, policies]); + + const content = useMemo(() => { + switch (choice) { + case CONST.ONBOARDING_CHOICES.MANAGE_TEAM: + return ( + <> + {translate('systemChatFooterMessage.newDotManageTeam.phrase1')} + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))}> + {adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} + + {translate('systemChatFooterMessage.newDotManageTeam.phrase2')} + + ); + default: + return ( + <> + {translate('systemChatFooterMessage.default.phrase1')} + ReportInstance.navigateToConciergeChat()}>{CONST?.CONCIERGE_CHAT_NAME} + {translate('systemChatFooterMessage.default.phrase2')} + + ); + } + }, [adminChatReport?.reportName, adminChatReport?.reportID, choice, translate]); + + return ( + {content}} + /> + ); +} + +SystemChatReportFooterMessage.displayName = 'SystemChatReportFooterMessage'; + +export default withOnyx({ + choice: { + key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + activePolicyID: { + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + initialValue: null, + }, +})(SystemChatReportFooterMessage); diff --git a/src/pages/home/report/reportActionSourcePropType.js b/src/pages/home/report/reportActionSourcePropType.js deleted file mode 100644 index 0ad9662eb693..000000000000 --- a/src/pages/home/report/reportActionSourcePropType.js +++ /dev/null @@ -1,3 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']); diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 1000ceff1a76..46f7d2410ffe 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -1,152 +1,56 @@ import {useIsFocused} from '@react-navigation/native'; -import {deepEqual} from 'fast-equals'; import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import type {ValueOf} from 'type-fest'; -import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; -import withCurrentReportID from '@components/withCurrentReportID'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePrevious from '@hooks/usePrevious'; +import type {PolicySelector} from '@hooks/useReportIDs'; +import {policySelector, useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import SidebarUtils from '@libs/SidebarUtils'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; -type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; -type PolicySelector = Pick; -type ReportActionsSelector = Array>; - type SidebarLinksDataOnyxProps = { - /** List of reports */ - chatReports: OnyxCollection; - /** Whether the reports are loading. When false it means they are ready to be used. */ isLoadingApp: OnyxEntry; /** The chat priority mode */ priorityMode: OnyxEntry>; - /** Beta features list */ - betas: OnyxEntry; - - /** All report actions for all reports */ - allReportActions: OnyxCollection; - /** The policies which the user has access to */ policies: OnyxCollection; - - /** All of the transaction violations */ - transactionViolations: OnyxCollection; - - /** Drafts of reports */ - reportsDrafts: OnyxCollection; }; -type SidebarLinksDataProps = CurrentReportIDContextValue & - SidebarLinksDataOnyxProps & { - /** Toggles the navigation menu open and closed */ - onLinkClick: () => void; +type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { + /** Toggles the navigation menu open and closed */ + onLinkClick: () => void; - /** Safe area insets required for mobile devices margins */ - insets: EdgeInsets; - }; + /** Safe area insets required for mobile devices margins */ + insets: EdgeInsets; +}; -function SidebarLinksData({ - allReportActions, - betas, - chatReports, - currentReportID, - insets, - isLoadingApp = true, - onLinkClick, - policies, - priorityMode = CONST.PRIORITY_MODE.DEFAULT, - transactionViolations, - reportsDrafts, -}: SidebarLinksDataProps) { +function SidebarLinksData({insets, isLoadingApp = true, onLinkClick, priorityMode = CONST.PRIORITY_MODE.DEFAULT, policies}: SidebarLinksDataProps) { const {accountID} = useCurrentUserPersonalDetails(); - const network = useNetwork(); const isFocused = useIsFocused(); const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); - const prevPriorityMode = usePrevious(priorityMode); const policyMemberAccountIDs = getPolicyEmployeeListByIdWithoutCurrentUser(policies, activeWorkspaceID, accountID); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '', policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef(null); const isLoading = isLoadingApp; - - const optionItemsMemoized: string[] = useMemo( - () => - SidebarUtils.getOrderedReportIDs( - null, - chatReports, - betas, - policies as OnyxCollection, - priorityMode, - allReportActions as OnyxCollection, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ), - // we need reports draft in deps array for reloading of list when reportDrafts will change - // eslint-disable-next-line react-hooks/exhaustive-deps - [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, reportsDrafts], - ); - - const optionListItems: string[] | null = useMemo(() => { - const reportIDs = optionItemsMemoized; - - if (deepEqual(reportIDsRef.current, reportIDs)) { - return reportIDsRef.current; - } - - // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 - // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. - // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { - reportIDsRef.current = reportIDs; - } - return reportIDsRef.current ?? []; - }, [optionItemsMemoized, priorityMode, isLoading, network.isOffline, prevPriorityMode]); - // We need to make sure the current report is in the list of reports, but we do not want - // to have to re-generate the list every time the currentReportID changes. To do that - // we first generate the list as if there was no current report, then here we check if - // the current report is missing from the list, which should very rarely happen. In this - // case we re-generate the list a 2nd time with the current report included. - const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !optionListItems?.includes(currentReportID)) { - return SidebarUtils.getOrderedReportIDs( - currentReportID, - chatReports as OnyxCollection, - betas, - policies as OnyxCollection, - priorityMode, - allReportActions as OnyxCollection, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); - } - return optionListItems ?? []; - }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs]); + const {orderedReportIDs, currentReportID} = useReportIDs(); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; @@ -167,8 +71,8 @@ function SidebarLinksData({ // Data props: isActiveReport={isActiveReport} isLoading={isLoading ?? false} - optionListItems={optionListItemsWithCurrentReport} activeWorkspaceID={activeWorkspaceID} + optionListItems={orderedReportIDs} /> ); @@ -176,105 +80,7 @@ function SidebarLinksData({ SidebarLinksData.displayName = 'SidebarLinksData'; -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - */ -const chatReportSelector = (report: OnyxEntry): ChatReportSelector => - (report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, - }, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }) as ChatReportSelector; - -const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => - (reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = reportAction.message?.[0]?.moderationDecision?.decision; - - return { - reportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ] as Message[], - originalMessage, - }; - })) as ReportActionsSelector; - -const policySelector = (policy: OnyxEntry): PolicySelector => - (policy && { - type: policy.type, - name: policy.name, - avatar: policy.avatar, - employeeList: policy.employeeList, - }) as PolicySelector; - -const SidebarLinkDataWithCurrentReportID = withCurrentReportID( - /* - While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs. - With that we can reduce app start up time by ~2s on heavy account. - More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534 - */ - memo( - SidebarLinksData, - (prevProps, nextProps) => - lodashIsEqual(prevProps.chatReports, nextProps.chatReports) && - lodashIsEqual(prevProps.allReportActions, nextProps.allReportActions) && - prevProps.isLoadingApp === nextProps.isLoadingApp && - prevProps.priorityMode === nextProps.priorityMode && - lodashIsEqual(prevProps.betas, nextProps.betas) && - lodashIsEqual(prevProps.policies, nextProps.policies) && - lodashIsEqual(prevProps.insets, nextProps.insets) && - prevProps.onLinkClick === nextProps.onLinkClick && - lodashIsEqual(prevProps.transactionViolations, nextProps.transactionViolations) && - prevProps.currentReportID === nextProps.currentReportID && - lodashIsEqual(prevProps.reportsDrafts, nextProps.reportsDrafts), - ), -); - -export default withOnyx, SidebarLinksDataOnyxProps>({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - initialValue: {}, - }, +export default withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, @@ -282,26 +88,24 @@ export default withOnyx + prevProps.isLoadingApp === nextProps.isLoadingApp && + prevProps.priorityMode === nextProps.priorityMode && + lodashIsEqual(prevProps.insets, nextProps.insets) && + prevProps.onLinkClick === nextProps.onLinkClick && + lodashIsEqual(prevProps.policies, nextProps.policies), + ), +); diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index fe61af021d7f..db30773f5155 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -1,6 +1,7 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; @@ -21,6 +22,8 @@ const startTimer = () => { function BaseSidebarScreen() { const styles = useThemeStyles(); const activeWorkspaceID = getPolicyIDFromNavigationState(); + const {translate} = useLocalize(); + useEffect(() => { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); Timing.start(CONST.TIMING.SIDEBAR_LOADED, true); @@ -36,7 +39,10 @@ function BaseSidebarScreen() { > {({insets}) => ( <> - + { return isFocused || (topmostCentralPane?.name === SCREENS.SEARCH.CENTRAL_PANE && isSmallScreenWidth); }; -type PolicySelector = Pick; +type PolicySelector = Pick; type FloatingActionButtonAndPopoverOnyxProps = { /** The list of policies the user has access to. */ @@ -51,11 +56,20 @@ type FloatingActionButtonAndPopoverOnyxProps = { /** Information on the last taken action to display as Quick Action */ quickAction: OnyxEntry; + /** The report data of the quick action */ + quickActionReport: OnyxEntry; + + /** The policy data of the quick action */ + quickActionPolicy: OnyxEntry; + /** The current session */ session: OnyxEntry; /** Personal details of all the users */ personalDetails: OnyxEntry; + + /** Has user seen track expense training interstitial */ + hasSeenTrackTraining: OnyxEntry; }; type FloatingActionButtonAndPopoverProps = FloatingActionButtonAndPopoverOnyxProps & { @@ -74,28 +88,35 @@ const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { type: policy.type, role: policy.role, + id: policy.id, isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, pendingAction: policy.pendingAction, - avatar: policy.avatar, + avatarURL: policy.avatarURL, name: policy.name, }) as PolicySelector; const getQuickActionIcon = (action: QuickActionName): React.FC => { switch (action) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: - return Expensicons.MoneyCircle; + return getIconForAction(CONST.IOU.TYPE.REQUEST); case CONST.QUICK_ACTIONS.REQUEST_SCAN: - return Expensicons.Receipt; + return Expensicons.ReceiptScan; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: return Expensicons.Car; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: case CONST.QUICK_ACTIONS.SPLIT_SCAN: case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: - return Expensicons.Transfer; + return getIconForAction(CONST.IOU.TYPE.SPLIT); case CONST.QUICK_ACTIONS.SEND_MONEY: - return Expensicons.Send; + return getIconForAction(CONST.IOU.TYPE.SEND); case CONST.QUICK_ACTIONS.ASSIGN_TASK: return Expensicons.Task; + case CONST.QUICK_ACTIONS.TRACK_DISTANCE: + return Expensicons.Car; + case CONST.QUICK_ACTIONS.TRACK_MANUAL: + return getIconForAction(CONST.IOU.TYPE.TRACK); + case CONST.QUICK_ACTIONS.TRACK_SCAN: + return Expensicons.ReceiptScan; default: return Expensicons.MoneyCircle; } @@ -135,7 +156,18 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { * FAB that can open or close the menu. */ function FloatingActionButtonAndPopover( - {onHideCreateMenu, onShowCreateMenu, isLoading = false, allPolicies, quickAction, session, personalDetails}: FloatingActionButtonAndPopoverProps, + { + onHideCreateMenu, + onShowCreateMenu, + isLoading = false, + allPolicies, + quickAction, + quickActionReport, + quickActionPolicy, + session, + personalDetails, + hasSeenTrackTraining, + }: FloatingActionButtonAndPopoverProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -146,10 +178,10 @@ function FloatingActionButtonAndPopover( const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); const prevIsFocused = usePrevious(isFocused); + const {isOffline} = useNetwork(); - const quickActionReport: OnyxEntry = useMemo(() => (quickAction ? ReportUtils.getReport(quickAction.chatReportID) : null), [quickAction]); - - const quickActionPolicy = allPolicies ? allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`] : undefined; + const {canUseSpotnanaTravel} = usePermissions(); + const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection), [allPolicies]); const quickActionAvatars = useMemo(() => { if (quickActionReport) { @@ -193,13 +225,13 @@ function FloatingActionButtonAndPopover( Task.clearOutTaskInfoAndNavigate(quickAction?.chatReportID ?? '', quickActionReport, quickAction.targetAccountID ?? 0, true); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL); + IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); break; case CONST.QUICK_ACTIONS.TRACK_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN); + IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN, true); break; case CONST.QUICK_ACTIONS.TRACK_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE); + IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); break; default: } @@ -290,9 +322,9 @@ function FloatingActionButtonAndPopover( ...(canUseTrackExpense && selfDMReportID ? [ { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), - onSelected: () => + onSelected: () => { interceptAnonymousUser(() => IOU.startMoneyRequest( CONST.IOU.TYPE.TRACK, @@ -301,12 +333,18 @@ function FloatingActionButtonAndPopover( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), - ), + ); + if (!hasSeenTrackTraining && !isOffline) { + setTimeout(() => { + Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); + }, CONST.ANIMATED_TRANSITION); + } + }, }, ] : []), { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => interceptAnonymousUser(() => @@ -332,7 +370,7 @@ function FloatingActionButtonAndPopover( ), }, { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {}), onSelected: () => interceptAnonymousUser(() => @@ -344,11 +382,37 @@ function FloatingActionButtonAndPopover( ), ), }, + ...(canSendInvoice + ? [ + { + icon: Expensicons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest( + CONST.IOU.TYPE.INVOICE, + // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + ReportUtils.generateReportID(), + ), + ), + }, + ] + : []), { icon: Expensicons.Task, text: translate('newTaskPage.assignTask'), onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), }, + ...(canUseSpotnanaTravel + ? [ + { + icon: Expensicons.Suitcase, + text: translate('travel.bookTravel'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)), + }, + ] + : []), ...(!isLoading && !Policy.hasActiveChatEnabledPolicies(allPolicies) ? [ { @@ -407,12 +471,21 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, + }, + quickActionPolicy: { + key: ({quickActionReport}) => `${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`, + }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, session: { key: ONYXKEYS.SESSION, }, + hasSeenTrackTraining: { + key: ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING, + }, })(forwardRef(FloatingActionButtonAndPopover)); export type {PolicySelector}; diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 8f016ec0d8d9..18b290a81ea4 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -46,6 +46,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { const {transactionID, reportID, backTo} = route.params; const report = ReportUtils.getReport(reportID); + + // We first check if the report is part of a policy - if not, then it's a personal request (1:1 request) + // For personal requests, we need to allow both users to put the request on hold + const isWorkspaceRequest = ReportUtils.isReportInGroupPolicy(report); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); const navigateBack = () => { @@ -53,7 +57,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { }; const onSubmit = (values: FormOnyxValues) => { - if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount. + // But, we still want the requestee to be able to put the request on hold + if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { return; } @@ -68,7 +75,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { if (!values.comment) { errors.comment = 'common.error.fieldRequired'; } - if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount. + // But, we still want the requestee to be able to put the request on hold + if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { const formErrors = {}; ErrorUtils.addErrorMessage(formErrors, 'reportModified', 'common.error.requestModified'); FormActions.setErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM, formErrors); @@ -76,7 +86,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { return errors; }, - [parentReportAction], + [parentReportAction, isWorkspaceRequest], ); useEffect(() => { diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx similarity index 80% rename from src/pages/iou/steps/MoneyRequestAmountForm.tsx rename to src/pages/iou/MoneyRequestAmountForm.tsx index 466619f71e26..46bd34006550 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -97,7 +97,6 @@ function MoneyRequestAmountForm( const textInput = useRef(null); const moneyRequestAmountInput = useRef(null); - const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); const [formError, setFormError] = useState(''); const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); @@ -205,6 +204,8 @@ function MoneyRequestAmountForm( */ const submitAndNavigateToNextPage = useCallback( (iouPaymentType?: PaymentMethodType | undefined) => { + const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); + // Skip the check for tax amount form as 0 is a valid input const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { @@ -224,7 +225,7 @@ function MoneyRequestAmountForm( onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType}); }, - [taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], + [taxAmount, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], ); const buttonText: string = useMemo(() => { @@ -232,10 +233,22 @@ function MoneyRequestAmountForm( if (skipConfirmation) { if (currentAmount !== '') { const currencyAmount = CurrencyUtils.convertToDisplayString(CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)), currency) ?? ''; - const text = iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.splitAmount', {amount: currencyAmount}) : translate('iou.submitAmount', {amount: currencyAmount}); + let text = translate('iou.submitAmount', {amount: currencyAmount}); + if (iouType === CONST.IOU.TYPE.SPLIT) { + text = translate('iou.splitAmount', {amount: currencyAmount}); + } else if (iouType === CONST.IOU.TYPE.TRACK) { + text = translate('iou.trackAmount', {amount: currencyAmount}); + } return text[0].toUpperCase() + text.slice(1); } - return iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.splitExpense') : translate('iou.submitExpense'); + + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.splitExpense'); + } + if (iouType === CONST.IOU.TYPE.TRACK) { + return translate('iou.trackExpense'); + } + return translate('iou.submitExpense'); } return isEditing ? translate('common.save') : translate('common.next'); }, [skipConfirmation, iouType, currency, isEditing, translate]); @@ -298,41 +311,43 @@ function MoneyRequestAmountForm( longPressHandlerStateChanged={updateLongPressHandlerState} /> ) : null} - {iouType === CONST.IOU.TYPE.PAY && skipConfirmation ? ( - - ) : ( -