diff --git a/.eslintrc.js b/.eslintrc.js index 809576f3de76..c1b6a9676052 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -261,13 +261,7 @@ module.exports = { 'no-restricted-imports': [ 'error', { - paths: [ - ...restrictedImportPaths, - { - name: 'underscore', - message: 'Please use the corresponding method from lodash instead', - }, - ], + paths: restrictedImportPaths, patterns: restrictedImportPatterns, }, ], 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/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index 2544fe05cddd..57a941105f90 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -14,6 +14,7 @@ const run = () => { // Initialize string to store Graphite metrics let graphiteString = ''; + let timestamp: number; // Iterate over each entry regressionEntries.forEach((entry) => { @@ -26,7 +27,9 @@ const run = () => { const current = JSON.parse(entry); // Extract timestamp, Graphite accepts timestamp in seconds - const timestamp = current.metadata?.creationDate ? Math.floor(new Date(current.metadata.creationDate).getTime() / 1000) : ''; + if (current.metadata?.creationDate) { + timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000); + } if (current.name && current.meanDuration && current.meanCount && timestamp) { const formattedName = current.name.split(' ').join('-'); diff --git a/.github/actions/javascript/getGraphiteString/index.js b/.github/actions/javascript/getGraphiteString/index.js index 7f512d575a23..0e042924d92a 100644 --- a/.github/actions/javascript/getGraphiteString/index.js +++ b/.github/actions/javascript/getGraphiteString/index.js @@ -2735,6 +2735,7 @@ const run = () => { const regressionEntries = regressionFile.split('\n'); // Initialize string to store Graphite metrics let graphiteString = ''; + let timestamp; // Iterate over each entry regressionEntries.forEach((entry) => { // Skip empty lines @@ -2744,7 +2745,9 @@ const run = () => { try { const current = JSON.parse(entry); // Extract timestamp, Graphite accepts timestamp in seconds - const timestamp = current.metadata?.creationDate ? Math.floor(new Date(current.metadata.creationDate).getTime() / 1000) : ''; + if (current.metadata?.creationDate) { + timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000); + } if (current.name && current.meanDuration && current.meanCount && timestamp) { const formattedName = current.name.split(' ').join('-'); const renderDurationString = `${GRAPHITE_PATH}.${formattedName}.renderDuration ${current.meanDuration} ${timestamp}`; 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/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts new file mode 100644 index 000000000000..5aa0d1daf342 --- /dev/null +++ b/.github/scripts/detectRedirectCycle.ts @@ -0,0 +1,64 @@ +import {parse} from 'csv-parse'; +import fs from 'fs'; + +const parser = parse(); + +const adjacencyList: Record = {}; +const visited: Map = new Map(); +const backEdges: Map = new Map(); + +function addEdge(source: string, target: string) { + if (!adjacencyList[source]) { + adjacencyList[source] = []; + } + adjacencyList[source].push(target); +} + +function isCyclic(currentNode: string): boolean { + visited.set(currentNode, true); + backEdges.set(currentNode, true); + + // Do a depth first search for all the neighbours. If a node is found in backedge, a cycle is detected. + const neighbours = adjacencyList[currentNode]; + if (neighbours) { + for (const node of neighbours) { + if (!visited.has(node)) { + if (isCyclic(node)) { + return true; + } + } else if (backEdges.has(node)) { + return true; + } + } + } + + backEdges.delete(currentNode); + + return false; +} + +function detectCycle(): boolean { + for (const [node] of Object.entries(adjacencyList)) { + if (!visited.has(node)) { + if (isCyclic(node)) { + const cycle = Array.from(backEdges.keys()); + console.log(`Infinite redirect found in the cycle: ${cycle.join(' -> ')} -> ${node}`); + return true; + } + } + } + return false; +} + +fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) + .pipe(parser) + .on('data', (row) => { + // Create a directed graph of sourceURL -> targetURL + addEdge(row[0], row[1]); + }) + .on('end', () => { + if (detectCycle()) { + process.exit(1); + } + process.exit(0); + }); diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh old mode 100644 new mode 100755 index 737d9bffacf9..b8942cd5b23d --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -5,11 +5,22 @@ declare -r REDIRECTS_FILE="docs/redirects.csv" +declare -r RED='\033[0;31m' +declare -r GREEN='\033[0;32m' +declare -r NC='\033[0m' + duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE) +if [[ -n "$duplicates" ]]; then + echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}" + exit 1 +fi -if [[ -z "$duplicates" ]]; then - exit 0 +npm run detectRedirectCycle +DETECT_CYCLE_EXIT_CODE=$? +if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then + echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}" + exit 1 fi -echo "duplicate redirects are not allowed: $duplicates" -exit 1 +echo -e "${GREEN}The redirects.csv is valid!${NC}" +exit 0 diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index 699bd379fb77..cda33d39102e 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -26,6 +26,8 @@ concurrency: jobs: build: + env: + IS_PR_FROM_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest steps: - name: Checkout @@ -36,8 +38,8 @@ jobs: - name: Create docs routes file run: ./.github/scripts/createDocsRoutes.sh - - - name: Check duplicates in redirect.csv + + - name: Check for duplicates and cycles in redirects.csv run: ./.github/scripts/verifyRedirect.sh - name: Build with Jekyll @@ -49,7 +51,7 @@ jobs: - name: Deploy to Cloudflare Pages uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca id: deploy - if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) + if: env.IS_PR_FROM_FORK != 'true' with: apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -57,16 +59,18 @@ jobs: directory: ./docs/_site - name: Setup Cloudflare CLI + if: env.IS_PR_FROM_FORK != 'true' run: pip3 install cloudflare==2.19.0 - name: Purge Cloudflare cache + if: env.IS_PR_FROM_FORK != 'true' run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["help.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache env: CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} - name: Leave a comment on the PR uses: actions-cool/maintain-one-comment@de04bd2a3750d86b324829a3ff34d47e48e16f4b - if: ${{ github.event_name == 'pull_request' }} + if: ${{ github.event_name == 'pull_request' && env.IS_PR_FROM_FORK != 'true' }} with: token: ${{ secrets.OS_BOTIFY_TOKEN }} body: ${{ format('A preview of your ExpensifyHelp changes have been deployed to {0} ⚡️', steps.deploy.outputs.alias) }} 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/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index f20939f9df0a..88d4d24a5723 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -30,7 +30,7 @@ jobs: # - git diff is used to see the files that were added on this branch # - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main # - wc counts the words in the result of the intersection - count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'web/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) + count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'desktop/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' 'workflow_tests/*.js' '.github/libs/*.js' '.github/scripts/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) if [ "$count_new_js" -gt "0" ]; then echo "ERROR: Found new JavaScript files in the project; use TypeScript instead." exit 1 diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index a2c7365f7de8..394e45f8d9ae 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -52,6 +52,10 @@ "/": "/split/*", "comment": "Split Expense" }, + { + "/": "/submit/*", + "comment": "Submit Expense" + }, { "/": "/request/*", "comment": "Submit Expense" @@ -76,6 +80,10 @@ "/": "/search/*", "comment": "Search" }, + { + "/": "/pay/*", + "comment": "Pay someone" + }, { "/": "/send/*", "comment": "Pay someone" diff --git a/README.md b/README.md index 8adadfc9cbe6..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) @@ -60,6 +60,7 @@ If you're using another operating system, you will need to ensure `mkcert` is in For an M1 Mac, read this [SO](https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1) for installing cocoapods. * If you haven't already, install Xcode tools and make sure to install the optional "iOS Platform" package as well. This installation may take awhile. + * After installation, check in System Settings that there's no update for Xcode. Otherwise, you may encounter issues later that don't explain that you solve them by updating Xcode. * Install project gems, including cocoapods, using bundler to ensure everyone uses the same versions. In the project root, run: `bundle install` * If you get the error `Could not find 'bundler'`, install the bundler gem first: `gem install bundler` and try again. * If you are using MacOS and get the error `Gem::FilePermissionError` when trying to install the bundler gem, you're likely using system Ruby, which requires administrator permission to modify. To get around this, install another version of Ruby with a version manager like [rbenv](https://github.com/rbenv/rbenv#installation). diff --git a/android/app/build.gradle b/android/app/build.gradle index b8350a8a0111..d5afad11a9fa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046307 - versionName "1.4.63-7" + versionCode 1001046700 + versionName "1.4.67-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 84364f2ef7ff..520602a28a02 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -62,6 +62,7 @@ + @@ -70,6 +71,7 @@ + @@ -81,6 +83,7 @@ + @@ -89,6 +92,7 @@ + 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/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/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/copilots-and-delegates/Assign-or-remove-a-Copilot.md b/docs/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot.md new file mode 100644 index 000000000000..5df171a93a72 --- /dev/null +++ b/docs/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot.md @@ -0,0 +1,38 @@ +--- +title: Assign or remove a Copilot +description: Safely delegate tasks without sharing login information. +--- + +You can safely delegate tasks to someone else without sharing your login information by assigning them as your Copilot. Your copilot can access your Expensify account through their own account to: +* Prepare expenses on your behalf +* Approve and reimburse expense reports on your behalf (Full Access Copilots only) +* View and make changes to your account, domain, and workspace settings +* View all expenses visible from your account + +# Assign a Copilot + +1. Hover over Settings and click **Account**. +2. Under Account Details, scroll down to the Copilot: Delegated Access section. +3. Enter the email address or phone number for the person you want to assign as your Copilot. +4. Select whether you want to give your Copilot Full or Submit Only access. + * **Full Access**: Your Copilot will have full access to your account. Nearly every action you can do and everything you can see in your account will also be available to your Copilot. However, Copilots do not have the ability to add or remove other Copilots from your account. + * **Submit Only Access**: Your Copilot will have the same access and limitations as a Full Access Copilot, but they will not be able to approve reports on your behalf—they can only submit them. +5. Click **Invite Copilot**. + +If your Copilot already has an Expensify account, they will get an email notifying them that they can now also access your account from within their own. If they do not have an Expensify account, they will get an email with a link to create one. Once created, they will be able to access your account from within their own. + +# Remove a Copilot + +{% include info.html %} +This action must be completed by the account owner. Copilots cannot remove other Copilots from an account. +{% include end-info.html %} + +1. Hover over Settings and click **Account**. +2. Under Account Details, scroll down to the Copilot: Delegated Access section. +3. Click the red X next to the copilot to remove them. + +# FAQs + +**Can I only have one Copilot?** + +You can assign as many Copilots as you like—there is no limit. 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/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-report-approvers-to-specific-employees.md b/docs/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees.md new file mode 100644 index 000000000000..5f79ea11378c --- /dev/null +++ b/docs/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees.md @@ -0,0 +1,25 @@ +--- +title: Assign report approvers to specific employees +description: Create approval hierarchies for reports +--- +
+ +{% include info.html %} +To assign different approvers for different employees, your workspace must use Advanced Approvals as the report approval workflow. +{% include end-info.html %} + +Rather than having one approver for all members of the workspace, you can use the Advanced Approvals workflow to assign different report approvers to specific employees. + +To assign a report approver to a specific member of your workspace, +1. Hover over Settings, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Members** tab on the left. +4. Click **Settings** next to the desired member. +5. Click the “Approves to” dropdown and select the desired approver for the member’s reports. +6. Click **Save**. + +You can also set +- Over-limit approval rules that require a secondary approver when a specific member’s report expenses exceed a set limit. +- Approvers for expenses under a specific tag or category. + +
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/Create-a-report-approval-workflow.md b/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md new file mode 100644 index 000000000000..015bbe2e8532 --- /dev/null +++ b/docs/articles/expensify-classic/reports/Create-a-report-approval-workflow.md @@ -0,0 +1,25 @@ +--- +title: Create a report approval workflow +description: Set up an approval workflow automation for employee reports +--- +
+ +Expensify allows Workspace Admins to create workflows and automations that determine how expense reports are approved for the workspace. You can choose from three different workflows that either: +- Allow all submitted expenses to be automatically approved (if they don’t have any violations). +- Assign one approver for all reports under the workspace. +- Set up multi-level approvals for more complex workflows. + +# Set approval workflow + +1. Hover over Settings, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Members** tab on the left. +4. Scroll down to the Approval Mode section. +5. Select an approval mode. + - **Submit and Close**: No approval is required. Once a report is submitted, it will be automatically approved and closed. This option may be useful if your expense approvals occur in another system or if the submitter and approver are the same person. + - **Submit and Approve**: All reports go to one person that you assign as the approver. Once a report is submitted, it is sent to the approver. This is the default option. + - **Advanced Approval**: Allows for more complex workflows, like assigning different approvers for different employees or requiring secondary approvals for expenses that exceed a set limit. + +To add to your approval workflow, you can also set up approval rules for specific categories and tags. + +
diff --git a/docs/articles/expensify-classic/reports/Require-review-for-over-limit-expenses.md b/docs/articles/expensify-classic/reports/Require-review-for-over-limit-expenses.md new file mode 100644 index 000000000000..27d8dbf4b6a2 --- /dev/null +++ b/docs/articles/expensify-classic/reports/Require-review-for-over-limit-expenses.md @@ -0,0 +1,46 @@ +--- +title: Require review for over-limit expenses +description: Require a manual review for expenses that exceed a set amount +--- +
+ +You can set rules that require a manual review for expenses that exceed a specific amount. These rules can be set for all expenses under a workspace and/or for a specific member of your workspace. + +{% include info.html %} +These rules do not prohibit purchases over this limit amount. They only ensure that expenses over the limit require a manual review. +{% include end-info.html %} + +# Set a manual approval rule for over-limit expenses + +To set approval limits for expenses submitted to a workspace, +1. Hover over Settings, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Members** tab on the left. +4. Scroll down to the Approval Mode section to where it says Expense Approvals. +5. In the “Manually approve all expenses over:” field, enter the expense limit amount. + +Any expenses that exceed the set limit will now require a manual review, even if the approval workflow does not require manual approval. + +# Set an over-limit approver for a member + +When over-limit approvals are set for a specific member, a secondary approver will be required when the member submits a report that contains expenses exceeding the limit amount. If the member is an approver for other members’ reports, the approval limit applies to those reports as well. + +For example, if you want to allow a project manager to review expenses under $500 but have a department head review expenses over $500, you can assign the department head as the project manager’s over-limit approver. + +{% include info.html %} +To set expense limits for specific workspace members, your workspace must use Advanced Approvals as the report approval workflow. +{% include end-info.html %} + +To set an over-limit approver for a specific member of your workspace, +1. Hover over Settings, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Members** tab on the left. +4. Click **Settings** next to the desired member. +5. In the “If report total is over” field, enter the amount that will require this member’s reports to need a secondary review. This limit also applies to reports that the member is in charge of reviewing. +6. Click the “Then approves to” dropdown and select the secondary approver. +7. Click **Save**. + +
+ + + diff --git a/docs/articles/expensify-classic/reports/Set-a-random-report-audit-schedule.md b/docs/articles/expensify-classic/reports/Set-a-random-report-audit-schedule.md new file mode 100644 index 000000000000..198bd8d78cea --- /dev/null +++ b/docs/articles/expensify-classic/reports/Set-a-random-report-audit-schedule.md @@ -0,0 +1,21 @@ +--- +title: Set a random report audit schedule +description: Randomly audit a percentage of compliant reports +--- +
+ +Expensify automatically flags reports that contain inaccurate or non-compliant expenses for review. However, you can also choose to randomly audit a percentage of compliant reports. + +To set a random audit schedule, +1. Hover over Settings, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Members** tab on the left. +4. Scroll down to the Expense Approvals heading under the Approval Modes section. +5. In the “Randomly route reports for manual approval” field, enter the percentage of reports that you want to be randomly audited. The default is set at 5% (or 1 in 20 reports). +6. Click **Save**. + +
+ + + + diff --git a/docs/articles/expensify-classic/reports/Track-report-history.md b/docs/articles/expensify-classic/reports/Track-report-history.md new file mode 100644 index 000000000000..8e7e86850f2c --- /dev/null +++ b/docs/articles/expensify-classic/reports/Track-report-history.md @@ -0,0 +1,20 @@ +--- +title: Track report history +description: See the comments and history on a report +--- +
+ +All changes and comments that have been made on a report are tracked at the bottom of the report. + +1. Click the **Reports** tab. +2. Open a report. +3. Scroll to the bottom of the report to review the report’s history and comments. + +Additionally, an email notification is sent to the employee when impactful changes are made to the report. For example, if the reimbursable status of an expense is changed, if an expense is approved or denied, or if a comment is added to the report. + +
+ + + + + diff --git a/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md b/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md new file mode 100644 index 000000000000..7c3d8077c14d --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md @@ -0,0 +1,111 @@ +--- +title: Enable and set up expense violations +description: Set up rules for expenses and enable violations +--- +
+ +Expensify automatically detects expense errors or discrepancies as violations that must be corrected. You can also set rules for a workspace that will trigger a violation if the rule is not met. These rules can be set for categories, tags, and even for specific domain groups. + +When reviewing submitted expense reports, approvers will see violations highlighted with an exclamation mark. There are two types of violations: +- **Yellow**: Automated highlights that require attention but may not require corrective action. For example, if a receipt was SmartScanned and then the amount was modified, a yellow violation will be added to call out the change for review. +- **Red**: Violations directly tied to your workspace settings. These violations must be addressed before the report can be submitted and reimbursed. + +You can hover over the icon to see a brief description, and you can find more detailed information below the list of expenses. + +{% include info.html %} +If your workspace has automations set to automatically submit reports for approval, the report that contains violations will not be submitted automatically until the violations are corrected. (However, if a comment is added to an expense, it will override the violation as the member is providing a reason for submission *unless* domain workspace rules are set to be strictly enforced, as detailed near the bottom of this article.) +{% include end-info.html %} + +# Enable or disable expense violations + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Expenses** tab on the left. +5. Click the “Enable violations” toggle. +6. If desired, enter the expense rules that will be used to create violations: + - **Max expense age (days)**: How old an expense can be + - **Max expense amount**: How much a single expense can cost + - **Receipt required amount**: How much a single expense can cost before a receipt is required + +{% include info.html %} +Expensify includes certain system mandatory violations that can't be disabled, even if your policy has violations turned off. +{% include end-info.html %} + +# Set category rules + +Admins on a Control workspace can enable specific rules for each category, including setting expense caps for specific categories, requiring receipts, and more. These rules can allow you to have a default expense limit of $2,500 but to only allow a daily entertainment limit of $150 per person. You can also choose to not require receipts for mileage or per diem expenses. + +To set up category rules, +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. Click **Edit** to the right of the category. +6. Enter your category rules, as desired: + - **GL Code and Payroll Code**: You can add general ledger (GL) or payroll codes to the category for accounting. GL codes populate automatically if you have an accounting integration connected with Expensify. + - **Max Amount**: You can set specific expense caps for the expense category. Use the Limit Type dropdown to determine if the amount is set per individual expense or per day, then enter the maximum amount into this field. + - **Receipts**: You can determine whether receipts are required for the category. For example, many companies disable receipt requirements for toll expenses. + - **Description**: You can determine whether a description is required for expenses under this category. + - **Description Hint**: You can add a hint in the description field to prompt the expense creator on what they should enter into the description field for expenses under this category. + - **Approver**: You can set a specific approver for expenses labeled with this category. + +If users are in violation of these rules, the violations will be shown in red on the report. + +{% include info.html %} +If Scheduled Submit is enabled on a workspace, expenses with category violations will not be auto-submitted unless the expense has a comment added. +{% include end-info.html %} + +# Make categories required + +This means all expenses must be coded with a Category. + +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. Enable the “People must categorize expenses” toggle. + +Each Workspace Member will now be required to select a category for their expense. If they do not select a category, the report will receive a violation, which can prevent submission if Scheduled Submit is enabled. + +# Make tags required + +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. Enable the “People must tag expenses” toggle. + +Each Workspace Member will now be required to select a tag for their expense before they’re able to submit it. + +# Require strict compliance by domain group + +You can require strict compliance to require members of a specific domain group to submit reports that meet **all** workspace rules before they can submit their expense report—even if they add a note. Every rule and regulation on the workspace must be met before a report can be submitted. + +{% include info.html %} +This will prevent members from submitting any reports where a manager has granted them a manual exception for any of the workspace rules. +{% include end-info.html %} + +To enable strict domain group compliance for reports, + +1. Hover over Settings, then click **Domains**. +2. Click the **Groups** tab on the left. +3. Click **Edit** to the right of the desired workspace name. +4. Enable the “Strictly enforce expense workspace rules” toggle. + +# FAQs + +**Why can’t my employees see the categories on their expenses?** + +The employee may have their default workspace set as their personal workspace. Look under the details section on top right of the report to ensure it is being reported under the correct workspace. + +**Will the account numbers from our accounting system (QuickBooks Online, Sage Intacct, etc.) show in the category list for employees?** + +The general ledger (GL) account numbers are visible only for Workspace Admins in the workspace settings when they are part of a control workspace. This information is not visible to other members of the workspace. However, if you wish to have this information available to your employees when they are categorizing their expenses, you can edit the account name in your accounting software to include the GL number (for example, Accounts Payable - 12345). + +**What causes a category violation?** + +- An expense is categorized with a category that is not included in the workspace's categories. This may happen if the employee creates an expense under the wrong workspace, which will cause a "category out of workspace" violation. +- If the workspace categories are being imported from an accounting integration and they’ve been updated in the accounting system but not in Expensify, this can cause an old category to still be in use on an open report which would throw a violation on submission. Simply reselect a proper category to clear violation. + +
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/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-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/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/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..edef142a80bf --- /dev/null +++ b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md @@ -0,0 +1,49 @@ +--- +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. +- > 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/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/redirects.csv b/docs/redirects.csv index af595ecc5f83..124b0377584c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -87,6 +87,7 @@ https://help.expensify.com/articles/new-expensify/payments/Request-Money,https:/ https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/tax-tracking,https://help.expensify.com/articles/expensify-classic/expenses/expenses/Apply-Tax https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/User-Roles.html,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/ https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Owner,https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account +https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Owner.html,https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.html,https://help.expensify.com/articles/expensify-classic/spending-insights/Other-Export-Options https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Global-Reimbursements https://help.expensify.com/expensify-classic/hubs/bank-accounts-and-credit-cards,https://help.expensify.com/expensify-classic/hubs/ @@ -123,7 +124,6 @@ https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/ https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt,https://help.expensify.com/articles/expensify-classic/expensify-billing/Tax-Exempt https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approving-Reports,https://help.expensify.com/expensify-classic/hubs/reports/ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members,https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles -https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Attendee-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules @@ -156,5 +156,14 @@ 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 diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 8b911fa849cd..b224296ed75a 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -6,7 +6,7 @@ layout: {% assign pages = site.html_pages | where_exp:'doc','doc.sitemap != false' | where_exp:'doc','doc.url != "/404.html"' %} {% for page in pages %} - {{ page.url | replace:'/index.html','/' | absolute_url | xml_escape }} + {{ page.url | replace:'/index.html','/' | absolute_url | xml_escape | replace:'.html','' }} {% endfor %} \ No newline at end of file diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index c9b8286cf50f..440309f63c6e 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index 18fbfec9390f..2de81ee85018 100644 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 7f50db5da85a..54486d5bf162 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -963,7 +963,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", @@ -1002,7 +1002,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", @@ -1085,7 +1085,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", @@ -1170,7 +1170,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", @@ -1256,7 +1256,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", @@ -1336,7 +1336,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", @@ -1414,7 +1414,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", @@ -1493,7 +1493,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", @@ -1755,7 +1755,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", @@ -1900,7 +1900,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", @@ -2038,7 +2038,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", @@ -2173,7 +2173,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", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4523bf1c4418..4d2e7ba3b992 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.63 + 1.4.67 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,12 @@ CFBundleVersion - 1.4.63.7 + 1.4.67.0 + FullStory + + OrgId + o-1WN56P-na1 + ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d40d0fa27486..952fbeddd75e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.63 + 1.4.67 CFBundleSignature ???? CFBundleVersion - 1.4.63.7 + 1.4.67.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index d472daa53ab7..d6920d746496 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.63 + 1.4.67 CFBundleVersion - 1.4.63.7 + 1.4.67.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f564bfd931e4..0398bd3b1324 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1380,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 @@ -1697,25 +1712,6 @@ PODS: - React-perflogger (= 0.73.4) - RNAppleAuthentication (2.2.2): - React-Core - - RNCAsyncStorage (1.21.0): - - 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 - RNCClipboard (1.13.2): - glog - hermes-engine @@ -1835,7 +1831,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.62): + - RNLiveMarkdown (0.1.64): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1853,9 +1849,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.62) + - RNLiveMarkdown/common (= 0.1.64) - Yoga - - RNLiveMarkdown/common (0.1.62): + - RNLiveMarkdown/common (0.1.64): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2154,7 +2150,6 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNAppleAuthentication (from `../node_modules/@invertase/react-native-apple-authentication`)" - - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) @@ -2385,8 +2380,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" RNAppleAuthentication: :path: "../node_modules/@invertase/react-native-apple-authentication" - RNCAsyncStorage: - :path: "../node_modules/@react-native-async-storage/async-storage" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" RNCPicker: @@ -2521,7 +2514,7 @@ SPEC CHECKSUMS: react-native-pdf: cd256a00b9d65cb1008dcca2792d7bfb8874838d react-native-performance: 1aa5960d005159f4ab20be15b44714b53b44e075 react-native-plaid-link-sdk: 93870f8cd1de8e0acca5cb5020188bdc94e15db6 - react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 + react-native-quick-sqlite: f7b9f578b8b3b608dc742240b0103faae5b61f63 react-native-release-profiler: 42fc8e09b4f6f9b7d14cc5a15c72165e871c0918 react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: e8bdd57d9f8d34cc336f0ee6acb30712a8454446 @@ -2551,7 +2544,6 @@ SPEC CHECKSUMS: React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 - RNCAsyncStorage: 559f22cc4b582414e783fd7255974b29e24b451c RNCClipboard: c73bbc2e9012120161f1012578418827983bfd0c RNCPicker: c77efa39690952647b83d8085520bf50ebf94ecb RNDeviceInfo: cbf78fdb515ae73e641ee7c6b474f77a0299e7e6 @@ -2564,7 +2556,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 47dfb50244f9ba1caefbc0efc6404ba41bf6620a + RNLiveMarkdown: ddc8b2d827febd397c88137ffc7a6e102d511b8b RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 @@ -2580,8 +2572,8 @@ SPEC CHECKSUMS: SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 - VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + VisionCamera: 8c5c9c50b3d76018782a823cee2f0b8b628c8604 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d diff --git a/package-lock.json b/package-lock.json index 52a975e3a83e..47737d0223d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.63-7", + "version": "1.4.67-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.63-7", + "version": "1.4.67-0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.62", + "@expensify/react-native-live-markdown": "0.1.64", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -25,7 +25,6 @@ "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", "@onfido/react-native-sdk": "10.6.0", - "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/geolocation": "3.2.1", @@ -109,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", @@ -132,8 +131,7 @@ "react-webcam": "^7.1.1", "react-window": "^1.8.9", "semver": "^7.5.2", - "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1" + "shim-keyboard-event-key": "^1.0.3" }, "devDependencies": { "@actions/core": "1.10.0", @@ -182,7 +180,6 @@ "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", - "@types/underscore": "^1.11.5", "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", @@ -201,6 +198,7 @@ "concurrently": "^8.2.2", "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", + "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "^29.2.0", @@ -243,8 +241,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 +3568,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.64", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.64.tgz", + "integrity": "sha512-X6NXYH420wC+BFNOuzJflpegwSKTiuzLvbDeehCpxrtS059Eyb2FbwkzrAVH7TGwDeghFgaQfY9rVkSCGUAbsw==", "engines": { "node": ">= 18.0.0" }, @@ -7094,9 +7092,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", @@ -7671,16 +7670,6 @@ } } }, - "node_modules/@react-native-async-storage/async-storage": { - "version": "1.21.0", - "license": "MIT", - "dependencies": { - "merge-options": "^3.0.4" - }, - "peerDependencies": { - "react-native": "^0.0.0-0 || >=0.60 <1.0" - } - }, "node_modules/@react-native-camera-roll/camera-roll": { "version": "7.4.0", "license": "MIT", @@ -12186,9 +12175,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": "*" } @@ -12225,9 +12215,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": "*" @@ -12295,22 +12286,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": { @@ -12370,6 +12364,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, @@ -12481,8 +12480,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", @@ -12501,6 +12501,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", @@ -12627,9 +12636,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", @@ -12639,20 +12649,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": { @@ -12661,9 +12683,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": "*" } @@ -12676,11 +12699,6 @@ "version": "4.0.2", "license": "MIT" }, - "node_modules/@types/underscore": { - "version": "1.11.5", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", @@ -12722,9 +12740,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": "*" } @@ -13566,31 +13585,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": { @@ -15548,21 +15578,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" @@ -15978,6 +16002,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", @@ -17740,6 +17779,12 @@ "version": "3.1.1", "license": "MIT" }, + "node_modules/csv-parse": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.5.tgz", + "integrity": "sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ==", + "dev": true + }, "node_modules/dag-map": { "version": "1.0.2", "license": "MIT" @@ -17885,6 +17930,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", @@ -17900,6 +17961,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, @@ -18316,15 +18389,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" }, @@ -23061,11 +23130,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": { @@ -23395,6 +23465,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", @@ -23464,6 +23567,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", @@ -23514,13 +23629,6 @@ "node": ">=6" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "license": "MIT", @@ -26766,6 +26874,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", @@ -27688,16 +27806,6 @@ "version": "1.0.1", "license": "MIT" }, - "node_modules/merge-options": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge-refs": { "version": "1.2.1", "license": "MIT", @@ -28429,8 +28537,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" @@ -29505,15 +29614,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": { @@ -31460,7 +31574,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": "*", @@ -32681,14 +32797,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": { @@ -33078,8 +33195,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" } @@ -33144,6 +33262,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, @@ -33326,10 +33456,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": { @@ -35338,8 +35470,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", @@ -36961,43 +37094,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 }, @@ -37012,11 +37144,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": { @@ -37097,54 +37230,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 } @@ -37152,8 +37290,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" }, @@ -37161,10 +37300,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", @@ -37174,21 +37357,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", @@ -37204,25 +37452,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 1cef7d94fcba..b01a03dc109f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.63-7", + "version": "1.4.67-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -28,6 +28,7 @@ "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.ts", + "detectRedirectCycle": "ts-node .github/scripts/detectRedirectCycle.ts", "desktop-build-adhoc": "scripts/build-desktop.sh adhoc", "ios-build": "fastlane ios build", "android-build": "fastlane android build", @@ -64,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.62", + "@expensify/react-native-live-markdown": "0.1.64", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -76,7 +77,6 @@ "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", "@onfido/react-native-sdk": "10.6.0", - "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", "@react-native-community/geolocation": "3.2.1", @@ -160,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", @@ -183,8 +183,7 @@ "react-webcam": "^7.1.1", "react-window": "^1.8.9", "semver": "^7.5.2", - "shim-keyboard-event-key": "^1.0.3", - "underscore": "^1.13.1" + "shim-keyboard-event-key": "^1.0.3" }, "devDependencies": { "@actions/core": "1.10.0", @@ -233,7 +232,6 @@ "@types/react-test-renderer": "^18.0.0", "@types/semver": "^7.5.4", "@types/setimmediate": "^1.0.2", - "@types/underscore": "^1.11.5", "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.7.0", "@typescript-eslint/eslint-plugin": "^6.13.2", @@ -252,6 +250,7 @@ "concurrently": "^8.2.2", "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", + "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "^29.2.0", @@ -294,8 +293,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/@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+fix-inverted-flatlist.patch b/patches/react-native+0.73.4+014+fix-inverted-flatlist.patch new file mode 100644 index 000000000000..7bed06d01913 --- /dev/null +++ b/patches/react-native+0.73.4+014+fix-inverted-flatlist.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp +index a8ecce5..6ad790e 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp +@@ -66,7 +66,17 @@ void ScrollViewShadowNode::layout(LayoutContext layoutContext) { + Point ScrollViewShadowNode::getContentOriginOffset() const { + auto stateData = getStateData(); + auto contentOffset = stateData.contentOffset; +- return {-contentOffset.x, -contentOffset.y + stateData.scrollAwayPaddingTop}; ++ auto props = getConcreteProps(); ++ ++ float productX = 1.0f; ++ float productY = 1.0f; ++ ++ for (const auto& operation : props.transform.operations) { ++ productX *= operation.x; ++ productY *= operation.y; ++ } ++ ++ return {-contentOffset.x * productX, (-contentOffset.y + stateData.scrollAwayPaddingTop) * productY}; + } + + } // 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+014+iOSCoreAnimationBorderRendering.patch b/patches/react-native+0.73.4+014+iOSCoreAnimationBorderRendering.patch new file mode 100644 index 000000000000..b59729e79622 --- /dev/null +++ b/patches/react-native+0.73.4+014+iOSCoreAnimationBorderRendering.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm +index b4cfb3d..7aa00e5 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/RCTMountingManager.mm +@@ -49,6 +49,9 @@ static void RCTPerformMountInstructions( + { + SystraceSection s("RCTPerformMountInstructions"); + ++ [CATransaction begin]; ++ [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; ++ + for (const auto &mutation : mutations) { + switch (mutation.type) { + case ShadowViewMutation::Create: { +@@ -147,6 +150,7 @@ static void RCTPerformMountInstructions( + } + } + } ++ [CATransaction commit]; + } + + @implementation RCTMountingManager { 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-screens+3.30.1+001+fix-screen-type.patch b/patches/react-native-screens+3.30.1+001+fix-screen-type.patch new file mode 100644 index 000000000000..f282ec58b07b --- /dev/null +++ b/patches/react-native-screens+3.30.1+001+fix-screen-type.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/react-native-screens/src/components/Screen.tsx b/node_modules/react-native-screens/src/components/Screen.tsx +index 3f9a1cb..45767f7 100644 +--- a/node_modules/react-native-screens/src/components/Screen.tsx ++++ b/node_modules/react-native-screens/src/components/Screen.tsx +@@ -79,6 +79,7 @@ export class InnerScreen extends React.Component { + // Due to how Yoga resolves layout, we need to have different components for modal nad non-modal screens + const AnimatedScreen = + Platform.OS === 'android' || ++ stackPresentation === undefined || + stackPresentation === 'push' || + stackPresentation === 'containedModal' || + stackPresentation === 'containedTransparentModal' 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/src/App.tsx b/src/App.tsx index a3a9f7a3f3b6..6316fa80fba1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ 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 {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; @@ -78,6 +79,7 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, + ReportIDsContextProvider, PlaybackContextProvider, FullScreenContextProvider, VolumeContextProvider, diff --git a/src/CONST.ts b/src/CONST.ts index a840cb481a1a..83690d5e9a85 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4,11 +4,14 @@ 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 = { - unit: string; + unit: Unit; rate: number; }; type CurrencyDefaultMileageRate = Record; @@ -52,6 +55,7 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', + INVOICE: 'invoice', } as const; // Explicit type annotation is required @@ -66,6 +70,8 @@ const onboardingChoices = { LOOKING_AROUND: 'newDotLookingAround', }; +type OnboardingPurposeType = ValueOf; + const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], @@ -73,8 +79,13 @@ const CONST = { // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, - ANIMATED_HIGHLIGHT_DELAY: 500, - ANIMATED_HIGHLIGHT_DURATION: 500, + WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, + ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, + ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, + ANIMATED_HIGHLIGHT_START_DELAY: 10, + ANIMATED_HIGHLIGHT_START_DURATION: 300, + ANIMATED_HIGHLIGHT_END_DELAY: 800, + ANIMATED_HIGHLIGHT_END_DURATION: 2000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, @@ -550,6 +561,7 @@ const CONST = { CONCIERGE_ICON_URL_2021: `${CLOUDFRONT_URL}/images/icons/concierge_2021.png`, CONCIERGE_ICON_URL: `${CLOUDFRONT_URL}/images/icons/concierge_2022.png`, UPWORK_URL: 'https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22', + DEEP_DIVE_EXPENSIFY_CARD: 'https://community.expensify.com/discussion/4848/deep-dive-expensify-card-and-quickbooks-online-auto-reconciliation-how-it-works', GITHUB_URL: 'https://github.com/Expensify/App', TERMS_URL: `${USE_EXPENSIFY_URL}/terms`, PRIVACY_URL: `${USE_EXPENSIFY_URL}/privacy`, @@ -794,6 +806,7 @@ const CONST = { EXPENSE: 'expense', IOU: 'iou', TASK: 'task', + INVOICE: 'invoice', }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { @@ -837,6 +850,16 @@ const CONST = { 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!', @@ -845,9 +868,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: { @@ -1132,6 +1154,11 @@ const CONST = { JPEG: 'image/jpeg', }, + IMAGE_OBJECT_POSITION: { + TOP: 'top', + INITIAL: 'initial', + }, + FILE_TYPE_REGEX: { // Image MimeTypes allowed by iOS photos app. IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/, @@ -1217,12 +1244,32 @@ const CONST = { }, QUICKBOOKS_ONLINE: 'quickbooksOnline', - QUICKBOOKS_IMPORTS: { + QUICK_BOOKS_CONFIG: { SYNC_CLASSES: 'syncClasses', ENABLE_NEW_CATEGORIES: 'enableNewCategories', SYNC_CUSTOMERS: 'syncCustomers', SYNC_LOCATIONS: 'syncLocations', SYNC_TAXES: 'syncTaxes', + PREFERRED_EXPORTER: 'exporter', + EXPORT_DATE: 'exportDate', + OUT_OF_POCKET_EXPENSES: 'outOfPocketExpenses', + EXPORT_INVOICE: 'exportInvoice', + EXPORT_ENTITY: 'exportEntity', + EXPORT_ACCOUNT: 'exportAccount', + EXPORT_ACCOUNT_PAYABLE: 'exportAccountPayable', + EXPORT_COMPANY_CARD_ACCOUNT: 'exportCompanyCardAccount', + EXPORT_COMPANY_CARD: 'exportCompanyCard', + AUTO_SYNC: 'autoSync', + SYNCE_PEOPLE: 'syncPeople', + AUTO_CREATE_VENDOR: 'autoCreateVendor', + REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID', + COLLECTION_ACCOUNT_ID: 'collectionAccountID', + }, + + QUICKBOOKS_EXPORT_ENTITY: { + VENDOR_BILL: 'vendorBill', + CHECK: 'check', + JOURNAL_ENTRY: 'journalEntry', }, ACCOUNT_ID: { @@ -1343,6 +1390,13 @@ const CONST = { ERROR: 'ERROR', EXIT: 'EXIT', }, + DEFAULT_DATA: { + bankName: '', + plaidAccessToken: '', + bankAccounts: [] as PlaidBankAccount[], + isLoading: false, + errors: {}, + }, }, ONFIDO: { @@ -1411,16 +1465,19 @@ const CONST = { ACTION: { EDIT: 'edit', CREATE: 'create', - REQUEST: 'request', + SUBMIT: 'submit', CATEGORIZE: 'categorize', SHARE: 'share', }, DEFAULT_AMOUNT: 0, TYPE: { SEND: 'send', + PAY: 'pay', SPLIT: 'split', REQUEST: 'request', - TRACK_EXPENSE: 'track-expense', + INVOICE: 'invoice', + SUBMIT: 'submit', + TRACK: 'track', }, REQUEST_TYPE: { DISTANCE: 'distance', @@ -1588,6 +1645,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', @@ -3300,7 +3378,7 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', - SENT: 'sent', + SHARED: 'shared', DRAFTS: 'drafts', WAITING_ON_YOU: 'waitingOnYou', FINISHED: 'finished', @@ -3377,9 +3455,9 @@ const CONST = { REFERRAL_PROGRAM: { CONTENT_TYPES: { - MONEY_REQUEST: 'request', + SUBMIT_EXPENSE: 'submitExpense', START_CHAT: 'startChat', - SEND_MONEY: 'sendMoney', + PAY_SOMEONE: 'paySomeone', REFER_FRIEND: 'referralFriend', SHARE_CODE: 'shareCode', }, @@ -3388,6 +3466,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 +3646,7 @@ const CONST = { "# 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' + + '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' + @@ -3571,7 +3659,7 @@ const CONST = { '# Expensify is the fastest way to get paid back!\n' + '\n' + 'To submit expenses for reimbursement:\n' + - '1. From the home screen, click the green + button > Request money.\n' + + '1. From the home screen, click the green + button > *Request money*.\n' + "2. Enter an amount or scan a receipt, then input your boss's email.\n" + '\n' + "That'll send a request to get you paid back. Let me know if you have any questions!", @@ -3579,7 +3667,7 @@ const CONST = { "# Let's start managing your team's expenses!\n" + '\n' + "To manage your team's 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' + + '1. From the home screen, click the green + button > *New Workspace*\n' + '2. Give your workspace a name (e.g. "Sales team expenses").\n' + '\n' + 'Then, invite your team to your workspace via the Members pane and [connect a business bank account](https://help.expensify.com/articles/new-expensify/bank-accounts/Connect-a-Bank-Account) to reimburse them. Let me know if you have any questions!', @@ -3587,7 +3675,7 @@ const CONST = { "# 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' + + '1. From the home screen, click the green + button > *New Workspace*\n' + '2. Give your workspace a name (e.g. "My expenses").\n' + '\n' + 'Then, add expenses to your workspace:\n' + @@ -3600,12 +3688,268 @@ const CONST = { '# Splitting the bill is as easy as a conversation!\n' + '\n' + 'To split an expense:\n' + - '1. From the home screen, click the green + button > Request money.\n' + + '1. From the home screen, click the green + button > *Request money*.\n' + '2. Enter an amount or scan a receipt, then choose who you want to split it with.\n' + '\n' + "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", }, + 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/guided-setup-track-business.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-business.jpg`, + 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/guided-setup-get-paid-back.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + type: 'submitExpense', + autoCompleted: false, + title: 'Submit an expense', + subtitle: 'Submit an expense by entering an amount or scanning a receipt.', + message: + 'Here’s how to submit an expense:\n' + + '\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' + + 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', + }, + { + 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: + 'Here’s how to set up your wallet:\n' + + '\n' + + '1. Click your profile picture.\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.', + }, + ], + }, + [onboardingChoices.MANAGE_TEAM]: { + message: 'Here are some important tasks to help get your team’s expenses under control.', + video: { + url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-manage-team.jpg`, + 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: 'meetGuide', + autoCompleted: false, + title: 'Meet your setup specialist', + subtitle: '', + message: ({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.`, + }, + { + type: 'setupCategories', + autoCompleted: false, + title: 'Set up categories', + subtitle: 'Set up categories so your team can code expenses for easy reporting.', + message: + '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' + + '4. Enable and disable default categories.\n' + + '5. Click Add categories to make your own.\n' + + '\n' + + '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: + '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' + + '\n' + + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team.', + }, + { + type: 'inviteTeam', + autoCompleted: false, + title: 'Invite your team', + subtitle: 'Invite your team to Expensify so they can start tracking expenses today.', + message: + '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' + + '4. Enter emails or phone numbers. \n' + + '5. Add an invite message if you want.\n' + + '\n' + + 'That’s it! Happy expensing :)', + }, + ], + }, + [onboardingChoices.PERSONAL_SPEND]: { + message: 'Here’s how to track your spend in a few clicks.', + video: { + url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + type: 'trackExpense', + autoCompleted: false, + title: 'Track an expense', + subtitle: 'Track an expense in any currency, whether you have a receipt or not.', + 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.CHAT_SPLIT]: { + message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', + video: { + 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, + }, + tasks: [ + { + type: 'startChat', + autoCompleted: false, + title: 'Start a chat', + subtitle: 'Start a chat with a friend or group using their email or phone number.', + message: + 'Here’s how to start a chat:\n' + + '\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' + + '\n' + + 'Every chat will also turn into an email or text that they can respond to directly.', + }, + { + type: 'splitExpense', + autoCompleted: false, + title: 'Split an expense', + subtitle: 'Split an expense right in your chat with one or more friends.', + message: + 'Here’s how to request money:\n' + + '\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' + + 'Feel free to add more details if you want, or just send it off. Let’s get you paid back!', + }, + { + 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: + 'Here’s how to enable your wallet:\n' + + '\n' + + '1. Click your profile picture.\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.', + }, + ], + }, + [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.", + tasks: [], + }, + }, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, @@ -3625,31 +3969,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(`{ @@ -4319,6 +4675,18 @@ const CONST = { }, }, + QUICKBOOKS_EXPORT_DATE: { + LAST_EXPENSE: 'lastExpense', + EXPORTED_DATE: 'exportedDate', + SUBMITTED_DATA: 'submittedData', + }, + + QUICKBOOKS_EXPORT_COMPANY_CARD: { + CREDIT_CARD: 'creditCard', + DEBIT_CARD: 'debitCard', + VENDOR_BILL: 'vendorBill', + }, + SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', }, @@ -4351,6 +4719,6 @@ type Country = keyof typeof CONST.ALL_COUNTRIES; type IOUType = ValueOf; type IOUAction = ValueOf; -export type {Country, IOUAction, IOUType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType}; export default CONST; 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 819680db0e8a..1a27d691e2ef 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', @@ -336,7 +340,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 +358,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 */ @@ -385,6 +391,8 @@ const ONYXKEYS = { DISPLAY_NAME_FORM_DRAFT: 'displayNameFormDraft', ONBOARDING_PERSONAL_DETAILS_FORM: 'onboardingPersonalDetailsForm', ONBOARDING_PERSONAL_DETAILS_FORM_DRAFT: 'onboardingPersonalDetailsFormDraft', + ONBOARDING_PERSONAL_WORK: 'onboardingWorkForm', + ONBOARDING_PERSONAL_WORK_DRAFT: 'onboardingWorkFormDraft', ROOM_NAME_FORM: 'roomNameForm', ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft', REPORT_DESCRIPTION_FORM: 'reportDescriptionForm', @@ -475,6 +483,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; [ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM]: FormTypes.DisplayNameForm; + [ONYXKEYS.FORMS.ONBOARDING_PERSONAL_WORK]: FormTypes.WorkForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm; [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm; [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm; @@ -538,7 +547,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; @@ -556,12 +564,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; @@ -569,7 +579,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; @@ -611,6 +620,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; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 94bd1c2b612d..df794d79559d 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: { @@ -92,12 +97,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', @@ -126,12 +131,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', @@ -208,11 +213,6 @@ const ROUTES = { route: 'r/:reportID/avatar', getRoute: (reportID: string) => `r/${reportID}/avatar` as const, }, - EDIT_REQUEST: { - route: 'r/:threadReportID/edit/:field/:tagIndex?', - getRoute: (threadReportID: string, field: ValueOf, tagIndex?: number) => - `r/${threadReportID}/edit/${field as string}${typeof tagIndex === 'number' ? `/${tagIndex}` : ''}` as const, - }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}` as const, @@ -277,11 +277,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, @@ -315,18 +310,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) => @@ -372,6 +364,11 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`${action as string}/${iouType as string}/distance/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_DISTANCE_RATE: { + route: ':action/:iouType/distanceRate/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/distanceRate/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_MERCHANT: { route: ':action/:iouType/merchant/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => @@ -418,20 +415,20 @@ const ROUTES = { }, MONEY_REQUEST_STATE_SELECTOR: { - route: 'request/state', + route: 'submit/state', getRoute: (state?: string, backTo?: string, label?: string) => - `${getUrlWithBackToParam(`request/state${state ? `?state=${encodeURIComponent(state)}` : ''}`, backTo)}${ + `${getUrlWithBackToParam(`submit/state${state ? `?state=${encodeURIComponent(state)}` : ''}`, backTo)}${ // the label param can be an empty string so we cannot use a nullish ?? operator // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing label ? `${backTo || state ? '&' : '?'}label=${encodeURIComponent(label)}` : '' }` as const, }, - IOU_REQUEST: 'request/new', - IOU_SEND: 'send/new', - IOU_SEND_ADD_BANK_ACCOUNT: 'send/new/add-bank-account', - IOU_SEND_ADD_DEBIT_CARD: 'send/new/add-debit-card', - IOU_SEND_ENABLE_PAYMENTS: 'send/new/enable-payments', + IOU_REQUEST: 'submit/new', + IOU_SEND: 'pay/new', + IOU_SEND_ADD_BANK_ACCOUNT: 'pay/new/add-bank-account', + IOU_SEND_ADD_DEBIT_CARD: 'pay/new/add-debit-card', + IOU_SEND_ENABLE_PAYMENTS: 'pay/new/enable-payments', NEW_TASK: 'new/task', NEW_TASK_ASSIGNEE: 'new/task/assignee', @@ -440,10 +437,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,10 +465,58 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile', getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const, }, + WORKSPACE_ACCOUNTING: { + route: 'settings/workspaces/:policyID/accounting', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, + }, WORKSPACE_PROFILE_CURRENCY: { route: 'settings/workspaces/:policyID/profile/currency', getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/currency` as const, }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT: { + 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/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_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/export/preferred-exporter', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/export/quickbooks-online/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', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/out-of-pocket-expense` as const, + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/out-of-pocket-expense/account-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/out-of-pocket-expense/account-select` as const, + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_SELECT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/out-of-pocket-expense/entity-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/out-of-pocket-expense/entity-select` as const, + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/date-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/date-select` as const, + }, WORKSPACE_PROFILE_NAME: { route: 'settings/workspaces/:policyID/profile/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const, @@ -560,6 +601,18 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/advanced` as const, + }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/account-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/account-selector` as const, + }, + WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const, + }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, @@ -690,9 +743,11 @@ const ROUTES = { route: 'referral/:contentType', getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, - PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', + TRACK_TRAINING_MODAL: 'track-training', + PROCESS_MONEY_REQUEST_HOLD: 'hold-expense-educational', ONBOARDING_ROOT: 'onboarding', ONBOARDING_PERSONAL_DETAILS: 'onboarding/personal-details', + ONBOARDING_WORK: 'onboarding/work', ONBOARDING_PURPOSE: 'onboarding/purpose', WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', @@ -733,6 +788,7 @@ const ROUTES = { */ const HYBRID_APP_ROUTES = { MONEY_REQUEST_CREATE: '/request/new/scan', + MONEY_REQUEST_SUBMIT_CREATE: '/submit/new/scan', } as const; export {HYBRID_APP_ROUTES, getUrlWithBackToParam}; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e55fbfc181b4..bfe2935eeb7f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -25,6 +25,7 @@ const SCREENS = { WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane', SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', + REPORT_RHP: 'Search_Report_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, SETTINGS: { @@ -116,7 +117,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 +131,7 @@ const SCREENS = { ROOM_INVITE: 'RoomInvite', REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', + SEARCH_REPORT: 'SearchReport', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -152,6 +153,7 @@ const SCREENS = { STEP_DATE: 'Money_Request_Step_Date', STEP_DESCRIPTION: 'Money_Request_Step_Description', STEP_DISTANCE: 'Money_Request_Step_Distance', + STEP_DISTANCE_RATE: 'Money_Request_Step_Rate', STEP_MERCHANT: 'Money_Request_Step_Merchant', STEP_PARTICIPANTS: 'Money_Request_Step_Participants', STEP_SCAN: 'Money_Request_Step_Scan', @@ -159,6 +161,7 @@ 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_SEND_FROM: 'Money_Request_Step_Send_From', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', @@ -214,6 +217,20 @@ const SCREENS = { QUICKBOOKS_ONLINE_CUSTOMERS: 'Policy_Accounting_Quickbooks_Online_Import_Customers', QUICKBOOKS_ONLINE_LOCATIONS: 'Policy_Accounting_Quickbooks_Online_Import_Locations', QUICKBOOKS_ONLINE_TAXES: 'Policy_Accounting_Quickbooks_Online_Import_Taxes', + 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: '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_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', + QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Out_Of_Pocket_Expenses_Account_Select', + 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', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', @@ -270,7 +287,6 @@ const SCREENS = { }, EDIT_REQUEST: { - ROOT: 'EditRequest_Root', CURRENCY: 'EditRequest_Currency', REPORT_FIELD: 'EditRequest_ReportField', }, @@ -292,12 +308,7 @@ const SCREENS = { ONBOARDING: { PERSONAL_DETAILS: 'Onboarding_Personal_Details', PURPOSE: 'Onboarding_Purpose', - }, - - ONBOARD_ENGAGEMENT: { - ROOT: 'Onboard_Engagement_Root', - MANAGE_TEAMS_EXPENSES: 'Manage_Teams_Expenses', - EXPENSIFY_CLASSIC: 'Expenisfy_Classic', + WORK: 'Onboarding_Work', }, WELCOME_VIDEO: { @@ -331,6 +342,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/AmountForm.tsx b/src/components/AmountForm.tsx index 3c255bb5f482..a102b715d526 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -117,10 +117,10 @@ function AmountForm( const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); const isForwardDelete = currentAmount.length > strippedAmount.length && forwardDeletePressedRef.current; - setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length)); + setSelection(getNewSelection(selection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length)); onInputChange?.(strippedAmount); }, - [amountMaxLength, currentAmount, decimals, onInputChange], + [amountMaxLength, currentAmount, decimals, onInputChange, selection], ); // Modifies the amount to match the decimals for changed currency. @@ -225,6 +225,8 @@ function AmountForm( }} onKeyPress={textInputKeyPress} isCurrencyPressable={isCurrencyPressable} + style={[styles.iouAmountTextInput]} + containerStyle={[styles.iouAmountTextInputContainer]} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index abdef6707327..e0a494ec6fb1 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type {ForwardedRef} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle, ViewStyle} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {TextSelection} from './Composer/types'; import TextInput from './TextInput'; @@ -31,21 +30,23 @@ type AmountTextInputProps = { /** Function to call to handle key presses in the text input */ onKeyPress?: (event: NativeSyntheticEvent) => void; + + /** Style for the TextInput container */ + containerStyle?: StyleProp; } & Pick; function AmountTextInput( - {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress, ...rest}: AmountTextInputProps, + {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress, containerStyle, ...rest}: AmountTextInputProps, ref: ForwardedRef, ) { - const styles = useThemeStyles(); return ( {}, onModalHide = () => {}, @@ -181,6 +185,7 @@ function AttachmentModal({ const nope = useSharedValue(false); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); + const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); const [file, setFile] = useState( originalFileName @@ -422,7 +427,7 @@ function AttachmentModal({ Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.REQUEST, + iouType, transaction?.transactionID ?? '', report?.reportID ?? '', Navigation.getActiveRouteWithoutParams(), @@ -449,7 +454,7 @@ function AttachmentModal({ } return menuItems; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReceiptAttachment, parentReport, parentReportActions, policy, transaction, file, sourceState]); + }, [isReceiptAttachment, parentReport, parentReportActions, policy, transaction, file, sourceState, iouType]); // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 358f5333bfba..bf48894beaab 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -90,7 +90,7 @@ function Avatar({ if (isWorkspace) { iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(name); } else if (useFallBackAvatar) { - iconColors = StyleUtils.getBackgroundColorAndFill(theme.border, theme.icon); + iconColors = StyleUtils.getBackgroundColorAndFill(theme.buttonHoveredBG, theme.icon); } else { iconColors = null; } 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/Badge.tsx b/src/components/Badge.tsx index b23d02cd7685..75a00e802e98 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -2,7 +2,6 @@ import React, {useCallback} from 'react'; import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -61,15 +60,21 @@ function Badge({ }: BadgeProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const theme = useTheme(); - const textColorStyles = success || error ? styles.textWhite : undefined; const Wrapper = pressable ? PressableWithoutFeedback : View; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; + const iconColor = StyleUtils.getIconColorStyle(success, error); + const wrapperStyles: (state: PressableStateCallbackType) => StyleProp = useCallback( - ({pressed}) => [styles.badge, styles.ml2, StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC), badgeStyles], - [styles.badge, styles.ml2, StyleUtils, success, error, environment, badgeStyles], + ({pressed}) => [ + styles.defaultBadge, + styles.alignSelfCenter, + styles.ml2, + StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC), + badgeStyles, + ], + [styles.defaultBadge, styles.alignSelfCenter, styles.ml2, StyleUtils, success, error, environment, badgeStyles], ); return ( @@ -87,12 +92,12 @@ function Badge({ width={variables.iconSizeExtraSmall} height={variables.iconSizeExtraSmall} src={icon} - fill={theme.icon} + fill={iconColor} /> )} {text} 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 ? ( ) : ( {}, i const styles = useThemeStyles(); const [timer, setTimer] = useState(null); const {isExtraSmallScreenHeight} = useWindowDimensions(); + const numberPressedRef = useRef(numberPressed); + + useEffect(() => { + numberPressedRef.current = numberPressed; + }, [numberPressed]); /** * Handle long press key on number pad. @@ -46,7 +51,7 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i longPressHandlerStateChanged(true); const newTimer = setInterval(() => { - numberPressed(key); + numberPressedRef.current?.(key); }, 100); setTimer(newTimer); diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 6cea253d5957..4d135cdd88e2 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -9,7 +9,7 @@ import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ComposerUtils from '@libs/ComposerUtils'; +import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import type {ComposerProps} from './types'; function Composer( @@ -21,7 +21,6 @@ function Composer( isComposerFullSize = false, setIsFullComposerAvailable = () => {}, autoFocus = false, - isFullComposerAvailable = false, style, // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. @@ -75,14 +74,13 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={setTextInputRef} value={value} - onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)} + onContentSizeChange={(e) => updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true)} rejectResponderTermination={false} smartInsertDelete={false} textAlignVertical="center" style={[composerStyle, maxHeightStyle]} markdownStyle={markdownStyle} autoFocus={autoFocus} - isFullComposerAvailable={isFullComposerAvailable} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 23d24a5ae5dd..4bc54d13b056 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -14,9 +14,7 @@ import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import * as ComposerUtils from '@libs/ComposerUtils'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; @@ -58,14 +56,11 @@ function Composer( style, shouldClear = false, autoFocus = false, - isFullComposerAvailable = false, shouldCalculateCaretPosition = false, - numberOfLines: numberOfLinesProp = 0, isDisabled = false, onClear = () => {}, onPasteFile = () => {}, onSelectionChange = () => {}, - onNumberOfLinesChange = () => {}, setIsFullComposerAvailable = () => {}, checkComposerVisibility = () => false, selection: selectionProp = { @@ -83,10 +78,8 @@ function Composer( const styles = useThemeStyles(); const markdownStyle = useMarkdownStyle(value); const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); const textRef = useRef(null); const textInput = useRef(null); - const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { start: number; @@ -109,7 +102,6 @@ function Composer( return; } textInput.current?.clear(); - setNumberOfLines(1); onClear(); }, [shouldClear, onClear]); @@ -126,12 +118,8 @@ function Composer( * Adds the cursor position to the selection change event. */ const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { - if (!isRendered) { - return; - } const webEvent = event as BaseSyntheticEvent; - - if (shouldCalculateCaretPosition) { + if (shouldCalculateCaretPosition && isRendered) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); @@ -236,41 +224,6 @@ function Composer( [onPasteFile, checkComposerVisibility], ); - /** - * Check the current scrollHeight of the textarea (minus any padding) and - * divide by line height to get the total number of rows for the textarea. - */ - const updateNumberOfLines = useCallback(() => { - if (!textInput.current) { - return; - } - // we reset the height to 0 to get the correct scrollHeight - textInput.current.style.height = '0'; - const computedStyle = window.getComputedStyle(textInput.current); - const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; - const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); - setTextInputWidth(computedStyle.width); - - const computedNumberOfLines = ComposerUtils.getNumberOfLines(lineHeight, paddingTopAndBottom, textInput.current.scrollHeight, maxLines); - const generalNumberOfLines = computedNumberOfLines === 0 ? numberOfLinesProp : computedNumberOfLines; - - onNumberOfLinesChange(generalNumberOfLines); - updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, generalNumberOfLines); - setNumberOfLines(generalNumberOfLines); - textInput.current.style.height = 'auto'; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); - - useEffect(() => { - updateNumberOfLines(); - }, [updateNumberOfLines]); - - const currentNumberOfLines = useMemo( - () => (isComposerFullSize ? undefined : numberOfLines), - - [isComposerFullSize, numberOfLines], - ); - useEffect(() => { if (!textInput.current) { return; @@ -333,7 +286,7 @@ function Composer( opacity: 0, }} > - + {`${valueBeforeCaret} `} [ StyleSheet.flatten([style, {outline: 'none'}]), - StyleUtils.getComposeTextAreaPadding(numberOfLines, isComposerFullSize), + StyleUtils.getComposeTextAreaPadding(isComposerFullSize), Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {}, scrollStyleMemo, + StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), isComposerFullSize ? ({height: '100%', maxHeight: 'none' as DimensionValue} as TextStyle) : undefined, ], - [numberOfLines, scrollStyleMemo, styles.rtlTextRenderForSafari, style, StyleUtils, isComposerFullSize], + [style, styles.rtlTextRenderForSafari, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize], ); return ( @@ -376,7 +326,7 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={(el) => (textInput.current = el)} selection={selection} - style={inputStyleMemo} + style={[inputStyleMemo]} markdownStyle={markdownStyle} value={value} defaultValue={defaultValue} @@ -384,24 +334,20 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - numberOfLines={currentNumberOfLines} + onContentSizeChange={(e) => { + setTextInputWidth(`${e.nativeEvent.contentSize.width}px`); + updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); + }} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { - if (isReportActionCompose) { - ReportActionComposeFocusManager.onComposerFocus(null); - } else { - // While a user edits a comment, if they open the LHN menu, we want to ensure that - // the focus returns to the message edit composer after they click on a menu item (e.g. mark as read). - // To achieve this, we re-assign the focus callback here. - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!textInput.current) { - return; - } - - textInput.current.focus(); - }); - } + ReportActionComposeFocusManager.onComposerFocus(() => { + if (!textInput.current) { + return; + } + + textInput.current.focus(); + }); props.onFocus?.(e); }} diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 6bc44aba69cd..531bcd03f8bf 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -21,15 +21,9 @@ type ComposerProps = TextInputProps & { /** The value of the comment box */ value?: string; - /** Number of lines for the comment */ - numberOfLines?: number; - /** Callback method handle when the input is changed */ onChangeText?: (numberOfLines: string) => void; - /** Callback method to update number of lines for the comment */ - onNumberOfLinesChange?: (numberOfLines: number) => void; - /** Callback method to handle pasting a file */ onPasteFile?: (file: File) => void; diff --git a/src/components/EnvironmentBadge.tsx b/src/components/EnvironmentBadge.tsx index 9a0854b815ef..5122b7f461b0 100644 --- a/src/components/EnvironmentBadge.tsx +++ b/src/components/EnvironmentBadge.tsx @@ -1,5 +1,6 @@ import React from 'react'; import useEnvironment from '@hooks/useEnvironment'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Environment from '@libs/Environment/Environment'; import CONST from '@src/CONST'; @@ -15,8 +16,15 @@ const ENVIRONMENT_SHORT_FORM = { function EnvironmentBadge() { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {environment, isProduction} = useEnvironment(); + const adhoc = environment === CONST.ENVIRONMENT.ADHOC; + const success = environment === CONST.ENVIRONMENT.STAGING; + const error = environment !== CONST.ENVIRONMENT.STAGING && environment !== CONST.ENVIRONMENT.ADHOC; + + const badgeEnviromentStyle = StyleUtils.getEnvironmentBadgeStyle(success, error, adhoc); + // If we are on production, don't show any badge if (isProduction) { return null; @@ -26,10 +34,10 @@ function EnvironmentBadge() { return ( void; + + /** Text to show on secondary button */ + helpText?: string; + + /** Link to navigate to when user wants to learn more */ + onHelp?: () => void; +}; + +function FeatureTrainingModal({ + animation, + videoURL, + videoAspectRatio: videoAspectRatioProp, + title = '', + description = '', + shouldShowDismissModalOption = false, + confirmText = '', + onConfirm = () => {}, + helpText = '', + onHelp = () => {}, +}: FeatureTrainingModalProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useOnboardingLayout(); + const [isModalVisible, setIsModalVisible] = useState(true); + const [willShowAgain, setWillShowAgain] = useState(true); + const [videoStatus, setVideoStatus] = useState('video'); + const [isVideoStatusLocked, setIsVideoStatusLocked] = useState(false); + const [videoAspectRatio, setVideoAspectRatio] = useState(videoAspectRatioProp ?? VIDEO_ASPECT_RATIO); + const {isSmallScreenWidth} = useWindowDimensions(); + const {isOffline} = useNetwork(); + + useEffect(() => { + if (isVideoStatusLocked) { + return; + } + + if (isOffline) { + setVideoStatus('animation'); + } else if (!isOffline) { + setVideoStatus('video'); + setIsVideoStatusLocked(true); + } + }, [isOffline, isVideoStatusLocked]); + + const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => { + if (!event) { + return; + } + + if ('naturalSize' in event) { + setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + } else { + setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + } + }; + + const renderIllustration = useCallback(() => { + const aspectRatio = videoAspectRatio || VIDEO_ASPECT_RATIO; + + return ( + + {videoStatus === 'video' ? ( + + ) : ( + + + + )} + + ); + }, [animation, videoURL, videoAspectRatio, videoStatus, isSmallScreenWidth, styles]); + + const toggleWillShowAgain = useCallback(() => setWillShowAgain((prevWillShowAgain) => !prevWillShowAgain), []); + + const closeModal = useCallback(() => { + if (!willShowAgain) { + User.dismissTrackTrainingModal(); + } + setIsModalVisible(false); + Navigation.goBack(); + }, [willShowAgain]); + + const closeAndConfirmModal = useCallback(() => { + closeModal(); + onConfirm?.(); + }, [onConfirm, closeModal]); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + + + {renderIllustration()} + + {title && description && ( + + {title} + {description} + + )} + {shouldShowDismissModalOption && ( + + )} + {helpText && ( + - + ); } 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 1294d2ca8aea..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'; @@ -27,7 +29,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -type MoneyRequestOptions = Record; +type MoneyRequestOptions = Record, PopoverMenuItem>; type AttachmentPickerWithMenuItemsOnyxProps = { /** The policy tied to the 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,28 +129,33 @@ 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.REQUEST]: { - icon: Expensicons.MoneyCircle, + [CONST.IOU.TYPE.SUBMIT]: { + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, report?.reportID ?? ''), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? ''), }, - [CONST.IOU.TYPE.SEND]: { - icon: Expensicons.Send, + [CONST.IOU.TYPE.PAY]: { + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID ?? ''), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? ''), }, - [CONST.IOU.TYPE.TRACK_EXPENSE]: { - icon: Expensicons.DocumentPlus, + [CONST.IOU.TYPE.TRACK]: { + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), + 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.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ + return ReportUtils.temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ ...options[option], })); }, [translate, report, policy, reportParticipantIDs, canUseTrackExpense]); @@ -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..a96375a93582 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -63,9 +63,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 +212,6 @@ function ComposerWithSuggestions( modal, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, parentReportActions, - numberOfLines, // Props: Report reportID, @@ -459,19 +455,6 @@ 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); @@ -760,8 +743,6 @@ function ComposerWithSuggestions( isComposerFullSize={isComposerFullSize} value={value} testID="composer" - numberOfLines={numberOfLines ?? undefined} - onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition onLayout={onLayout} onScroll={hideSuggestionMenu} @@ -808,12 +789,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/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index b9b9025bb02b..75d0c703b5b1 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -342,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; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 9884d005b3f4..05e1163da200 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,8 +1,8 @@ import Str from 'expensify-common/lib/str'; import lodashSortBy from 'lodash/sortBy'; -import type {ForwardedRef, RefAttributes} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import type {Mention} from '@components/MentionSuggestions'; @@ -10,6 +10,7 @@ import MentionSuggestions from '@components/MentionSuggestions'; import {usePersonalDetails} from '@components/OnyxProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import * as LoginUtils from '@libs/LoginUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -17,6 +18,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; import {isValidRoomName} from '@libs/ValidationUtils'; +import * as ReportUserActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Report} from '@src/types/onyx'; @@ -31,11 +33,6 @@ type SuggestionValues = { prefixType: string; }; -type RoomMentionOnyxProps = { - /** All reports shared with the user */ - reports: OnyxCollection; -}; - /** * Check if this piece of string looks like a mention */ @@ -50,24 +47,15 @@ const defaultSuggestionsValues: SuggestionValues = { }; function SuggestionMention( - { - value, - selection, - setSelection, - updateComment, - isAutoSuggestionPickerLarge, - measureParentContainer, - isComposerFocused, - reports, - isGroupPolicyReport, - policyID, - }: SuggestionProps & RoomMentionOnyxProps, + {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused, isGroupPolicyReport, policyID}: SuggestionProps, ref: ForwardedRef, ) { const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const {translate, formatPhoneNumber} = useLocalize(); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); + const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMentionSuggestionsMenuVisible = !!suggestionValues.suggestedMentions.length && suggestionValues.shouldShowSuggestionMenu; @@ -90,6 +78,21 @@ function SuggestionMention( // Used to decide whether to block the suggestions list from showing to prevent flickering const shouldBlockCalc = useRef(false); + /** + * Search for reports suggestions in server. + * + * The function is debounced to not perform requests on every keystroke. + */ + const debouncedSearchInServer = useDebounce( + useCallback(() => { + const foundSuggestionsCount = suggestionValues.suggestedMentions.length; + if (suggestionValues.prefixType === '#' && foundSuggestionsCount < 5 && isGroupPolicyReport) { + ReportUserActions.searchInServer(value, policyID); + } + }, [suggestionValues.suggestedMentions.length, suggestionValues.prefixType, policyID, value, isGroupPolicyReport]), + CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME, + ); + const formatLoginPrivateDomain = useCallback( (displayText = '', userLogin = '') => { if (userLogin !== displayText) { @@ -137,6 +140,7 @@ function SuggestionMention( setSuggestionValues((prevState) => ({ ...prevState, suggestedMentions: [], + shouldShowSuggestionMenu: false, })); }, [ @@ -323,10 +327,11 @@ function SuggestionMention( const shouldDisplayRoomMentionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === ''); if (prefixType === '#' && shouldDisplayRoomMentionsSuggestions) { - // filter reports by room name and current policy - const filteredRoomMentions = getRoomMentionOptions(prefix, reports); - nextState.suggestedMentions = filteredRoomMentions; - nextState.shouldShowSuggestionMenu = !!filteredRoomMentions.length; + // Filter reports by room name and current policy + nextState.suggestedMentions = getRoomMentionOptions(prefix, reports); + + // Even if there are no reports, we should show the suggestion menu - to perform live search + nextState.shouldShowSuggestionMenu = true; } setSuggestionValues((prevState) => ({ @@ -342,6 +347,10 @@ function SuggestionMention( calculateMentionSuggestion(selection.end); }, [selection, calculateMentionSuggestion]); + useEffect(() => { + debouncedSearchInServer(); + }, [suggestionValues.suggestedMentions.length, suggestionValues.prefixType, policyID, value, debouncedSearchInServer]); + const updateShouldShowSuggestionMenuToFalse = useCallback(() => { setSuggestionValues((prevState) => { if (prevState.shouldShowSuggestionMenu) { @@ -390,8 +399,4 @@ function SuggestionMention( SuggestionMention.displayName = 'SuggestionMention'; -export default withOnyx, RoomMentionOnyxProps>({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, -})(forwardRef(SuggestionMention)); +export default forwardRef(SuggestionMention); diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 8d11744740bd..84a689c6f03c 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -66,6 +66,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'; @@ -120,6 +121,11 @@ type ReportActionItemProps = { /** Report action belonging to the report's parent */ parentReportAction: OnyxEntry; + /** The transaction thread report's parentReportAction */ + /** It's used by withOnyx HOC */ + // eslint-disable-next-line react/no-unused-prop-types + parentReportActionForTransactionThread?: OnyxEntry; + /** All the data of the action item */ action: OnyxTypes.ReportAction; @@ -393,7 +399,7 @@ function ReportActionItem({ text: 'actionableMentionTrackExpense.submit', key: `${action.reportActionID}-actionableMentionTrackExpense-submit`, onPress: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(transactionID, report.reportID, CONST.IOU.ACTION.REQUEST, action.reportActionID); + ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(transactionID, report.reportID, CONST.IOU.ACTION.SUBMIT, action.reportActionID); }, isMediumSized: true, }, @@ -799,7 +805,7 @@ function ReportActionItem({ ); } - if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) { return ( {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( @@ -909,6 +915,7 @@ function ReportActionItem({ originalReportID={originalReportID ?? ''} isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} + disabledActions={!ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []} isVisible={hovered && draftMessage === undefined && !hasErrors} draftMessage={draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} @@ -988,10 +995,10 @@ export default withOnyx({ key: ONYXKEYS.USER_WALLET, }, transaction: { - key: ({transactionThreadReport, reportActions}) => { - const parentReportActionID = isEmptyObject(transactionThreadReport) ? '0' : transactionThreadReport.parentReportActionID; - const action = reportActions?.find((reportAction) => reportAction.reportActionID === parentReportActionID); - const transactionID = (action as OnyxTypes.OriginalMessageIOU)?.originalMessage.IOUTransactionID ? (action as OnyxTypes.OriginalMessageIOU).originalMessage.IOUTransactionID : 0; + key: ({parentReportActionForTransactionThread}) => { + const transactionID = (parentReportActionForTransactionThread as OnyxTypes.OriginalMessageIOU)?.originalMessage.IOUTransactionID + ? (parentReportActionForTransactionThread as OnyxTypes.OriginalMessageIOU).originalMessage.IOUTransactionID + : 0; return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, }, diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index dda17e1e83d3..042f217e057d 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -84,8 +84,11 @@ function ReportActionItemSingle({ const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // 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 displayAllActors = useMemo( + () => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport && !ReportUtils.isInvoiceReport(iouReport), + [action?.actionName, iouReport], + ); + const isWorkspaceActor = ReportUtils.isInvoiceReport(iouReport ?? {}) || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 206f7558baac..3c6038697c67 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -50,6 +50,9 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { /** The report's parentReportAction */ parentReportAction: OnyxEntry; + /** The transaction thread report's parentReportAction */ + parentReportActionForTransactionThread: OnyxEntry; + /** Sorted actions prepared for display */ sortedReportActions: OnyxTypes.ReportAction[]; @@ -148,6 +151,7 @@ function ReportActionsList({ listID, onContentSizeChange, shouldEnableAutoScrollToTopThreshold, + parentReportActionForTransactionThread, }: ReportActionsListProps) { const personalDetailsList = usePersonalDetails() || CONST.EMPTY_OBJECT; const styles = useThemeStyles(); @@ -321,14 +325,16 @@ 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. - if (!isFromCurrentUser || !hasNewestReportActionRef.current) { + // 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]))) { return; } InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); }, - [reportScrollManager], + [sortedReportActions, reportScrollManager], ); useEffect(() => { // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? @@ -522,6 +528,7 @@ function ReportActionsList({ reportAction={reportAction} reportActions={reportActions} parentReportAction={parentReportAction} + parentReportActionForTransactionThread={parentReportActionForTransactionThread} index={index} report={report} transactionThreadReport={transactionThreadReport} @@ -544,6 +551,7 @@ function ReportActionsList({ parentReportAction, reportActions, transactionThreadReport, + parentReportActionForTransactionThread, ], ); diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index 7b0dea06eb45..e35b58dd9dae 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -17,6 +17,9 @@ type ReportActionsListItemRendererProps = { /** The report's parentReportAction */ parentReportAction: OnyxEntry; + /** The transaction thread report's parentReportAction */ + parentReportActionForTransactionThread: OnyxEntry; + /** Position index of the report action in the overall report FlatList view */ index: number; @@ -58,6 +61,7 @@ function ReportActionsListItemRenderer({ shouldDisplayNewMarker, linkedReportActionID = '', shouldDisplayReplyDivider, + parentReportActionForTransactionThread, }: ReportActionsListItemRendererProps) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && !ReportActionsUtils.isTransactionThread(parentReportAction); @@ -147,13 +151,14 @@ function ReportActionsListItemRenderer({ parentReportAction={parentReportAction} report={report} transactionThreadReport={transactionThreadReport} + parentReportActionForTransactionThread={parentReportActionForTransactionThread} action={action} reportActions={reportActions} linkedReportActionID={linkedReportActionID} displayAsGroup={displayAsGroup} shouldDisplayNewMarker={shouldDisplayNewMarker} shouldShowSubscriptAvatar={ - (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isExpenseReport(report)) && + ReportUtils.isPolicyExpenseChat(report) && [CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, CONST.REPORT.ACTIONS.TYPE.SUBMITTED, CONST.REPORT.ACTIONS.TYPE.APPROVED].some( (type) => type === reportAction.actionName, ) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 3e3ebf1a9cc3..26f796b8bdc4 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -12,6 +12,7 @@ import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; +import Log from '@libs/Log'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as NumberUtils from '@libs/NumberUtils'; import {generateNewRandomInt} from '@libs/NumberUtils'; @@ -137,21 +138,22 @@ 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 = useMemo(() => { + const [combinedReportActions, parentReportActionForTransactionThread] = useMemo(() => { if (isEmptyObject(transactionThreadReportActions)) { - return allReportActions; + 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); // 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); - }, [allReportActions, transactionThreadReportActions]); + return [ReportActionsUtils.getSortedReportActions(filteredReportActions, true), moneyRequestAction ?? null]; + }, [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID]); const indexOfLinkedAction = useMemo(() => { if (!reportActionID) { @@ -298,6 +300,17 @@ function ReportActionsView({ * displaying. */ const loadOlderChats = useCallback(() => { + Log.info( + `[ReportActionsView] loadOlderChats ${JSON.stringify({ + isOffline: network.isOffline, + isLoadingOlderReportActions, + isLoadingInitialReportActions, + oldestReportActionID: oldestReportAction?.reportActionID, + hasCreatedAction, + isTransactionThread: !isEmptyObject(transactionThreadReport), + })}`, + ); + // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if (!!network.isOffline || isLoadingOlderReportActions || isLoadingInitialReportActions) { return; @@ -323,13 +336,25 @@ function ReportActionsView({ }, [network.isOffline, isLoadingOlderReportActions, isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID, reportActionIDMap, transactionThreadReport]); const loadNewerChats = useCallback(() => { - if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return; - } // Determines if loading older reports is necessary when the content is smaller than the list // and there are fewer than 23 items, indicating we've reached the oldest message. const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; + Log.info( + `[ReportActionsView] loadNewerChats ${JSON.stringify({ + isOffline: network.isOffline, + isLoadingOlderReportActions, + isLoadingInitialReportActions, + newestReportAction: newestReportAction.pendingAction, + firstReportActionID: newestReportAction?.reportActionID, + isLoadingOlderReportsFirstNeeded, + })}`, + ); + + if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } + if ((reportActionID && indexOfLinkedAction > -1 && !isLoadingOlderReportsFirstNeeded) || (!reportActionID && !isLoadingOlderReportsFirstNeeded)) { handleReportActionPagination({firstReportActionID: newestReportAction?.reportActionID}); } @@ -492,6 +517,7 @@ function ReportActionsView({ transactionThreadReport={transactionThreadReport} reportActions={reportActions} parentReportAction={parentReportAction} + parentReportActionForTransactionThread={parentReportActionForTransactionThread} onLayout={recordTimeToMeasureItemLayout} sortedReportActions={reportActionsToDisplay} mostRecentIOUReportActionID={mostRecentIOUReportActionID} diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 04fbd0308390..11d9a0a4871d 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,7 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); + const canWriteInReport = ReportUtils.canWriteInReport(report); const allPersonalDetails = usePersonalDetails(); @@ -131,7 +133,7 @@ function ReportFooter({ return ( <> {hideComposer && ( - + {isAnonymousUser && !isArchivedRoom && ( )} {isArchivedRoom && } + {!canWriteInReport && } {!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,6 +88,7 @@ const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { type: policy.type, role: policy.role, + id: policy.id, isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, pendingAction: policy.pendingAction, avatar: policy.avatar, @@ -85,7 +100,7 @@ const getQuickActionIcon = (action: QuickActionName): React.FC => { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: return Expensicons.MoneyCircle; 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: @@ -93,9 +108,13 @@ const getQuickActionIcon = (action: QuickActionName): React.FC => { case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: return Expensicons.Transfer; 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: + case CONST.QUICK_ACTIONS.TRACK_MANUAL: + case CONST.QUICK_ACTIONS.TRACK_SCAN: + return getIconForAction(CONST.IOU.TYPE.TRACK); default: return Expensicons.MoneyCircle; } @@ -135,7 +154,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 +176,9 @@ 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 canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection), [allPolicies]); const quickActionAvatars = useMemo(() => { if (quickActionReport) { @@ -169,13 +198,13 @@ function FloatingActionButtonAndPopover( const navigateToQuickAction = () => { switch (quickAction?.action) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); + IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); return; case CONST.QUICK_ACTIONS.REQUEST_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN, true); + IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN, true); return; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); + IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); return; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); @@ -187,19 +216,19 @@ function FloatingActionButtonAndPopover( IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); return; case CONST.QUICK_ACTIONS.SEND_MONEY: - IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); + IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); return; case CONST.QUICK_ACTIONS.ASSIGN_TASK: Task.clearOutTaskInfoAndNavigate(quickAction?.chatReportID ?? '', quickActionReport, quickAction.targetAccountID ?? 0, true); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, 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_EXPENSE, 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_EXPENSE, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE); + IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); break; default: } @@ -270,6 +299,8 @@ function FloatingActionButtonAndPopover( showCreateMenu(); } }; + // eslint-disable-next-line react-hooks/exhaustive-deps + const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), [isLoading]); return ( @@ -285,31 +316,35 @@ function FloatingActionButtonAndPopover( text: translate('sidebarScreen.fabNewChat'), onSelected: () => interceptAnonymousUser(Report.startNewChat), }, - ...(canUseTrackExpense + ...(canUseTrackExpense && selfDMReportID ? [ { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), - onSelected: () => + onSelected: () => { interceptAnonymousUser(() => IOU.startMoneyRequest( - CONST.IOU.TYPE.TRACK_EXPENSE, + CONST.IOU.TYPE.TRACK, // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), - ), + ); + if (!hasSeenTrackTraining && !isOffline) { + Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); + } + }, }, ] : []), { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest( - CONST.IOU.TYPE.REQUEST, + CONST.IOU.TYPE.SUBMIT, // When starting to create an expense 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(), @@ -317,18 +352,48 @@ function FloatingActionButtonAndPopover( ), }, { - icon: Expensicons.Send, + icon: Expensicons.Transfer, + text: translate('iou.splitExpense'), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest( + CONST.IOU.TYPE.SPLIT, + // When starting to create a money request 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: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {}), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest( - CONST.IOU.TYPE.SEND, + CONST.IOU.TYPE.PAY, // When starting to pay someone 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(), ), ), }, + ...(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'), @@ -392,12 +457,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..93018fe34e5e 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.isGroupPolicy(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 54% rename from src/pages/iou/steps/MoneyRequestAmountForm.tsx rename to src/pages/iou/MoneyRequestAmountForm.tsx index a5ed35374e00..f6055a271388 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -2,22 +2,20 @@ import {useIsFocused} from '@react-navigation/core'; import type {ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import type {ValueOf} from 'type-fest'; import BigNumberPad from '@components/BigNumberPad'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; +import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput'; +import type {MoneyRequestAmountInputRef} from '@components/MoneyRequestAmountInput'; import ScrollView from '@components/ScrollView'; import SettlementButton from '@components/SettlementButton'; -import TextInputWithCurrencySymbol from '@components/TextInputWithCurrencySymbol'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as Browser from '@libs/Browser'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import getOperatingSystem from '@libs/getOperatingSystem'; import type {MaybePhraseKey} from '@libs/Localize'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -68,19 +66,6 @@ type MoneyRequestAmountFormProps = { selectedTab?: SelectedTabRequest; }; -type Selection = { - start: number; - end: number; -}; - -/** - * Returns the new selection object based on the updated amount's length - */ -const getNewSelection = (oldSelection: Selection, prevLength: number, newLength: number): Selection => { - const cursorPosition = oldSelection.end + (newLength - prevLength); - return {start: cursorPosition, end: cursorPosition}; -}; - const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01; const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) => isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(Math.abs(taxAmount)); @@ -97,7 +82,7 @@ function MoneyRequestAmountForm( isCurrencyPressable = true, isEditing = false, skipConfirmation = false, - iouType = CONST.IOU.TYPE.REQUEST, + iouType = CONST.IOU.TYPE.SUBMIT, policyID = '', bankAccountRoute = '', onCurrencyButtonPress, @@ -108,28 +93,18 @@ function MoneyRequestAmountForm( ) { const styles = useThemeStyles(); const {isExtraSmallScreenHeight} = useWindowDimensions(); - const {translate, toLocaleDigit, numberFormat} = useLocalize(); + const {translate} = useLocalize(); const textInput = useRef(null); + const moneyRequestAmountInput = useRef(null); const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); - const decimals = CurrencyUtils.getCurrencyDecimals(currency); - const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : ''; - - const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString); const [formError, setFormError] = useState(''); const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); const isFocused = useIsFocused(); const wasFocused = usePrevious(isFocused); - const [selection, setSelection] = useState({ - start: selectedAmountAsString.length, - end: selectedAmountAsString.length, - }); - - const forwardDeletePressedRef = useRef(false); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(Math.abs(taxAmount), currency); /** @@ -141,8 +116,10 @@ function MoneyRequestAmountForm( return; } + const selection = moneyRequestAmountInput.current?.getSelection() ?? {start: 0, end: 0}; + event.preventDefault(); - setSelection({ + moneyRequestAmountInput.current?.changeSelection({ start: selection.end, end: selection.end, }); @@ -155,10 +132,22 @@ function MoneyRequestAmountForm( } }; + useEffect(() => { + if (!isFocused || wasFocused) { + return; + } + const selection = moneyRequestAmountInput.current?.getSelection() ?? {start: 0, end: 0}; + + moneyRequestAmountInput.current?.changeSelection({ + start: selection.end, + end: selection.end, + }); + }, [isFocused, wasFocused]); + const initializeAmount = useCallback((newAmount: number) => { const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmount(newAmount).toString() : ''; - setCurrentAmount(frontendAmount); - setSelection({ + moneyRequestAmountInput.current?.changeAmount(frontendAmount); + moneyRequestAmountInput.current?.changeSelection({ start: frontendAmount.length, end: frontendAmount.length, }); @@ -169,70 +158,9 @@ function MoneyRequestAmountForm( return; } initializeAmount(amount); - // we want to re-initialize the state only when the selected tab or amount changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTab, amount]); - - /** - * Sets the selection and the amount accordingly to the value passed to the input - * @param {String} newAmount - Changed amount from user input - */ - const setNewAmount = useCallback( - (newAmount: string) => { - // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value - // More info: https://github.com/Expensify/App/issues/16974 - const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(newAmount); - // Use a shallow copy of selection to trigger setSelection - // More info: https://github.com/Expensify/App/issues/16385 - if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces, decimals)) { - setSelection((prevSelection) => ({...prevSelection})); - return; - } - if (formError) { - setFormError(''); - } - - // setCurrentAmount contains another setState(setSelection) making it error-prone since it is leading to setSelection being called twice for a single setCurrentAmount call. This solution introducing the hasSelectionBeenSet flag was chosen for its simplicity and lower risk of future errors https://github.com/Expensify/App/issues/23300#issuecomment-1766314724. - - let hasSelectionBeenSet = false; - setCurrentAmount((prevAmount) => { - const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); - const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current; - if (!hasSelectionBeenSet) { - hasSelectionBeenSet = true; - setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); - } - return strippedAmount; - }); - }, - [decimals, formError], - ); - - // Modifies the amount to match the decimals for changed currency. - useEffect(() => { - // If the changed currency supports decimals, we can return - if (MoneyRequestUtils.validateAmount(currentAmount, decimals)) { - return; - } - - // If the changed currency doesn't support decimals, we can strip the decimals - setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount)); - - // we want to update only when decimals change (setNewAmount also changes when decimals change). + // we want to re-initialize the state only when the selected tab // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setNewAmount]); - - // Removes text selection if user visits currency selector with selection and comes back - useEffect(() => { - if (!isFocused || wasFocused) { - return; - } - - setSelection({ - start: selection.end, - end: selection.end, - }); - }, [selection.end, isFocused, selection, wasFocused]); + }, [selectedTab]); /** * Update amount with number or Backspace pressed for BigNumberPad. @@ -243,19 +171,21 @@ function MoneyRequestAmountForm( if (shouldUpdateSelection && !textInput.current?.isFocused()) { textInput.current?.focus(); } + const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; + const selection = moneyRequestAmountInput.current?.getSelection() ?? {start: 0, end: 0}; // Backspace button is pressed if (key === '<' || key === 'Backspace') { if (currentAmount.length > 0) { const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start; const newAmount = `${currentAmount.substring(0, selectionStart)}${currentAmount.substring(selection.end)}`; - setNewAmount(MoneyRequestUtils.addLeadingZero(newAmount)); + moneyRequestAmountInput.current?.setNewAmount(MoneyRequestUtils.addLeadingZero(newAmount)); } return; } const newAmount = MoneyRequestUtils.addLeadingZero(`${currentAmount.substring(0, selection.start)}${key}${currentAmount.substring(selection.end)}`); - setNewAmount(newAmount); + moneyRequestAmountInput.current?.setNewAmount(newAmount); }, - [currentAmount, selection, shouldUpdateSelection, setNewAmount], + [shouldUpdateSelection], ); /** @@ -276,6 +206,7 @@ function MoneyRequestAmountForm( const submitAndNavigateToNextPage = useCallback( (iouPaymentType?: PaymentMethodType | undefined) => { // Skip the check for tax amount form as 0 is a valid input + const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { setFormError('iou.error.invalidAmount'); return; @@ -293,38 +224,33 @@ function MoneyRequestAmountForm( onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType}); }, - [currentAmount, taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], + [taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], ); - /** - * Input handler to check for a forward-delete key (or keyboard shortcut) press. - */ - const textInputKeyPress = ({nativeEvent}: NativeSyntheticEvent) => { - const key = nativeEvent?.key.toLowerCase(); - if (Browser.isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) { - // Optimistically anticipate forward-delete on iOS Safari (in cases where the Mac Accessiblity keyboard is being - // used for input). If the Control-D shortcut doesn't get sent, the ref will still be reset on the next key press. - forwardDeletePressedRef.current = true; - return; - } - // Control-D on Mac is a keyboard shortcut for forward-delete. See https://support.apple.com/en-us/HT201236 for Mac keyboard shortcuts. - // Also check for the keyboard shortcut on iOS in cases where a hardware keyboard may be connected to the device. - const operatingSystem = getOperatingSystem(); - forwardDeletePressedRef.current = key === 'delete' || ((operatingSystem === CONST.OS.MAC_OS || operatingSystem === CONST.OS.IOS) && nativeEvent?.ctrlKey && key === 'd'); - }; - - const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); const buttonText: string = useMemo(() => { + const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; 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, currentAmount, currency, isEditing, translate]); + }, [skipConfirmation, iouType, currency, isEditing, translate]); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -339,11 +265,18 @@ function MoneyRequestAmountForm( onMouseDown={(event) => onMouseDown(event, [AMOUNT_VIEW_ID])} style={[styles.moneyRequestAmountContainer, styles.flex1, styles.flexRow, styles.w100, styles.alignItemsCenter, styles.justifyContentCenter]} > - { + if (!formError) { + return; + } + setFormError(''); + }} + shouldUpdateSelection={shouldUpdateSelection} ref={(ref) => { if (typeof forwardedRef === 'function') { forwardedRef(ref); @@ -353,19 +286,9 @@ function MoneyRequestAmountForm( } textInput.current = ref; }} - selectedCurrencyCode={currency} - selection={selection} - onSelectionChange={(e: NativeSyntheticEvent) => { - if (!shouldUpdateSelection) { - return; - } - const maxSelection = formattedAmount.length; - const start = Math.min(e.nativeEvent.selection.start, maxSelection); - const end = Math.min(e.nativeEvent.selection.end, maxSelection); - setSelection({start, end}); - }} - onKeyPress={textInputKeyPress} - isCurrencyPressable={isCurrencyPressable} + moneyRequestAmountInputRef={moneyRequestAmountInput} + inputStyle={[styles.iouAmountTextInput]} + containerStyle={[styles.iouAmountTextInputContainer]} /> {!!formError && ( ) : null} - {iouType === CONST.IOU.TYPE.SEND && skipConfirmation ? ( - - ) : ( -