diff --git a/.eslintignore b/.eslintignore index aa8b769dfede..3d966d096add 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,3 +10,4 @@ docs/vendor/** docs/assets/** web/gtm.js **/.expo/** +src/libs/SearchParser/searchParser.js diff --git a/.eslintrc.js b/.eslintrc.js index 22bb0158bc8e..198620c70b0f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,7 +8,7 @@ const restrictedImportPaths = [ '', "For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.", "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", - "For 'StatusBar', please use '@src/libs/StatusBar' instead.", + "For 'StatusBar', please use '@libs/StatusBar' instead.", "For 'Text', please use '@components/Text' instead.", "For 'ScrollView', please use '@components/ScrollView' instead.", ].join('\n'), @@ -59,8 +59,12 @@ const restrictedImportPaths = [ }, { name: 'expensify-common', - importNames: ['Device'], - message: "Do not import Device directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", + importNames: ['Device', 'ExpensiMark'], + message: [ + '', + "For 'Device', do not import it directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", + "For 'ExpensiMark', please use '@libs/Parser' instead.", + ].join('\n'), }, ]; @@ -109,7 +113,6 @@ module.exports = { }, rules: { // TypeScript specific rules - '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/prefer-enum-initializers': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-non-null-assertion': 'error', diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index 47e13f6313a0..08cb55cab9b9 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -53,7 +53,7 @@ runs: distribution: "oracle" java-version: "17" - - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + - uses: ruby/setup-ruby@v1.187.0 with: ruby-version: "2.7" bundler-cache: true @@ -74,7 +74,7 @@ runs: shell: bash - name: Upload APK - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 + uses: actions/upload-artifact@v4 with: name: ${{ inputs.ARTIFACT_NAME }} path: ${{ inputs.APP_OUTPUT_PATH }} diff --git a/.github/actions/javascript/bumpVersion/bumpVersion.ts b/.github/actions/javascript/bumpVersion/bumpVersion.ts index ff43ab9ee5c5..92b81836ce13 100644 --- a/.github/actions/javascript/bumpVersion/bumpVersion.ts +++ b/.github/actions/javascript/bumpVersion/bumpVersion.ts @@ -49,7 +49,7 @@ if (!semanticVersionLevel || !versionUpdater.isValidSemverLevel(semanticVersionL console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } -const {version: previousVersion}: PackageJson = JSON.parse(fs.readFileSync('./package.json').toString()); +const {version: previousVersion} = JSON.parse(fs.readFileSync('./package.json').toString()) as PackageJson; if (!previousVersion) { core.setFailed('Error: Could not read package.json'); } diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index 93ea47bed2ae..c8360931845a 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -1928,7 +1928,7 @@ class SemVer { do { const a = this.build[i] const b = other.build[i] - debug('prerelease compare', i, a, b) + debug('build compare', i, a, b) if (a === undefined && b === undefined) { return 0 } else if (b === undefined) { diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts index aed8b9dcba0a..caff455e9fa5 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.ts @@ -8,13 +8,13 @@ import GitUtils from '@github/libs/GitUtils'; type IssuesCreateResponse = Awaited>['data']; -type PackageJSON = { +type PackageJson = { version: string; }; async function run(): Promise { // Note: require('package.json').version does not work because ncc will resolve that to a plain string at compile time - const packageJson: PackageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) as PackageJson; const newVersionTag = packageJson.version; try { diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index 5231caa79ed5..93d5d8a9618b 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -33,7 +33,7 @@ const run = () => { } try { - const current: RegressionEntry = JSON.parse(entry); + const current = JSON.parse(entry) as RegressionEntry; // Extract timestamp, Graphite accepts timestamp in seconds if (current.metadata?.creationDate) { diff --git a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts index a178d4073cbb..7799ffe7c9ec 100644 --- a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts +++ b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts @@ -11,7 +11,7 @@ function run() { core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); } - const {version: currentVersion}: PackageJson = JSON.parse(readFileSync('./package.json', 'utf8')); + const {version: currentVersion} = JSON.parse(readFileSync('./package.json', 'utf8')) as PackageJson; if (!currentVersion) { core.setFailed('Error: Could not read package.json'); } diff --git a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts index ad0f393a96a2..d843caf61518 100644 --- a/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts +++ b/.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts @@ -3,7 +3,7 @@ import type {CompareResult, PerformanceEntry} from '@callstack/reassure-compare/ import fs from 'fs'; const run = (): boolean => { - const regressionOutput: CompareResult = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')); + const regressionOutput = JSON.parse(fs.readFileSync('.reassure/output.json', 'utf8')) as CompareResult; const countDeviation = Number(core.getInput('COUNT_DEVIATION', {required: true})); const durationDeviation = Number(core.getInput('DURATION_DEVIATION_PERCENTAGE', {required: true})); diff --git a/.github/scripts/createDocsRoutes.ts b/.github/scripts/createDocsRoutes.ts index fc3f376a1774..a8ac3d511ff9 100644 --- a/.github/scripts/createDocsRoutes.ts +++ b/.github/scripts/createDocsRoutes.ts @@ -5,6 +5,7 @@ import type {ValueOf} from 'type-fest'; type Article = { href: string; title: string; + order?: number; }; type Section = { @@ -60,11 +61,12 @@ function toTitleCase(str: string): string { /** * @param filename - The name of the file */ -function getArticleObj(filename: string): Article { +function getArticleObj(filename: string, order?: number): Article { const href = filename.replace('.md', ''); return { href, title: toTitleCase(href.replaceAll('-', ' ')), + order, }; } @@ -90,6 +92,12 @@ function pushOrCreateEntry(hubs: Hub[], hub: string, } } +function getOrderFromArticleFrontMatter(path: string): number | undefined { + const frontmatter = fs.readFileSync(path, 'utf8').split('---')[1]; + const frontmatterObject = yaml.load(frontmatter) as Record; + return frontmatterObject.order as number | undefined; +} + /** * Add articles and sections to hubs * @param hubs - The hubs inside docs/articles/ for a platform @@ -113,7 +121,8 @@ function createHubsWithArticles(hubs: string[], platformName: ValueOf { - articles.push(getArticleObj(subArticle)); + const order = getOrderFromArticleFrontMatter(`${docsDir}/articles/${platformName}/${hub}/${section}/${subArticle}`); + articles.push(getArticleObj(subArticle, order)); }); pushOrCreateEntry(routeHubs, hub, 'sections', { diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 624c00de6831..5030ea1c2f2b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,6 +28,24 @@ jobs: - name: 🚀 Push tags to trigger staging deploy 🚀 run: git push --tags + + - name: Warn deployers if staging deploy failed + if: ${{ failure() }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 NewDot staging deploy failed. 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} deployProduction: runs-on: ubuntu-latest @@ -65,6 +83,24 @@ jobs: PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --generate-notes + run: gh release create ${{ env.PRODUCTION_VERSION }} --notes '${{ steps.getReleaseBody.outputs.RELEASE_BODY }}' env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} + + - name: Warn deployers if production deploy failed + if: ${{ failure() }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 NewDot production deploy failed. 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index ffce73644263..add4879d8de1 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -156,7 +156,7 @@ jobs: run: mkdir zip - name: Download baseline APK - uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d + uses: actions/download-artifact@v4 id: downloadBaselineAPK with: name: baseline-apk-${{ needs.buildBaseline.outputs.VERSION }} @@ -170,7 +170,7 @@ jobs: run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk" - name: Download delta APK - uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d + uses: actions/download-artifact@v4 id: downloadDeltaAPK with: name: delta-apk-${{ needs.buildDelta.outputs.DELTA_REF }} @@ -184,7 +184,7 @@ jobs: - name: Copy e2e code into zip folder run: cp tests/e2e/dist/index.js zip/testRunner.ts - + - name: Copy profiler binaries into zip folder run: cp -r node_modules/@perf-profiler/android/cpp-profiler/bin zip/bin @@ -257,12 +257,12 @@ jobs: - name: Check if test failed, if so post the results and add the DeployBlocker label id: checkIfRegressionDetected run: | - if grep -q '🔴' ./output.md; then + if grep -q '🔴' "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md"; then # Create an output to the GH action that the test failed: echo "performanceRegressionDetected=true" >> "$GITHUB_OUTPUT" gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash - gh pr comment ${{ inputs.PR_NUMBER }} -F ./output.md + gh pr comment ${{ inputs.PR_NUMBER }} -F "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md" gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." else echo "performanceRegressionDetected=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 640d1eaa1172..ba776f257a3c 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -62,7 +62,7 @@ jobs: java-version: '17' - name: Setup Ruby - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + uses: ruby/setup-ruby@v1.187.0 with: ruby-version: '2.7' bundler-cache: true @@ -80,38 +80,42 @@ jobs: - name: Set version in ENV run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_ENV" - - name: Run Fastlane beta - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane android beta + - name: Run Fastlane + run: bundle exec fastlane android ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || 'beta' }} env: + RUBYOPT: '-rostruct' MYAPP_UPLOAD_STORE_PASSWORD: ${{ secrets.MYAPP_UPLOAD_STORE_PASSWORD }} MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - - - 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 + uses: actions/upload-artifact@v4 with: name: android-sourcemap-${{ github.ref_name }} path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map - - name: Upload Android version to GitHub artifacts + - name: Upload Android build to GitHub artifacts if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: app-production-release.aab path: android/app/build/outputs/bundle/productionRelease/app-production-release.aab - - name: Upload Android version to Browser Stack + - name: Upload Android build to Browser Stack if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@./android/app/build/outputs/bundle/productionRelease/app-production-release.aab" env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + - name: Upload Android build to GitHub Release + if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: | + RUN_ID="$(gh run list --workflow platformDeploy.yml --event push --branch ${{ github.event.release.tag_name }} --json databaseId --jq '.[0].databaseId')" + gh run download "$RUN_ID" --name app-production-release.aab + gh release upload ${{ github.event.release.tag_name }} app-production-release.aab + env: + GITHUB_TOKEN: ${{ github.token }} + - name: Warn deployers if Android production deploy failed if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: 8398a7/action-slack@v3 @@ -147,9 +151,13 @@ jobs: env: DEVELOPER_ID_SECRET_PASSPHRASE: ${{ secrets.DEVELOPER_ID_SECRET_PASSPHRASE }} - - name: Build production desktop app - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: npm run desktop-build + - name: Build desktop app + run: | + if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then + npm run desktop-build + else + npm run desktop-build-staging + fi env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} @@ -159,18 +167,17 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }} + - name: Upload desktop build to GitHub Workflow + uses: actions/upload-artifact@v4 + with: + name: NewExpensify.dmg + path: desktop-build/NewExpensify.dmg - - name: Build staging desktop app - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: npm run desktop-build-staging + - name: Upload desktop build to GitHub Release + if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: gh release upload ${{ github.event.release.tag_name }} desktop-build/NewExpensify.dmg env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }} + GITHUB_TOKEN: ${{ github.token }} iOS: name: Build and deploy iOS @@ -191,7 +198,7 @@ jobs: uses: ./.github/actions/composite/setupNode - name: Setup Ruby - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + uses: ruby/setup-ruby@v1.187.0 with: ruby-version: '2.7' bundler-cache: true @@ -236,43 +243,45 @@ jobs: env: LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + - name: Set iOS version in ENV + run: echo "IOS_VERSION=$(echo '${{ github.event.release.tag_name }}' | tr '-' '.')" >> "$GITHUB_ENV" + - name: Run Fastlane - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios beta + run: bundle exec fastlane ios ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || 'beta' }} env: APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }} APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} + VERSION: ${{ env.IOS_VERSION }} - name: Archive iOS sourcemaps - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ios-sourcemap-${{ github.ref_name }} path: main.jsbundle.map - - name: Upload iOS version to GitHub artifacts + - name: Upload iOS build to GitHub artifacts if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: New Expensify.ipa path: /Users/runner/work/App/App/New Expensify.ipa - - name: Upload iOS version to Browser Stack + - name: Upload iOS build to Browser Stack if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/Users/runner/work/App/App/New Expensify.ipa" env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Set iOS version in ENV - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: echo "IOS_VERSION=$(echo '${{ github.event.release.tag_name }}' | tr '-' '.')" >> "$GITHUB_ENV" - - - name: Run Fastlane for App Store release + - name: Upload iOS build to GitHub Release if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios production + run: | + RUN_ID="$(gh run list --workflow platformDeploy.yml --event push --branch ${{ github.event.release.tag_name }} --json databaseId --jq '.[0].databaseId')" + gh run download "$RUN_ID" --name 'New Expensify.ipa' + gh release upload ${{ github.event.release.tag_name }} 'New Expensify.ipa' env: - VERSION: ${{ env.IOS_VERSION }} + GITHUB_TOKEN: ${{ github.token }} - name: Warn deployers if iOS production deploy failed if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} @@ -314,41 +323,33 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 - - name: Build web for production - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: npm run build - - - name: Build web for staging - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: npm run build-staging - - - name: Build storybook docs for production - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: npm run storybook-build - continue-on-error: true + - name: Build web + run: | + if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then + npm run build + else + npm run build-staging + fi - - name: Build storybook docs for staging - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: npm run storybook-build-staging + - name: Build storybook docs continue-on-error: true + run: | + if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then + npm run storybook-build + else + npm run storybook-build-staging + fi - - name: Deploy production to S3 - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://expensify-cash/ && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://expensify-cash/.well-known/apple-app-site-association s3://expensify-cash/.well-known/apple-app-site-association && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://expensify-cash/.well-known/apple-app-site-association s3://expensify-cash/apple-app-site-association - - - name: Deploy staging to S3 - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://staging-expensify-cash/ && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://staging-expensify-cash/.well-known/apple-app-site-association s3://staging-expensify-cash/.well-known/apple-app-site-association && aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE s3://staging-expensify-cash/.well-known/apple-app-site-association s3://staging-expensify-cash/apple-app-site-association - - - name: Purge production Cloudflare cache - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache + - name: Deploy to S3 + run: | + aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_URL }}/ + aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_URL }}/.well-known/apple-app-site-association ${{ env.S3_URL }}/.well-known/apple-app-site-association + aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_URL }}/.well-known/apple-app-site-association ${{env.S3_URL }}/apple-app-site-association env: - CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} + S3_URL: s3://${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && '' || 'staging-' }}expensify-cash - - name: Purge staging Cloudflare cache - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["staging.new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache + - name: Purge Cloudflare cache + run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && '' || 'staging.' }}new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache env: CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index f09865de0194..bb1cd71c9518 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -32,7 +32,9 @@ jobs: - name: Exit failed workflow if: ${{ needs.typecheck.result == 'failure' || needs.lint.result == 'failure' || needs.test.result == 'failure' }} - run: exit 1 + run: | + echo "Checks failed, exiting ~ typecheck: ${{ needs.typecheck.result }}, lint: ${{ needs.lint.result }}, test: ${{ needs.test.result }}" + exit 1 chooseDeployActions: runs-on: ubuntu-latest diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a695c0acf942..a0489a52711b 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -47,13 +47,13 @@ jobs: BASELINE_BRANCH=${BASELINE_BRANCH:="main"} git fetch origin "$BASELINE_BRANCH" --no-tags --depth=1 git switch "$BASELINE_BRANCH" - npm install --force - npx reassure --baseline + npm install --force || (rm -rf node_modules && npm install --force) + NODE_OPTIONS=--experimental-vm-modules npx reassure --baseline git switch --force --detach - git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours git checkout --ours . - npm install --force - npx reassure --branch + npm install --force || (rm -rf node_modules && npm install --force) + NODE_OPTIONS=--experimental-vm-modules npx reassure --branch - name: Validate output.json id: validateReassureOutput diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 53b3d3374a9e..30a30918f4f6 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -25,7 +25,7 @@ jobs: shell: bash run: | set -e - npx reassure --baseline + NODE_OPTIONS=--experimental-vm-modules npx reassure --baseline - name: Get merged pull request id: getMergedPullRequest diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 10912aaeb436..e0ff9296abc0 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -88,7 +88,7 @@ jobs: java-version: '17' - name: Setup Ruby - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + uses: ruby/setup-ruby@v1.187.0 with: ruby-version: '2.7' bundler-cache: true @@ -117,6 +117,7 @@ jobs: id: runFastlaneBetaTest run: bundle exec fastlane android build_internal env: + RUBYOPT: '-rostruct' S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} S3_BUCKET: ad-hoc-expensify-cash @@ -125,7 +126,7 @@ jobs: MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: android path: ./android_paths.json @@ -161,7 +162,7 @@ jobs: run: sudo xcode-select -switch /Applications/Xcode_15.0.1.app - name: Setup Ruby - uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011 + uses: ruby/setup-ruby@v1.187.0 with: ruby-version: '2.7' bundler-cache: true @@ -217,7 +218,7 @@ jobs: S3_REGION: us-east-1 - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ios path: ./ios_paths.json @@ -321,7 +322,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} - name: Download Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} - name: Read JSONs with android paths diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 476b01f87b07..3bfc0ed28d1a 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -34,7 +34,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' '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' ':!src/libs/SearchParser/*.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/.prettierignore b/.prettierignore index 09de20ba30b0..a9f7e1464529 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,3 +19,6 @@ package-lock.json src/libs/E2E/reactNativeLaunchingTest.ts # Temporary while we keep react-compiler in our repo lib/** + +# Automatically generated files +src/libs/SearchParser/searchParser.js diff --git a/.prettierrc.js b/.prettierrc.js index 3118dc378694..d981112fffae 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -6,6 +6,7 @@ module.exports = { arrowParens: 'always', printWidth: 190, singleAttributePerLine: true, + plugins: [require.resolve('@trivago/prettier-plugin-sort-imports')], /** `importOrder` should be defined in an alphabetical order. */ importOrder: [ '@assets/(.*)$', diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index a7811a5a387d..0aa38f2e3b82 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -100,6 +100,11 @@ const webpackConfig = ({config}: {config: Configuration}) => { }), ); + config.module.rules?.push({ + test: /\.lottie$/, + type: 'asset/resource', + }); + return config; }; diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 0b7dda4621ad..5bcafdc1856c 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,9 +1,65 @@ -import {useIsFocused as realUseIsFocused, useTheme as realUseTheme} from '@react-navigation/native'; +/* eslint-disable import/prefer-default-export, import/no-import-module-exports */ +import type * as ReactNavigation from '@react-navigation/native'; +import createAddListenerMock from '../../../tests/utils/createAddListenerMock'; -// We only want these mocked for storybook, not jest -const useIsFocused: typeof realUseIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true; +const isJestEnv = process.env.NODE_ENV === 'test'; -const useTheme = process.env.NODE_ENV === 'test' ? realUseTheme : () => ({}); +const realReactNavigation = isJestEnv ? jest.requireActual('@react-navigation/native') : (require('@react-navigation/native') as typeof ReactNavigation); + +const useIsFocused = isJestEnv ? realReactNavigation.useIsFocused : () => true; +const useTheme = isJestEnv ? realReactNavigation.useTheme : () => ({}); + +const {triggerTransitionEnd, addListener} = isJestEnv + ? createAddListenerMock() + : { + triggerTransitionEnd: () => {}, + addListener: () => {}, + }; + +const useNavigation = () => ({ + ...realReactNavigation.useNavigation, + navigate: jest.fn(), + getState: () => ({ + routes: [], + }), + addListener, +}); + +type NativeNavigationMock = typeof ReactNavigation & { + triggerTransitionEnd: () => void; +}; export * from '@react-navigation/core'; -export {useIsFocused, useTheme}; +const Link = realReactNavigation.Link; +const LinkingContext = realReactNavigation.LinkingContext; +const NavigationContainer = realReactNavigation.NavigationContainer; +const ServerContainer = realReactNavigation.ServerContainer; +const DarkTheme = realReactNavigation.DarkTheme; +const DefaultTheme = realReactNavigation.DefaultTheme; +const ThemeProvider = realReactNavigation.ThemeProvider; +const useLinkBuilder = realReactNavigation.useLinkBuilder; +const useLinkProps = realReactNavigation.useLinkProps; +const useLinkTo = realReactNavigation.useLinkTo; +const useScrollToTop = realReactNavigation.useScrollToTop; +export { + // Overriden modules + useIsFocused, + useTheme, + useNavigation, + triggerTransitionEnd, + + // Theme modules are left alone + Link, + LinkingContext, + NavigationContainer, + ServerContainer, + DarkTheme, + DefaultTheme, + ThemeProvider, + useLinkBuilder, + useLinkProps, + useLinkTo, + useScrollToTop, +}; + +export type {NativeNavigationMock}; diff --git a/__mocks__/@ua/react-native-airship.ts b/__mocks__/@ua/react-native-airship.ts index ae7661ab672f..14909b58b31c 100644 --- a/__mocks__/@ua/react-native-airship.ts +++ b/__mocks__/@ua/react-native-airship.ts @@ -15,31 +15,31 @@ const iOS: Partial = { }, }; -const pushIOS: AirshipPushIOS = jest.fn().mockImplementation(() => ({ +const pushIOS = jest.fn().mockImplementation(() => ({ setBadgeNumber: jest.fn(), setForegroundPresentationOptions: jest.fn(), setForegroundPresentationOptionsCallback: jest.fn(), -}))(); +}))() as AirshipPushIOS; -const pushAndroid: AirshipPushAndroid = jest.fn().mockImplementation(() => ({ +const pushAndroid = jest.fn().mockImplementation(() => ({ setForegroundDisplayPredicate: jest.fn(), -}))(); +}))() as AirshipPushAndroid; -const push: AirshipPush = jest.fn().mockImplementation(() => ({ +const push = jest.fn().mockImplementation(() => ({ iOS: pushIOS, android: pushAndroid, enableUserNotifications: () => Promise.resolve(false), clearNotifications: jest.fn(), getNotificationStatus: () => Promise.resolve({airshipOptIn: false, systemEnabled: false, airshipEnabled: false}), getActiveNotifications: () => Promise.resolve([]), -}))(); +}))() as AirshipPush; -const contact: AirshipContact = jest.fn().mockImplementation(() => ({ +const contact = jest.fn().mockImplementation(() => ({ identify: jest.fn(), getNamedUserId: () => Promise.resolve(undefined), reset: jest.fn(), module: jest.fn(), -}))(); +}))() as AirshipContact; const Airship: Partial = { addListener: jest.fn(), diff --git a/__mocks__/fs.ts b/__mocks__/fs.ts index cca0aa9520ec..3f8579557c82 100644 --- a/__mocks__/fs.ts +++ b/__mocks__/fs.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ const {fs} = require('memfs'); module.exports = fs; diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 27b78b308446..3deeabf6df2a 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -41,7 +41,7 @@ jest.doMock('react-native', () => { }; }; - const reactNativeMock: ReactNativeMock = Object.setPrototypeOf( + const reactNativeMock = Object.setPrototypeOf( { NativeModules: { ...ReactNative.NativeModules, @@ -86,7 +86,7 @@ jest.doMock('react-native', () => { }, Dimensions: { ...ReactNative.Dimensions, - addEventListener: jest.fn(), + addEventListener: jest.fn(() => ({remove: jest.fn()})), get: () => dimensions, set: (newDimensions: Record) => { dimensions = newDimensions; @@ -98,11 +98,14 @@ jest.doMock('react-native', () => { // so it seems easier to just run the callback immediately in tests. InteractionManager: { ...ReactNative.InteractionManager, - runAfterInteractions: (callback: () => void) => callback(), + runAfterInteractions: (callback: () => void) => { + callback(); + return {cancel: () => {}}; + }, }, }, ReactNative, - ); + ) as ReactNativeMock; return reactNativeMock; }); diff --git a/android/app/build.gradle b/android/app/build.gradle index 4d15eb49cf2a..407d787c3b40 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,6 +15,7 @@ fullstory { org 'o-1WN56P-na1' enabledVariants 'all' logcatLevel 'debug' + recordOnStart false } react { @@ -107,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009000302 - versionName "9.0.3-2" + versionCode 1009001002 + versionName "9.0.10-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/LaptopwithSecondScreenandHourglass.svg b/assets/images/LaptopwithSecondScreenandHourglass.svg new file mode 100644 index 000000000000..c838c82c55ce --- /dev/null +++ b/assets/images/LaptopwithSecondScreenandHourglass.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/circular-arrow-backwards.svg b/assets/images/circular-arrow-backwards.svg new file mode 100644 index 000000000000..209c0aea5fa7 --- /dev/null +++ b/assets/images/circular-arrow-backwards.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/assets/images/computer.svg b/assets/images/computer.svg index 9c2628245eb1..be9eca391e0b 100644 --- a/assets/images/computer.svg +++ b/assets/images/computer.svg @@ -1,216 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/expensifyCard/cardIllustration.svg b/assets/images/expensifyCard/cardIllustration.svg new file mode 100644 index 000000000000..c81bb21568a7 --- /dev/null +++ b/assets/images/expensifyCard/cardIllustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/integrationicons/netsuite-icon-square.svg b/assets/images/integrationicons/netsuite-icon-square.svg index d4f19f4f44c0..1b4557c5a044 100644 --- a/assets/images/integrationicons/netsuite-icon-square.svg +++ b/assets/images/integrationicons/netsuite-icon-square.svg @@ -1,57 +1 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/integrationicons/sage-intacct-icon-square.svg b/assets/images/integrationicons/sage-intacct-icon-square.svg index 33d86259a2d1..fe10342d711e 100644 --- a/assets/images/integrationicons/sage-intacct-icon-square.svg +++ b/assets/images/integrationicons/sage-intacct-icon-square.svg @@ -1,23 +1 @@ - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/product-illustrations/folder-with-papers.svg b/assets/images/product-illustrations/folder-with-papers.svg new file mode 100644 index 000000000000..3d00fb147ccd --- /dev/null +++ b/assets/images/product-illustrations/folder-with-papers.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__empty-state.svg b/assets/images/simple-illustrations/simple-illustration__empty-state.svg new file mode 100644 index 000000000000..154b2269c285 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__empty-state.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg b/assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg new file mode 100644 index 000000000000..01669d07c0f0 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__tire.svg b/assets/images/simple-illustrations/simple-illustration__tire.svg new file mode 100644 index 000000000000..9107c88eb3e2 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__tire.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__virtualcard.svg b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg new file mode 100644 index 000000000000..2c1f538102a2 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js index 5a995fb5de91..ad3a23407b89 100644 --- a/config/electronBuilder.config.js +++ b/config/electronBuilder.config.js @@ -47,7 +47,7 @@ module.exports = { }, target: [ { - target: 'dmg', + target: 'default', arch: ['universal'], }, ], diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index bedd7e50ef94..33fd9131eca0 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -4,13 +4,13 @@ import dotenv from 'dotenv'; import fs from 'fs'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; -import type {Compiler, Configuration} from 'webpack'; +import type {Class} from 'type-fest'; +import type {Configuration, WebpackPluginInstance} from 'webpack'; import {DefinePlugin, EnvironmentPlugin, IgnorePlugin, ProvidePlugin} from 'webpack'; import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import CustomVersionFilePlugin from './CustomVersionFilePlugin'; import type Environment from './types'; -// importing anything from @vue/preload-webpack-plugin causes an error type Options = { rel: string; as: string; @@ -18,13 +18,10 @@ type Options = { include: string; }; -type PreloadWebpackPluginClass = { - new (options?: Options): PreloadWebpackPluginClass; - apply: (compiler: Compiler) => void; -}; +type PreloadWebpackPluginClass = Class; -// require is necessary, there are no types for this package and the declaration file can't be seen by the build process which causes an error. -const PreloadWebpackPlugin: PreloadWebpackPluginClass = require('@vue/preload-webpack-plugin'); +// require is necessary, importing anything from @vue/preload-webpack-plugin causes an error +const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin') as PreloadWebpackPluginClass; const includeModules = [ 'react-native-animatable', diff --git a/desktop/main.ts b/desktop/main.ts index 6ab0bc6579d7..d8c46bbbc89b 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -141,7 +141,7 @@ const manuallyCheckForUpdates = (menuItem?: MenuItem, browserWindow?: BrowserWin autoUpdater .checkForUpdates() - .catch((error) => { + .catch((error: unknown) => { isSilentUpdating = false; return {error}; }) @@ -617,7 +617,7 @@ const mainWindow = (): Promise => { }); const downloadQueue = createDownloadQueue(); - ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData) => { + ipcMain.on(ELECTRON_EVENTS.DOWNLOAD, (event, downloadData: DownloadItem) => { const downloadItem: DownloadItem = { ...downloadData, win: browserWindow, diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index 5fd65532c021..85ffbb30c360 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -69,8 +69,8 @@ platforms: icon: /assets/images/handshake.svg description: Discover the benefits of becoming an Expensify Partner. - - href: integrations - title: Integrations + - href: connections + title: Connections icon: /assets/images/simple-illustration__monitor-remotesync.svg description: Integrate with accounting or HR software to streamline expense approvals. @@ -114,8 +114,8 @@ platforms: icon: /assets/images/shield.svg description: Configure rules, settings, and limits for your company’s spending. - - href: expenses - title: Expenses + - href: expenses-&-payments + title: Expenses & Payments icon: /assets/images/money-into-wallet.svg description: Learn more about expense tracking and submission. diff --git a/docs/_includes/hub.html b/docs/_includes/hub.html index 7f1e25243c09..cac0eaeec382 100644 --- a/docs/_includes/hub.html +++ b/docs/_includes/hub.html @@ -12,13 +12,17 @@

{{ hub.description }}

+{% assign sortedSectionsAndArticles = hub.sections | concat: hub.articles | sort: 'title' %} +
- {% for section in hub.sections %} - {% include section-card.html platform=activePlatform hub=hub.href section=section.href title=section.title %} - {% endfor %} - {% for article in hub.articles %} - {% include article-card.html hub=hub.href href=article.href title=article.title platform=activePlatform %} + {% for item in sortedSectionsAndArticles %} + + {% if item.articles %} + {% include section-card.html platform=activePlatform hub=hub.href section=item.href title=item.title %} + {% else %} + {% include article-card.html hub=hub.href href=item.href title=item.title platform=activePlatform %} + {% endif %} {% endfor %}
diff --git a/docs/_includes/lhn-template.html b/docs/_includes/lhn-template.html index 80302f33f52e..d8298aa22aa7 100644 --- a/docs/_includes/lhn-template.html +++ b/docs/_includes/lhn-template.html @@ -21,43 +21,43 @@ {% for platform in site.data.routes.platforms %} {% if platform.href == activePlatform %}
  • - - + {% for hub in platform.hubs %}
      {% if hub.href == activeHub %} - - +
        - {% for section in hub.sections %} -
      • - {% if section.href == activeSection %} - - - {{ section.title }} - -
          - {% for article in section.articles %} - {% assign article_href = section.href | append: '/' | append: article.href %} - {% include lhn-article-link.html platform=activePlatform hub=hub.href href=article_href title=article.title %} - {% endfor %} -
        - {% else %} - - - {{ section.title }} - - {% endif %} - -
      • - {% endfor %} - - {% for article in hub.articles %} - {% include lhn-article-link.html platform=activePlatform hub=hub.href href=article.href title=article.title %} + {% assign sortedSectionsAndArticles = hub.sections | concat: hub.articles | sort: 'title' %} + {% for item in sortedSectionsAndArticles %} + {% if item.articles %} +
      • + {% if item.href == activeSection %} + +
          + {% for article in item.articles %} + {% assign article_href = item.href | append: '/' | append: article.href %} + {% include lhn-article-link.html platform=activePlatform hub=hub.href href=article_href title=article.title %} + {% endfor %} +
        + {% else %} + + + {{ item.title }} + + {% endif %} +
      • + {% else %} + {% include lhn-article-link.html platform=activePlatform hub=hub.href href=item.href title=item.title %} + {% endif %} {% endfor %}
      {% else %} diff --git a/docs/_includes/section.html b/docs/_includes/section.html index 786e7d997462..b6def157e954 100644 --- a/docs/_includes/section.html +++ b/docs/_includes/section.html @@ -15,7 +15,8 @@

      - {% for article in section.articles %} + {% assign sortedArticles = section.articles | sort: 'order', 'last' | default: 999 %} + {% for article in sortedArticles %} {% assign article_href = section.href | append: '/' | append: article.href %} {% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %} {% endfor %} diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 4232c565e715..cc45e83efa59 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -14,7 +14,7 @@ - + diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index 29b02d8aeb00..f3bf1035ed56 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -749,6 +749,7 @@ button { width: 20px; height: 20px; cursor: pointer; + display: inline-block; } .homepage { diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md index 2ff74760b376..0fd47f1341fa 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements.md @@ -7,29 +7,9 @@ description: International Reimbursements If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you now have the option to send direct reimbursements to nearly any country across the globe! The process to enable global reimbursements is dependent on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below. -# How to request international reimbursements - -## The reimbursement account is in USD - -If your reimbursement bank account is in USD, the first step is connecting the bank account to Expensify. -The individual who plans on sending reimbursements internationally should head to **Settings > Account > Payments > Add Verified Bank Account**. From there, you will provide company details, input personal information, and upload a copy of your ID. - -Once the USD bank account is verified (or if you already had a USD business bank account connected), click the support icon in your Expensify account to inform your Setup Specialist, Account Manager, or Concierge that you’d like to enable international reimbursements. From there, Expensify will ask you to confirm the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -To request international reimbursements, contact Expensify Support to make that request. - -You can do this by clicking on the support icon and informing your Setup Specialist, Account Manager, or Concierge that you’d like to set up global reimbursements on your account. -From there, Expensify will ask you to confirm both the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - # How to verify the bank account for sending international payments -Once international payments are enabled on your Expensify account, the next step is verifying the bank account to send the reimbursements. +The steps for USD accounts and non-USD accounts differ slightly. ## The reimbursement account is in USD @@ -38,9 +18,9 @@ First, confirm the workspace settings are set up correctly by doing the followin 2. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the reimbursement method to direct 3. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the USD bank account to the default account -Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account). +Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account. -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. +From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. If additional information is required, our Support Team will contact you with more details. ## The reimbursement account is in AUD, CAD, GBP, EUR @@ -53,7 +33,7 @@ Next, add the bank account to Expensify: 4. Enter the bank account details 5. Click **Save & Continue** -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. +From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. If additional information is required, our Support Team will contact you with more details. # How to start reimbursing internationally diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md deleted file mode 100644 index 81dcf3488462..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Pay Bills -description: How to receive and pay company bills in Expensify ---- - - -# Overview -Simplify your back office by receiving bills from vendors and suppliers in Expensify. Anyone with or without an Expensify account can send you a bill, and Expensify will file it as a Bill and help you issue the payment. - -# How to Receive Vendor or Supplier Bills in Expensify - -There are three ways to get a vendor or supplier bill into Expensify: - -**Option 1:** Have vendors send bills to Expensify directly: Ask your vendors to email all bills to your Expensify billing intake email. - -**Option 2:** Forward bills to Expensify: If your bills are emailed to you, you can forward those bills to your Expensify billing intake email yourself. - -**Option 3:** Manually upload bills to Expensify: If you receive physical bills, you can manually create a Bill in Expensify on the web from the Reports page: -1. Click **New Report** and choose **Bill** -2. Add the expense details and vendor's email address to the pop-up window -3. Upload a pdf/image of the bill -4. Click **Submit** - -# How to Pay Bills - -There are multiple ways to pay Bills in Expensify. Let’s go over each method below: - -## ACH bank-to-bank transfer - -To use this payment method, you must have a business bank account connected to your Expensify account. - -To pay with an ACH bank-to-bank transfer: - -1. Sign in to your Expensify account on the web at www.expensify.com. -2. Go to the Inbox or Reports page and locate the Bill that needs to be paid. -3. Click the **Pay** button to be redirected to the Bill. -4. Choose the ACH option from the drop-down list. -5. Follow the prompts to connect your business bank account to Expensify. - -**Fees:** None - -## Pay using a credit or debit card - -This option is available to all US and International customers receiving an bill from a US vendor with a US business bank account. - -To pay with a credit or debit card: -1. Sign-in to your Expensify account on the web app at www.expensify.com. -2, Click on the Bill you’d like to pay to see the details. -3, Click the **Pay** button. -4. You’ll be prompted to enter your credit card or debit card details. - -**Fees:** Includes 2.9% credit card payment fee - -## Venmo - -If both you and the vendor have Venmo setup in their Expensify account, you can opt to pay the bill through Venmo. - -**Fees:** Venmo charges a 3% sender’s fee - -## Pay Outside of Expensify - -If you are not able to pay using one of the above methods, then you can mark the Bill as paid manually in Expensify to update its status and indicate that you have made payment outside Expensify. - -To mark a Bill as paid outside of Expensify: - -1. Sign-in to your Expensify account on the web app at www.expensify.com. -2. Click on the Bill you’d like to pay to see the details. -3. Click on the **Reimburse** button. -4. Choose **I’ll do it manually** - -**Fees:** None - -# Deep Dive: How company bills and vendor invoices are processed in Expensify - -Here is how a vendor or supplier bill goes from received to paid in Expensify: - -1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy. -2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox. -3. The final approver pays the Bill from their Expensify account on the web via one of the methods. -4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy. - - -{% include faq-begin.md %} - -## What is my company's billing intake email? -Your billing intake email is [yourdomain.com]@expensify.cash. Example, if your domain is `company.io` your billing email is `company.io@expensify.cash`. - -## When a vendor or supplier bill is sent to Expensify, who receives it? - -Bills are received by the Primary Contact for the domain. This is the email address listed at **Settings > Domains > Domain Admins**. - -## Who can view a Bill in Expensify? - -Only the primary contact of the domain can view a Bill. - -## Who can pay a Bill? - -Only the primary domain contact (owner of the bill) will be able to pay the Mill. - -## How can you share access to Bills? - -To give others the ability to view a Bill, the primary contact can manually “share” the Bill under the Details section of the report via the Sharing Options button. -To give someone else the ability to pay Bills, the primary domain contact will need to grant those individuals Copilot access to the primary domain contact's account. - -## Is Bill Pay supported internationally? - -Payments are currently only supported for users paying in United States Dollars (USD). - -## What’s the difference between a Bill and an Invoice in Expensify? - -A Bill is a payable which represents an amount owed to a payee (usually a vendor or supplier), and is usually created from a vendor invoice. An Invoice is a receivable, and indicates an amount owed to you by someone else. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.md deleted file mode 100644 index a31c0a582fd7..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.md +++ /dev/null @@ -1,42 +0,0 @@ -# Overview - -If you want to know more about how and when you’ll be reimbursed through Expensify, we’ve answered your questions below. - -# How to Get Reimbursed - -To get paid back after submitting a report for reimbursement, you’ll want to be sure to connect your bank account. You can do that under **Settings** > **Account** > **Payments** > **Add a Deposit Account**. Once your employer has approved your report, the reimbursement will be paid into the account you added. - -# Deep Dive - -## Reimbursement Timing - -### US Bank Accounts - -If your company uses Expensify's ACH reimbursement we'll first check to see if the report is eligible for Rapid Reimbursement (next business day). For a report to be eligible for Rapid Reimbursement it must fall under two limits: - - - $100 per deposit bank account per day or less for the individuals being reimbursed or businesses receiving payments for bills. - - Less than $10,000 being disbursed in a 24-hour time period from the verified bank account being used to pay the reimbursement. - -If the request passes both checks, then you can expect to see funds deposited into your bank account on the next business day. - -If either limit has been reached, then you can expect to see funds deposited within your bank account within the typical ACH timeframe of 3-5 business days. - -### International Bank Accounts - -If receiving reimbursement to an international deposit account via Global Reimbursement, you should expect to see funds deposited in your bank account within 4 business days. - -## Bank Processing Timeframes - -Banks only process transactions and ACH activity on weekdays that are not bank holidays. These are considered business days. Additionally, the business day on which a transaction will be processed depends upon whether or not a request is created before or after the cutoff time, which is typically 3 pm PST. -For example, if your reimbursement is initiated at 4 pm on Wednesday, this is past the bank's cutoff time, and it will not begin processing until the next business day. -If that same reimbursement starts processing on Thursday, and it's estimated to take 3-5 business days, this will cover a weekend, and both days are not considered business days. So, assuming there are no bank holidays added into this mix, here is how that reimbursement timeline would play out: - -**Wednesday**: Reimbursement initiated after 3 pm PST; will be processed the next business day by your company’s bank. -**Thursday**: Your company's bank will begin processing the withdrawal request -**Friday**: Business day 1 -**Saturday**: Weekend -**Sunday**: Weekend -**Monday**: Business day 2 -**Tuesday**: Business day 3 -**Wednesday**: Business day 4 -**Thursday**: Business day 5 diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md deleted file mode 100644 index 69b39bae2874..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Reimbursing Reports -description: How to reimburse employee expense reports ---- -# Overview - -One essential aspect of the Expensify workflow is the ability to reimburse reports. This process allows for the reimbursement of expenses that have been submitted for review to the person who made the request. Detailed explanations of the various methods for reimbursing reports within Expensify are provided below. - -# How to reimburse reports - -Reports can be reimbursed directly within Expensify by clicking the **Reimburse** button at the top of the report to reveal the available reimbursement options. - -## Direct Deposit - -To reimburse directly in Expensify, the following needs to be already configured: -- The employee that's receiving reimbursement needs to add a deposit bank account to their Expensify account (under **Settings > Account > Payments > Add a Deposit-only Bank Account**) -- The reimburser needs to add a business bank account to Expensify (under **Settings > Account > Payments > Add a Verified Business Bank Account**) -- The reimburser needs to ensure Expensify is whitelisted to withdraw funds from the bank account - -If all of those settings are in place, to reimburse a report, you will click **Reimburse** on the report and then select **Via Direct Deposit (ACH)**. - -![Reimbursing Reports Dropdown]({{site.url}}/assets/images/Reimbursing Reports Dropdown.png){:width="100%"} - -## Indirect or Manual Reimbursement - -If you don't have the option to utilize direct reimbursement, you can choose to mark a report as reimbursed by clicking the **Reimburse** button at the top of the report and then selecting **I’ll do it manually – just mark as reimbursed**. - -This will effectively mark the report as reimbursed within Expensify, but you'll handle the payment elsewhere, outside of the platform. - -# Best Practices -- Plan ahead! Consider sharing a business bank account with multiple workspace admins so they can reimburse employee reports if you're unavailable. We recommend having at least two workspace admins with reimbursement permissions. - -- Understand there is a verification process when sharing a business bank account. The new reimburser will need access to the business bank account’s transaction history (or access to someone who has access to it) to verify the set of test transactions sent from Expensify. - -- Get into the routine of having every new employee connect a deposit-only bank account to their Expensify account. This will ensure reimbursements happen in a timely manner. - -- Employees can see the expected date of their reimbursement at the top of and in the comments section of their report. - -# How to cancel a reimbursement - -Reimbursed a report by mistake? No worries! Any workspace admin with access to the same Verified Bank Account can cancel the reimbursement from within the report until it is withdrawn from the payment account. - -**Steps to Cancel an ACH Reimbursement:** -1. On your web account, navigate to the Reports page -2. Open the report -3. Click **Cancel Reimbursement** -4. After the prompt, "Are you sure you want to cancel the reimbursement?" click **Cancel Reimbursement**. - -It's important to note that there is a small window of time (roughly less than 24 hours) when a reimbursement can be canceled. If you don't see the **Cancel Reimbursement** button on a report, this means your bank has already begun withdrawing the funds from the reimbursement account and the withdrawal cannot be canceled. - -In that case, you’ll want to contact your bank directly to see if they can cancel the reimbursement on their end - or manage the return of funds directly with your employee, outside of Expensify. - -If you cancel a reimbursement after the withdrawal has started, it will be automatically returned to your Verified Bank Account within 3-5 business days. - -# Deep Dive - -## Rapid Reimbursement -If your company uses Expensify's ACH reimbursement, we'll first check to see if the report is eligible for Rapid Reimbursement (next business day). For a report to be eligible for Rapid Reimbursement, it must fall under two limits: -- $100 per deposit only bank account per day for the individuals being reimbursed or businesses receiving payments for bills -- $10,000 per verified bank account for the company paying bills and reimbursing - -If neither limit is met, you can expect to see funds deposited into your bank account on the next business day. - -If either limit has been reached, you can expect funds deposited within your bank account within the typical ACH time frame of four to five business days. - -Rapid Reimbursement is not available for non-US-based reimbursement. If you are receiving a reimbursement to a non-US-based deposit account, you should expect to see the funds deposited in your bank account within four business days. - -{% include faq-begin.md %} - -## Who can reimburse reports? -Only a workspace admin who has added a verified business bank account to their Expensify account can reimburse employees. - -## Why can’t I trigger direct ACH reimbursements in bulk? - -Instead of a bulk reimbursement option, you can set up automatic reimbursement. With this configured, reports below a certain threshold (defined by you) will be automatically reimbursed via ACH as soon as they're "final approved." - -To set your manual reimbursement threshold, head to **Settings > Workspace > Group > _[Workspace Name]_ > Reimbursement > Manual Reimbursement**. - -![Manual Reimbursement]({{site.url}}/assets/images/Reimbursing Manual.png){:width="100%"} - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Cancel-an-ACH-Reimbursement.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Cancel-an-ACH-Reimbursement.md new file mode 100644 index 000000000000..ab75067b1a7f --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Cancel-an-ACH-Reimbursement.md @@ -0,0 +1,17 @@ +--- +title: Cancel an ACH reimbursement +description: Cancel an ACH payment after it has been sent +--- +
      + +If a report was reimbursed with an ACH payment by mistake or otherwise needs to be canceled, a Workspace Admin with access to the verified bank account can cancel the reimbursement up until it is withdrawn from the payment account. + +To cancel an ACH reimbursement, + +1. Click the **Reports** tab. +2. Open the report. +3. Click **Cancel Reimbursement**. + - If you don’t see the Cancel Reimbursement button, this means your bank has already begun transferring the funds and it cannot be canceled. In this case, you’ll need to contact your bank for cancellation. +4. Click **Cancel Reimbursement** to confirm cancellation. + +
      diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md new file mode 100644 index 000000000000..00fb236e1763 --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments.md @@ -0,0 +1,36 @@ +--- +title: Receive payments +description: Receive reimbursements from an employer +--- +
      + +To get paid after submitting a report for reimbursement, you must first connect a personal U.S. bank account or a personal Australian bank account. Then once your employer approves your report or invoice, the reimbursement will be paid directly to your bank account. +Funds for U.S. and global payments are generally deposited within a maximum of four to five business days: 2 days for the funds to be debited from the business bank account, and 2-3 business days for the ACH or wire to deposit into the employee account. + +However, banks only process ACH transactions before the daily cutoff (generally 3 p.m. PST) and only on business weekdays that are not bank holidays. This may affect when the payment is disbursed. If the payment qualifies for Rapid Reimbursement, you may receive the payment sooner. + +{% include info.html %} +Companies also have the option to submit payments outside of Expensify via check, cash, or a third-party payment processor. Check with your Workspace Admin to know how you will be reimbursed. +{% include end-info.html %} + +# Rapid Reimbursement (U.S. only) + +With Expensify’s ACH reimbursement, payments may be eligible for reimbursement by the next business day with Rapid Reimbursement if they meet the following qualifications: +- **Deposit-only accounts**: Payment must not exceed $100 +- **Verified business bank accounts**: The account does not disburse more than $10,000 within a 24-hour time period. + +If the payment amount exceeds the limit, funds will be deposited within the typical ACH time frame of four to five business days. + +{% include faq-begin.md %} + +**Is there a way I can track my payment?** + +For U.S. ACH payments and global reimbursements, the expected date of reimbursement is provided at the top of the report and in the comments section of the report. Funds will be deposited within the typical ACH time frame of four to five business days unless the payment is eligible for Rapid Reimbursement. + +**For global payments, what currency is the payment provided in?** + +Global payments are reimbursed in the recipient's currency. + +{% include faq-end.md %} + +
      diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Australian-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Australian-Reports.md new file mode 100644 index 000000000000..90a89ff3c75e --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Australian-Reports.md @@ -0,0 +1,63 @@ +--- +title: Reimburse Australian reports +description: Send payment for Australian expense reports +--- +
      + +Workspace Admins can reimburse AUD expense reports by downloading an .aba file containing the accounts needing payment and uploading the file to the bank. This can be done for a single report or for a batch of payments at once. + +{% include info.html %} +Your financial institution may require .aba files to include a self-balancing transaction. If you are unsure, check with your bank. Otherwise, the .aba file may not work with your bank’s internet banking platform. +{% include end-info.html %} + +# Reimburse a single report + +1. Open the report, invoice, or bill from the email or Concierge notification, or from the **Reports** tab. +2. Click the **Reimburse** dropdown and select **Via ABA File**. +3. Click **Generate ABA and Mark as Reimbursed**. +4. Click **Download**. +5. Upload the .aba file to your bank. For additional guidance, use any of the following bank guides: + - [ANZ Bank](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/) + - [CommBank](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) + - [Westpac](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/) + - [NAB](https://www.nab.com.au/business/online-banking/nab-connect/help) + - [Bendigo Bank](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf) + - [Bank of Queensland](https://www.boq.com.au/help-and-support/online-banking/ob-faqs-and-support/faq-pfuf) + +# Send batch payments + +Once employees submit their expense reports, a Workspace Admin exports the reports (which contains the employees’ bank account information) and uploads the .aba file to the bank. + +## Step 1: Verify currency & reimbursement settings + +1. Hover over **Settings**, then click **Workspaces**. +2. Select the desired workspace. +3. Click the **Reports** tab on the left. +4. Click the Report Currency dropdown and select **AUD A$**. +5. Click the **Reimbursement** tab on the left. +6. Verify that **Indirect** is selected as the Reimbursement type or select it if not. + +## Step 2: Download and upload the ABA file + +1. Click the **Reports** tab. +2. Use the checkbox on the left to select all the reports needing payment. +3. Click **Bulk Actions** and select **Reimburse via ABA**. +5. Click **Generate ABA and Mark as Reimbursed**. +6. Click **Download Report**. +7. Upload the .aba file to your bank. For additional guidance, use any of the following bank guides: + - [ANZ Bank](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/) + - [CommBank](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) + - [Westpac](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/) + - [NAB](https://www.nab.com.au/business/online-banking/nab-connect/help) + - [Bendigo Bank](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf) + - [Bank of Queensland](https://www.boq.com.au/help-and-support/online-banking/ob-faqs-and-support/faq-pfuf) + +{% include faq-begin.md %} + +**Can I use direct deposit for an AUD bank account?** + +No, AUD bank accounts do not rely on direct deposit or ACH. + +{% include faq-end.md %} + +
      diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills.md new file mode 100644 index 000000000000..b2cfbf833e13 --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills.md @@ -0,0 +1,108 @@ +--- +title: Reimburse reports, invoices, and bills +description: Use direct deposit or indirect reimbursement to pay reports, invoices, and bills +--- +
      + +Once a report, invoice, or bill has been submitted and approved for reimbursement, you can reimburse the expenses using direct deposit or an indirect reimbursement option outside of Expensify (like cash, a check, or a third-party payment processor). + +# Pay with direct deposit + +{% include info.html %} +Before a report can be reimbursed with direct deposit, the employee or vendor receiving the reimbursement must connect their personal U.S. bank account, and the reimburser must connect a verified business bank account. + +Direct deposit is available for U.S. and global reimbursements. It is not available for Australian bank accounts. For Australian accounts, review the process for reimbursing Australian expenses. +{% include end-info.html %} + +1. Open the report, invoice, or bill from the email or Concierge notification, or from the **Reports** tab. +2. Click the **Reimburse** (for reports) or **Pay** (for bills and invoices) dropdown and select **Via Direct Deposit (ACH)**. +3. Confirm that the correct VBA is selected or use the dropdown menu to select a different one. +4. Click **Accept Terms & Pay**. + +The reimbursement is now queued in the daily batch. + +# Pay with indirect reimbursement + +When payments are submitted through Expensify, the report is automatically labeled as Reimbursed after it has been paid. However, if you are reimbursing reports via paper check, payroll, or any other method that takes place outside of Expensify, you’ll want to manually mark the bill as paid in Expensify to track the payment history. + +To label a report as Reimbursed after sending a payment outside of Expensify, + +1. Pay the report, invoice, or bill outside of Expensify. +2. Open the report, invoice, or bill from the email or Concierge notification, or from the **Reports** tab. +3. Click **Reimburse**. +4. Select **I’ll do it manually - just mark it as reimbursed**. This changes the report status to Reimbursed. + +Once the recipient has received the payment, the submitter can return to the report and click **Confirm** at the top of the report. This will change the report status to Reimbursed: CONFIRMED. + +{% include faq-begin.md %} + +**Is there a maximum total report total?** + +Expensify cannot process a reimbursement for any single report over $20,000. If you have a report with expenses exceeding $20,000 we recommend splitting the expenses into multiple reports. + +**Why is my account locked?** + +When you reimburse a report, you authorize Expensify to withdraw the funds from your account and send them to the person requesting reimbursement. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved. + +Withdrawal requests can be rejected if the bank account has not been enabled for direct debit or due to insufficient funds. If you need to enable direct debits from your verified bank account, your bank will require the following details: +- The ACH CompanyIDs: 1270239450 and 4270239450 +- The ACH Originator Name: Expensify + +Once resolved, you can request to unlock the bank account by completing the following steps: + +1. Hover over **Settings**, then click **Account**. +2. Click the **Payments** tab. +3. Click **Bank Accounts**. +4. Next to the bank account, click **Fix**. + +Our support team will review and process the request within 4-5 business days. + +**How are bills and invoices processed in Expensify?** + +Here is the process a vendor or supplier bill goes through from receipt to payment: + +1. A vendor or supplier bill is received in Expensify. +2. Automatically, the document is SmartScanned and a bill is created for the primary domain contact. The bill will appear under the Reports tab on their default group policy. +3. When the bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow until the bill has been fully approved. +4. The final approver pays the bill from their Expensify account using one of the methods outlined in the article above. +5. If the workspace is connected to an accounting integration, the bill is automatically coded with the relevant imported GL codes and can be exported back to the accounting software. + +**When a vendor or supplier bill is sent to Expensify, who receives it?** + +Bills are sent to the primary contact for the domain. They’ll see a notification from Concierge on their Home page, and they’ll also receive an email. + +**How can I share access to bills?** + +By default, only the primary contact for the domain can view and pay the bill. However, you can allow someone else to view or pay bills. + +- **To allow someone to view a bill**: The primary contact can manually share the bill with others to allow them to view it. + 1. Click the **Reports** tab. + 2. Click the report. + 3. Click **Details** in the top right. + 4. Click the **Add Person** icon. + 5. Enter the email address or phone number of the person you will share the report with. + 6. Enter a message, if desired. + 7. Click **Share Report**. + +- **To allow someone to pay bills**: The primary domain contact can allow others to pay bills on their behalf by [assigning those individuals as Copilots](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot). + +**Is Bill Pay supported internationally?** + +Payments are currently only supported for users paying in United States Dollars (USD). + +**What’s the difference between a bill and an invoice?** + +- A **bill** is a payable that represents an amount owed to a payee (usually a vendor or supplier), and it is usually created from a vendor invoice. +- An **invoice** is a receivable that indicates an amount owed to you by someone else. + +**Who can reimburse reports?** + +Only a Workspace Admin who has added a verified business bank account to their Expensify account can reimburse employee reports. + +**Why can’t I trigger direct ACH reimbursements in bulk?** + +Expensify does not offer bulk reimbursement, but you can set up automatic reimbursement to automatically reimburse approved reports via ACH that do not exceed the threshold that you define. + +{% include faq-end.md %} + +
      diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md index 05366a91d9fa..f94e692f5e56 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md +++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md @@ -8,6 +8,15 @@ Whether you're encountering issues related to company cards, require assistance ## How to add company cards to Expensify You can add company credit cards under the Domain settings in your Expensify account by navigating to *Settings* > *Domain* > _Domain Name_ > *Company Cards* and clicking *Import Card/Bank* and following the prompts. +## To Locate Missing Card Transactions in Expensify +1. **Wait for Posting**: Bank transactions may take up to 24 hours to import into Expensify after they have "posted" at your bank. Ensure sufficient time has passed for transactions to appear. +2. **Update Company Cards**: Go to Settings > Domains > Company Cards. Click on the card in question and click "Update" to refresh the card feed. +3. **Reconcile Cards**: Navigate to the Reconciliation section under Settings > Domains > Company Cards. Refer to the detailed guide on how to use the [Reconciliation Dashboard](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation#identifying-outstanding-unapproved-expenses-using-the-reconciliation-dashboard). +4. **Review Transactions**: Use the Reconciliation Dashboard to view all transactions within a specific timeframe. Transactions will display on the Expenses page based on their "Posted Date". If needed, uncheck the "use posted date" checkbox near the filters to view transactions based on their "Transaction Date" instead. +5. **Address Gaps**: If there is a significant gap in transactions or if transactions are still missing, contact Expensify's Concierge or your Account Manager. They can initiate a historical data update on your card feed to ensure all transactions are properly imported. + +Following these steps should help you identify and resolve any issues with missing card transactions in Expensify. + ## Known issues importing transactions The first step should always be to "Update" your card, either from Settings > Your Account > Credit Card Import or Settings > Domain > [Domain Name] > Company Cards for centrally managed cards. If a "Fix" or "Fix card" option appears, follow the steps to fix the connection. If this fails to import your missing transactions, there is a known issue whereby some transactions will not import for certain API-based company card connections. So far this has been reported on American Express, Chase and Wells Fargo. This can be temporarily resolved by creating the expenses manually instead: @@ -63,6 +72,24 @@ If you've answered "yes" to any of these questions, a Domain Admins need to upda Make sure you're importing your card in the correct spot in Expensify and selecting the right bank connection. For company cards, use the master administrative credentials to import your set of cards at *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Import Card*. Please note there are some things that cannot be bypassed within Expensify, including two-factor authentication being enabled within your bank account. This will prevent the connection from remaining stable and will need to be turned off on the bank side. +## Why Can’t I See the Transactions Before a Certain Date? +When importing a card into Expensify, the platform typically retrieves 30-90 days of historical transactions, depending on the card or account type. For commercial feeds, transactions cannot be imported before the bank starts sending data. If needed, banks can send backdated files, and Expensify can run a historical update upon request. + +Additionally, Expensify does not import transactions dated before the "start date" you specify when assigning the card. Unless transitioning from an old card to a new one to avoid duplicates, it's advisable to set the start date to "earliest possible" or leave it blank. + +For historical expenses that cannot be imported automatically, consider using Expensify's [company card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import) or [personal card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards#importing-expenses-via-a-spreadsheet) spreadsheet import method. This allows you to manually input missing transactions into the system. + +## Why Am I / Why Is My Employee Seeing Duplicates? +If an employee is seeing duplicate expenses, they may have accidentally imported the card as a personal credit card as well as having the Domain Admin assign them a company card. + +To troubleshoot: +- Have the employee navigate to their Settings > Your Account > Credit Card Import and confirm that their card is only listed once. +- If the card is listed twice, delete the entry without the "padlock" icon. + +**Important:** Deleting a duplicate card will delete all unapproved expenses from that transaction feed. Transactions associated with the remaining card will not be affected. If receipts were attached to those transactions, they will still be on the Expenses page, and the employee can click to SmartScan them again. + +Duplicate expenses might also occur if you recently unassigned and reassigned a company card with an overlapping start date. If this is the case and expenses on the “new” copy have not been submitted, you can unassign the card again and reassign it with a more appropriate start date. This action will delete all unsubmitted expenses from the new card feed. + ## What are the most reliable bank connections in Expensify?* All bank connections listed below are extremely reliable, but we recommend transacting with the Expensify Visa® Commercial Card. It also offers daily and monthly settlement, unapproved expense limits, realtime compliance for secure and efficient spending, and cash back on all US purchases. [Click here to learn more about the Expensify Card](https://use.expensify.com/company-credit-card). diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md b/docs/articles/expensify-classic/connections/ADP.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/ADP.md rename to docs/articles/expensify-classic/connections/ADP.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md similarity index 100% rename from docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md rename to docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md b/docs/articles/expensify-classic/connections/Egencia.md similarity index 75% rename from docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md rename to docs/articles/expensify-classic/connections/Egencia.md index 178621a62d90..35d232d2df67 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md +++ b/docs/articles/expensify-classic/connections/Egencia.md @@ -24,7 +24,7 @@ Egencia controls the feed, so to connect Expensify you will need to: # How to Connect to a Central Purchasing Account Once your Egencia account manager has established the feed, you can automatically forward all Egencia booking receipts to a single Expensify account. To do this: 1. Open a chat with Concierge. -2. Tell Concierge “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”. +2. Tell Concierge the address of your central purchasing account, “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”. -The receipt the traveler receives is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports. The reservation sent to the traveler's account is added to their mobile app Trips feature so that the traveler can easily keep tabs on upcoming travel and receive trip notifications. +A receipt will be sent to both the traveler and the central account. The receipt sent to the traveler is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports. diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md b/docs/articles/expensify-classic/connections/Global-VaTax.md similarity index 100% rename from docs/articles/expensify-classic/integrations/travel-integrations/Global-VaTax.md rename to docs/articles/expensify-classic/connections/Global-VaTax.md diff --git a/docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md b/docs/articles/expensify-classic/connections/Google-Apps-SSO.md similarity index 100% rename from docs/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO.md rename to docs/articles/expensify-classic/connections/Google-Apps-SSO.md diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md b/docs/articles/expensify-classic/connections/Greenhouse.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md rename to docs/articles/expensify-classic/connections/Greenhouse.md diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md b/docs/articles/expensify-classic/connections/Gusto.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md rename to docs/articles/expensify-classic/connections/Gusto.md diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md b/docs/articles/expensify-classic/connections/Indirect-Accounting-Integrations.md similarity index 100% rename from docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md rename to docs/articles/expensify-classic/connections/Indirect-Accounting-Integrations.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md b/docs/articles/expensify-classic/connections/Lyft.md similarity index 100% rename from docs/articles/expensify-classic/integrations/travel-integrations/Lyft.md rename to docs/articles/expensify-classic/connections/Lyft.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md b/docs/articles/expensify-classic/connections/Navan.md similarity index 100% rename from docs/articles/expensify-classic/integrations/travel-integrations/Navan.md rename to docs/articles/expensify-classic/connections/Navan.md diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md b/docs/articles/expensify-classic/connections/QuickBooks-Time.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md rename to docs/articles/expensify-classic/connections/QuickBooks-Time.md diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md b/docs/articles/expensify-classic/connections/Rippling.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md rename to docs/articles/expensify-classic/connections/Rippling.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md b/docs/articles/expensify-classic/connections/TravelPerk.md similarity index 100% rename from docs/articles/expensify-classic/integrations/travel-integrations/TravelPerk.md rename to docs/articles/expensify-classic/connections/TravelPerk.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/connections/Uber.md similarity index 90% rename from docs/articles/expensify-classic/integrations/travel-integrations/Uber.md rename to docs/articles/expensify-classic/connections/Uber.md index 16da0c0caa5b..213a90b6d288 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/connections/Uber.md @@ -22,3 +22,5 @@ Now, every time you use Uber for Business – be it for rides or meals – the r ![Uber integration set up steps: Connecting your account](https://help.expensify.com/assets/images/Uber1.png){:width="100%"} ![Uber integration set up steps: Selecting Expensify](https://help.expensify.com/assets/images/Uber2.png){:width="100%"} + +To disconnect Uber and Expensify, simply follow the above path and select Disconnect on the Expensify option. diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Workday.md b/docs/articles/expensify-classic/connections/Workday.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/Workday.md rename to docs/articles/expensify-classic/connections/Workday.md diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md b/docs/articles/expensify-classic/connections/Zenefits.md similarity index 100% rename from docs/articles/expensify-classic/integrations/HR-integrations/Zenefits.md rename to docs/articles/expensify-classic/connections/Zenefits.md diff --git a/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md new file mode 100644 index 000000000000..fd0a6ca59069 --- /dev/null +++ b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md @@ -0,0 +1,7 @@ +--- +title: Accelo Troubleshooting +description: Accelo Troubleshooting +order: 3 +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/connections/accelo/Configure-Accelo.md b/docs/articles/expensify-classic/connections/accelo/Configure-Accelo.md new file mode 100644 index 000000000000..abb14767b196 --- /dev/null +++ b/docs/articles/expensify-classic/connections/accelo/Configure-Accelo.md @@ -0,0 +1,7 @@ +--- +title: Configure Accelo +description: Configure Accelo +order: 2 +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Accelo.md b/docs/articles/expensify-classic/connections/accelo/Connect-To-Accelo.md similarity index 99% rename from docs/articles/expensify-classic/integrations/accounting-integrations/Accelo.md rename to docs/articles/expensify-classic/connections/accelo/Connect-To-Accelo.md index fffe0abb43aa..96b0a17a1528 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Accelo.md +++ b/docs/articles/expensify-classic/connections/accelo/Connect-To-Accelo.md @@ -1,6 +1,7 @@ --- title: Accelo description: Help doc for Accelo integration +order: 1 --- diff --git a/docs/articles/expensify-classic/connections/certinia/Certinia-Troubleshooting.md b/docs/articles/expensify-classic/connections/certinia/Certinia-Troubleshooting.md new file mode 100644 index 000000000000..82a2762ee99a --- /dev/null +++ b/docs/articles/expensify-classic/connections/certinia/Certinia-Troubleshooting.md @@ -0,0 +1,6 @@ +--- +title: Certinia Troubleshooting +description: Certinia Troubleshooting +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/connections/certinia/Configure-Certinia.md b/docs/articles/expensify-classic/connections/certinia/Configure-Certinia.md new file mode 100644 index 000000000000..dc7398c11888 --- /dev/null +++ b/docs/articles/expensify-classic/connections/certinia/Configure-Certinia.md @@ -0,0 +1,7 @@ +--- +title: Configure Certinia +description: Configure Certinia +order: 2 +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md b/docs/articles/expensify-classic/connections/certinia/Connect-To-Certinia.md similarity index 99% rename from docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md rename to docs/articles/expensify-classic/connections/certinia/Connect-To-Certinia.md index 6c7014827ea6..5e5174a336b3 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md +++ b/docs/articles/expensify-classic/connections/certinia/Connect-To-Certinia.md @@ -1,6 +1,7 @@ --- title: Certinia description: Guide to connecting Expensify and Certinia FFA and PSA/SRP (formerly known as FinancialForce) +order: 1 --- # Overview [Cetinia](https://use.expensify.com/financialforce) (formerly known as FinancialForce) is a cloud-based software solution that provides a range of financial management and accounting applications built on the Salesforce platform. There are two versions: PSA/SRP and FFA and we support both. diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md similarity index 73% rename from docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md rename to docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index ee116f65a398..0650cf5b516f 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -1,136 +1,14 @@ --- -title: NetSuite -description: Connect and configure NetSuite directly to Expensify. +title: Configure Netsuite +description: Configure NetSuite's export, coding, and advanced settings. --- -# Overview -Expensify's seamless integration with NetSuite enables you to streamline your expense reporting process. This integration allows you to automate the export of reports, tailor your coding preferences, and tap into NetSuite's array of advanced features. By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient. -Before getting started with connecting NetSuite to Expensify, there are a few things to note: -- Token-based authentication works by ensuring that each request to NetSuite is accompanied by a signed token which is verified for authenticity -- You must be able to login to NetSuite as an administrator to initiate the connection -- You must have a Control Plan in Expensify to integrate with NetSuite -- Employees don’t need NetSuite access or a NetSuite license to submit expense reports since the connection is managed by the Workspace Admin -- Each NetSuite subsidiary will need its own Expensify Group Workspace -- Ensure that your workspace's report output currency setting matches the NetSuite Subsidiary default currency -- Make sure your page size is set to 1000 for importing your customers and vendors. Go to Setup > Integration > Web Services Preferences > 'Search Page Size' +By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient. -# How to Connect to NetSuite - -## Step 1: Install the Expensify Bundle in NetSuite - -1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for "Expensify" -2. Click on the Expensify Connect bundle (Bundle ID 283395) -3. Click Install -4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_ and update it to the latest version -5. Select **Show on Existing Custom Forms** for all available fields - -## Step 2: Enable Token-Based Authentication - -1. Head to _Setup > Company > Enable Features > SuiteCloud > Manage Authentication_ -2. Make sure “Token Based Authentication” is enabled -3. Click **Save** - -## Step 3: Add Expensify Integration Role to a User - -The user you select must have access to at least the permissions included in the Expensify Integration Role, but they're not required to be an Admin. -1. In NetSuite, head to Lists > Employees, and find the user you want to add the Expensify Integration role to -2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user -3. Click **Save** - -Remember that Tokens are linked to a User and a Role, not solely to a User. It's important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you've initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions. - -## Step 4: Create Access Tokens - -1. Using Global Search in NetSuite, enter “page: tokens” -2. Click **New Access Token** -3. Select Expensify as the application (this must be the original Expensify integration from the bundle) -4. Select the role Expensify Integration -5. Press **Save** -6. Copy and Paste the token and token ID to a saved location on your computer (this is the only time you will see these details) - -## Step 5: Confirm Expense Reports are Enabled in NetSuite. - -Enabling Expense Reports is required as part of Expensify's integration with NetSuite: -1. Logged into NetSuite as an administrator, go to Setup > Company > Enable Features > Employees -2. Confirm the checkbox next to Expense Reports is checked -3. If not, click the checkbox and then Save to enable Expense Reports - -## Step 6: Confirm Expense Categories are set up in NetSuite. - -Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are an alias for General Ledger accounts for coding expenses. - -1. Logged into NetSuite as an administrator, go to Setup > Accounting > Expense Categories (a list of Expense Categories should show) -2. If no Expense Categories are visible, click **New** to create new ones - -## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly - -1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_ -2. Click **Customize** or **Edit** next to the Standard Journal Entry form -3. Then, click Screen Fields > Main. Please verify the "Created From" label has "Show" checked and the Display Type is set to Normal -4. Click the sub-header Lines and verify that the "Show" column for "Receipt URL" is checked -5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the journal type have this same configuration - -## Step 8: Confirm Expense Report Transaction Forms are Configured Properly - -1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_ -2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main** -3. Verify the "Created From" label has "Show" checked and the Display Type is set to Normal -4. Click the second sub-header, Expenses, and verify that the 'Show' column for 'Receipt URL' is checked -5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the expense report type have this same configuration - -## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly - -1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_ -2. Click **Customize** or **Edit** next to your preferred Vendor Bill form -3. Then, click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked -4. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class -5. Go to _Customization > Forms > Transaction Forms_ and provide all other transaction forms with the vendor bill type have this same configuration - -## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly - -1. While logged in as an administrator, go to _Customization > Forms > Transaction Forms_ -2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked -3. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class -4. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the vendor credit type have this same configuration - -## Step 11: Set up Tax Groups (only applicable if tracking taxes) - -Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under _Setup > Accounting > Tax Groups_. - -Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify. - -Before importing NetSuite Tax Groups into Expensify: -1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_ -2. Click **New** -3. Select the country for your Tax Group -4. Enter the Tax Name (this is what employees will see in Expensify) -5. Select the subsidiary for this Tax Group -6. Select the Tax Code from the table you wish to include in this Tax Group -7. Click **Add** -8. Click **Save** -9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify - -Ensure Tax Groups can be applied to expenses by going to _Setup > Accounting > Set Up Taxes_ and setting the Tax Code Lists Include preference to "Tax Groups And Tax Codes" or "Tax Groups Only." - -If this field does not display, it’s not needed for that specific country. - -## Step 12: Connect Expensify and NetSuite - -1. Log into Expensify as a Policy Admin and go to **Settings > Workspaces > _[Workspace Name]_ > Connections > NetSuite** -2. Click **Connect to NetSuite** -3. Enter your Account ID (Account ID can be found in NetSuite by going to _Setup > Integration > Web Services Preferences_) -4. Then, enter the token and token secret -5. Click **Connect to NetSuite** - -From there, the NetSuite connection will sync, and the configuration dialogue box will appear. - -Please note that you must create the connection using a NetSuite account with the Expensify Integration role - -Once connected, all reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration). - -# How to Configure Export Settings +# Configure Export Settings There are numerous options for exporting Expensify reports to NetSuite. Let's explore how to configure these settings to align with your business needs. + To access these settings, head to **Settings > Workspace > Group > Connections** and select the **Configure** button. ## Export Options @@ -206,9 +84,9 @@ Select the Accounts Receivable account you want your Invoice Reports to export. ### Default Vendor Bills -The list of vendors will be available in the dropdown when selecting the option to export non-reimbursable expenses as vendor bills. +When selecting the option to export non-reimbursable expenses as vendor bills, the list of vendors will be available in the dropdown menu. -# How to Configure Coding Settings +# Configure Coding Settings The Coding tab is where NetSuite information is configured in Expensify, which allows employees to code expenses and reports accurately. There are several coding options in NetSuite. Let’s go over each of those below. @@ -313,7 +191,7 @@ If configuring Custom Segments as Report Fields, use the Field ID on the Transac If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under _Custom Segments > Transaction Columns_). Lastly, head over to Expensify and do the following: -1. Navigate to **Settings > Workspace > Group > [Workspace Name]_ > Connections > Configure > Coding tab** +1. Navigate to **Settings > Workspace > Group > [Workspace Name] > Connections > Configure > Coding tab** 2. Choose how to import Custom Records (Report Fields or Tags) 3. Fill out the three fields (the name or label of the record, Internal ID, Transaction Column ID) 4. Click **Submit** @@ -328,7 +206,7 @@ To add Custom Lists to your workspace, you’ll need to locate two fields in Net **To find the record:** 1. Log into Expensify -2. Head to **Settings > Workspace > Group > _[Workspace Name]_ > Connections > Configure > Coding tab** +2. Head to **Settings > Workspace > Group > [Workspace Name] > Connections > Configure > Coding tab** 3. The name of the record will be populated in a dropdown list The name of the record will populate in a dropdown list. If you don't see the one you are looking for, click **Refresh Custom List Options**. @@ -339,14 +217,14 @@ The name of the record will populate in a dropdown list. If you don't see the on 3. Open the option that is holding the record to get the ID Lastly, head over to Expensify, and do the following: -1. Navigate to **Settings > Workspaces > Group > _[Workspace Name]_ > Connections > Configure > Coding tab** +1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections > Configure > Coding tab** 2. Choose how to import Custom Lists (Report Fields or Tags) 3. Enter the ID in Expensify in the configuration screen 4. Click **Submit** From there, you should see the values for the Custom Lists under the Tag or Report Field settings in Expensify. -# How to Configure Advanced Settings +# Configure Advanced Settings The NetSuite integration’s advanced configuration settings are accessed under **Settings > Workspaces > Group > _[Workspace Name]_ > Connections > NetSuite > Configure > Advanced tab**. @@ -384,13 +262,13 @@ Besides inviting employees, you can also establish an approval process in NetSui By doing this, the Approval Workflow in Expensify will automatically follow the same rules as NetSuite, typically starting with Manager Approval. -- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. +- **Basic Approval:** This is a single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. - **Manager Approval (default):** Two levels of approval route reports first to an employee's NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page. - **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace's People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > _[Workspace Name]_ > People page**. You can set a user role for each new employee and enforce an approval workflow. ## Automatically Create Employees/Vendors -With this feature enabled, Expensify will automatically create a new employee or vendor (if one doesn’t already exist) from the email of the report submitter in NetSuite. +With this feature enabled, Expensify will automatically create a new employee or vendor (if one doesn’t already exist) from the report submitter's email in NetSuite. ## Export Foreign Currency Amount @@ -430,13 +308,12 @@ If you do not wish to use Approval Routing in NetSuite, go to _Setup > Accountin When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting. -# Deep Dive - +# Additional Settings ## Categories You can use the Auto-Categorization feature so that expenses are automatically categorized. -To set Category Rules (e.g., receipt requirements or comments), go to the categories page in the workspace under **Settings > Workspaces > _[Workspace Name]_ > Categories**. +To set Category Rules (e.g., receipt requirements or comments), go to the categories page in the workspace under **Settings > Workspaces > [Workspace Name] > Categories**. With this setting enabled, when an Expense Category updates in NetSuite, it will update in Expensify automatically. @@ -448,7 +325,7 @@ NetSuite's company card feature simplifies exporting reimbursable and non-reimbu 2. **Default Accounts Payable (A/P) Account:** Expense reports enable you to set a default A/P account for export on your subsidiary record. Unlike vendor bills, where the A/P account defaults to the last selected account, the expense report export option allows you to establish a default A/P account. 3. **Mix Reimbursable and Non-Reimbursable Expenses:** You can freely mix reimbursable and non-reimbursable expenses without categorizing them in NetSuite after export. NetSuite's corporate card feature automatically categorizes expenses into the correct GL accounts, ensuring a neat and organized GL impact. -#### Let’s go over an example! +**Let’s go over an example!** Consider an expense report with one reimbursable and one non-reimbursable expense. Each needs to be exported to different accounts and expense categories. @@ -456,7 +333,7 @@ In NetSuite, you can quickly identify the non-reimbursable expense marked as a c Furthermore, each expense is categorized according to your selected expense category. -You'll need to set up default corporate cards in NetSuite to use the expense report option for your corporate card expenses. +To use the expense report option for your corporate card expenses, you'll need to set up default corporate cards in NetSuite. For non-reimbursable expenses, choose the appropriate card on the subsidiary record. You can find the default in your accounting preferences if you're not using a OneWorld account. @@ -545,7 +422,7 @@ When exporting to NetSuite, we match the recipient's email address on the invoic Once exported, the invoice will appear in the Accounts Receivable account you selected during your NetSuite Export configuration. -### Updating an Invoice to paid +### Updating the status of an invoice to "paid" When you mark an invoice as "Paid" in Expensify, this status will automatically update in NetSuite. Similarly, if the invoice is marked as "Paid" in NetSuite, it will sync with Expensify. The payment will be reflected in the Collection account specified in your Advanced Settings Configuration. @@ -562,10 +439,6 @@ Send these two files to your Account Manager or Concierge so we can continue tro {% include faq-begin.md %} -## What type of Expensify plan is required for connecting to NetSuite? - -You need a group workspace on a Control Plan to integrate with NetSuite. - ## How does Auto Sync work with reimbursed reports? If a report is reimbursed via ACH or marked as reimbursed in Expensify and then exported to NetSuite, the report is automatically marked as paid in NetSuite during the next sync. @@ -574,6 +447,24 @@ If a report is exported to NetSuite and then marked as paid in NetSuite, the rep ## If I enable Auto Sync, what happens to existing approved and reimbursed reports? -If you previously had Auto Sync disabled but want to allow that feature to be used going forward, you can safely turn on Auto Sync without affecting existing reports. Auto Sync will only take effect for reports created after enabling that feature. +If you previously had Auto Sync disabled but want to allow that feature to be used going forward, you can safely turn it on without affecting existing reports. Auto Sync will only take effect for reports created after enabling that feature. + +## Why are some of my customers not importing from NetSuite? + +If only part of your customer list is importing from NetSuite to Expensify, ensure your page size is set to 1000 for importing customers and vendors: +1. Navigate to **Setup > Integration > Web Services Preferences > Search Page Size** +2. Adjust this setting to 1000 +3. Sync your connection again under **Settings > Workspaces > Group > Workspace Name > Connections** + +Additionally, ensure the "Company Name" field is completed for each customer profile; otherwise, they won't import into the Group Workspace. + +## Why aren't all my Categories pulling into Expensify from NetSuite? + +If you're having trouble importing your Categories, you'll want to start by checking that they are set up in NetSuite as actual Expense Categories, not General Ledger accounts: +- Log into NetSuite as an administrator and go to **Setup > Accounting > Expense Categories** +- A list of Expense Categories should be available +- If no Expense Categories are visible click on "New" to create new Expense Categories + +If you have confirmed that your categories are set as Expense Categories in NetSuite and they still aren't importing to Expensify, make sure that the subsidiary of the Expense Category matches the subsidiary selected in your connection settings. {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md b/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md new file mode 100644 index 000000000000..1f96d9b8a633 --- /dev/null +++ b/docs/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite.md @@ -0,0 +1,164 @@ +--- +title: NetSuite +description: Set up the direct connection from Expensify to NetSuite. +order: 1 +--- +# Overview +Expensify's integration with NetSuite allows you to automate report exports, tailor your coding preferences, and tap into NetSuite's array of advanced features. By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient. + +**Before connecting NetSuite to Expensify, a few things to note:** +- Token-based authentication works by ensuring that each request to NetSuite is accompanied by a signed token which is verified for authenticity +- You must be able to login to NetSuite as an administrator to initiate the connection +- You must have a Control Plan in Expensify to integrate with NetSuite +- Employees don’t need NetSuite access or a NetSuite license to submit expense reports since the connection is managed by the Workspace Admin +- Each NetSuite subsidiary will need its own Expensify Group Workspace +- Ensure that your workspace's report output currency setting matches the NetSuite Subsidiary default currency +- Make sure your page size is set to 1000 for importing your customers and vendors. You can check this in NetSuite under **Setup > Integration > Web Services Preferences > 'Search Page Size'** + +# Connect to NetSuite + +## Step 1: Install the Expensify Bundle in NetSuite + +1. While logged into NetSuite as an administrator, go to Customization > SuiteBundler > Search & Install Bundles, then search for "Expensify" +2. Click on the Expensify Connect bundle (Bundle ID 283395) +3. Click Install +4. If you already have the Expensify Connect bundle installed, head to _Customization > SuiteBundler > Search & Install Bundles > List_ and update it to the latest version +5. Select **Show on Existing Custom Forms** for all available fields + +## Step 2: Enable Token-Based Authentication + +1. Head to _Setup > Company > Enable Features > SuiteCloud > Manage Authentication_ +2. Make sure “Token Based Authentication” is enabled +3. Click **Save** + +## Step 3: Add Expensify Integration Role to a User + +The user you select must have access to at least the permissions included in the Expensify Integration Role, but they're not required to be an Admin. +1. In NetSuite, head to Lists > Employees, and find the user you want to add the Expensify Integration role to +2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user +3. Click **Save** + +Remember that Tokens are linked to a User and a Role, not solely to a User. It's important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you've initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions. + +## Step 4: Create Access Tokens + +1. Using Global Search in NetSuite, enter “page: tokens” +2. Click **New Access Token** +3. Select Expensify as the application (this must be the original Expensify integration from the bundle) +4. Select the role Expensify Integration +5. Press **Save** +6. Copy and Paste the token and token ID to a saved location on your computer (this is the only time you will see these details) + +## Step 5: Confirm Expense Reports are Enabled in NetSuite. + +Enabling Expense Reports is required as part of Expensify's integration with NetSuite: +1. Logged into NetSuite as an administrator, go to Setup > Company > Enable Features > Employees +2. Confirm the checkbox next to Expense Reports is checked +3. If not, click the checkbox and then Save to enable Expense Reports + +## Step 6: Confirm Expense Categories are set up in NetSuite. + +Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are an alias for General Ledger accounts used to code expenses. + +1. Logged into NetSuite as an administrator, go to Setup > Accounting > Expense Categories (a list of Expense Categories should show) +2. If no Expense Categories are visible, click **New** to create new ones + +## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly + +1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_ +2. Click **Customize** or **Edit** next to the Standard Journal Entry form +3. Then, click Screen Fields > Main. Please verify the "Created From" label has "Show" checked and the Display Type is set to Normal +4. Click the sub-header Lines and verify that the "Show" column for "Receipt URL" is checked +5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the journal type have this same configuration + +## Step 8: Confirm Expense Report Transaction Forms are Configured Properly + +1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_ +2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main** +3. Verify the "Created From" label has "Show" checked and the Display Type is set to Normal +4. Click the second sub-header, Expenses, and verify that the 'Show' column for 'Receipt URL' is checked +5. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the expense report type have this same configuration + +## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly + +1. Logged into NetSuite as an administrator, go to _Customization > Forms > Transaction Forms_ +2. Click **Customize** or **Edit** next to your preferred Vendor Bill form +3. Then, click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked +4. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class +5. Go to _Customization > Forms > Transaction Forms_ and provide all other transaction forms with the vendor bill type have this same configuration + +## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly + +1. While logged in as an administrator, go to _Customization > Forms > Transaction Forms_ +2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click _Screen Fields > Main_ and verify that the "Created From" label has "Show" checked and that Departments, Classes, and Locations have the "Show" label unchecked +3. Under the Expenses sub-header (make sure to click the "Expenses" sub-header at the very bottom and not "Expenses & Items"), ensure "Show" is checked for Receipt URL, Department, Location, and Class +4. Go to _Customization > Forms > Transaction Forms_ and ensure all other transaction forms with the vendor credit type have this same configuration + +## Step 11: Set up Tax Groups (only applicable if tracking taxes) + +Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under _Setup > Accounting > Tax Groups_. + +Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify. + +Before importing NetSuite Tax Groups into Expensify: +1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_ +2. Click **New** +3. Select the country for your Tax Group +4. Enter the Tax Name (this is what employees will see in Expensify) +5. Select the subsidiary for this Tax Group +6. Select the Tax Code from the table you wish to include in this Tax Group +7. Click **Add** +8. Click **Save** +9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify + +Ensure Tax Groups can be applied to expenses by going to _Setup > Accounting > Set Up Taxes_ and setting the Tax Code Lists Include preference to "Tax Groups And Tax Codes" or "Tax Groups Only." + +If this field does not display, it’s not needed for that specific country. + +## Step 12: Connect Expensify and NetSuite + +1. Log into Expensify as a Policy Admin and go to **Settings > Workspaces > _[Workspace Name]_ > Connections > NetSuite** +2. Click **Connect to NetSuite** +3. Enter your Account ID (Account ID can be found in NetSuite by going to _Setup > Integration > Web Services Preferences_) +4. Then, enter the token and token secret +5. Click **Connect to NetSuite** + +From there, the NetSuite connection will sync, and the configuration dialogue box will appear. + +Please note that you must create the connection using a NetSuite account with the Expensify Integration role + +Once connected, all reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration). + +{% include faq-begin.md %} + +## Can negative expenses be exported to NetSuite? +You can export reports with a negative total to NetSuite by selecting “Vendor Bill” as your export option. When a report total is negative, we’ll create a Vendor Credit in NetSuite instead of a bill. + +**Important**: Only enable this if you pay your employees/vendors outside of Expensify. A Vendor Credit reduces the total amount payable in NetSuite, but not in Expensify. + +To use this feature, make sure you have configured your Vendor Credit transaction form in NetSuite and are using the latest version of the Expensify bundle (version 1.4). If you need to update, go to **Customization > SuiteBundler > Search & Install Bundles > List** and click **Update** next to **Expensify Connect**. + +## How do you switch the owner of the connection between NetSuite and Expensify? + +Follow the steps below to transfer ownership of the NetSuite connection to someone else: +1. Head to **Settings > Workspaces > Workspace Name > Connections > NetSuite** +2. Click **Configure** to review and save the settings for future reference +3. Select **Do not connect to NetSuite** +4. Select **Connect to NetSuite** +5. Enter the email address of the new admin who will take over as the NetSuite User ID +6. Enter the NetSuite Account ID (found in NetSuite under **Setup > Integration > Web Services Preferences**) +7. Click **Create a new NetSuite Connection** +8. Confirm completion of prerequisites and proceed by clicking Continue +9. You will be redirected to the NetSuite SSO page, where you will enter the email address of the new connection owner and the NetSuite password for that account +10. Once redirected to the NetSuite page, click **View all roles** and ensure you are logged in under the Administrator role +11. After confirmation, sign out +12. Return to Expensify to reconfigure the sync and export settings on the updated connection +13. Click **Save** + +**If you run into any issues updating the connection, follow these additional troubleshooting steps:** +- In NetSuite, access the role of the current connection owner +- Click Edit > Access > Choose any role other than Administrator > Save +- Click Edit > Access > Select Administrator role > Save +- Repeat the steps outlined above + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md b/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md new file mode 100644 index 000000000000..bd1f424a2dd3 --- /dev/null +++ b/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md @@ -0,0 +1,466 @@ +--- +title: Netsuite Troubleshooting +description: Troubleshoot common NetSuite sync and export errors. +--- + +# Overview of NetSuite Troubleshooting +Synchronizing and exporting data between Expensify and NetSuite can streamline your financial processes, but occasionally, users may encounter errors that prevent a smooth integration. These errors often arise from discrepancies in settings, missing data, or configuration issues within NetSuite or Expensify. + +This troubleshooting guide aims to help you identify and resolve common sync and export errors, ensuring a seamless connection between your financial management systems. By following the step-by-step solutions provided for each specific error, you can quickly address issues and maintain accurate and efficient expense reporting and data management. + +# ExpensiError NS0005: Please enter value(s) for Department, Location or Class + +This error occurs when the classification (like Location) is required at the header level of your transaction form in NetSuite. + +For expense reports and journal entries, NetSuite uses classifications from the employee record default. Expensify only exports this information at the line item level. + +For vendor bills, these classifications can't be mandatory because we use the vendor record instead of the employee record, and vendor records don’t have default classifications. + +**Vendor Bills:** +When exporting as a Vendor Bill, we pull from the vendor record, not the employee. Therefore, employee defaults don’t apply at the header ("main") level. This error appears if your NetSuite transaction form requires those fields. + +## How to Fix ExpensiError NS0005 +1. Go to **Customization > Forms > Transaction Forms**. +2. Click **"Edit"** on your preferred vendor bill form. +3. Go to **Screen Fields > Main**. +4. Uncheck both **"Show"** and **"Mandatory"** for the listed fields in your error message. +5. Sync the workspace connection in Expensify (**Settings > Workspaces > Workspace Name > Connections > Sync**). +6. Attempt the export again. + +**Journal Entries and Expense Reports:** +If you see this error when exporting a Journal Entry or Expense Report, it might be because the report submitter doesn’t have default settings for Departments, Classes, or Locations. + +**To fix this:** +1. Go to **Lists > Employees** in NetSuite. +2. Click **"Edit"** next to the employee's name who submitted the report. +3. Scroll down to the **Classification** section. +4. Select a default **Department**, **Class**, and **Location** for the employee. +5. Click **Save**. +6. Sync your NetSuite connection in Expensify. + + +# ExpensiError NS0012: Currency Does Not Exist In NetSuite + +**Scenario One:** When dealing with foreign transactions, Expensify sends the conversion rate and currency of the original expense to NetSuite. If the currency isn't listed in your NetSuite subsidiary, you'll see an error message saying the currency does not exist in NetSuite. + +**To fix this:** +1. Ensure the currency in Expensify matches what's in your NetSuite subsidiary. +2. If you see an error saying 'The currency X does not exist in NetSuite', re-sync your connection to NetSuite through the workspace admin section in Expensify. +3. Try exporting again. + +**Scenario Two:** This error can happen if you’re using a non-OneWorld NetSuite instance and exporting a currency other than EUR, GBP, USD, or CAD. + +**To fix this:** +1. Head to NetSuite. +2. Go to **Setup > Enable Features**. +3. Check the **Multiple Currencies** box. + +Once you've done this, you can add the offending currency by searching **New Currencies** in the NetSuite global search. + + +# ExpensiError NS0021: Invalid tax code reference key + +This error usually indicates an issue with the Tax Group settings in NetSuite, which can arise from several sources. + +#### Tax Group to Tax Code Mapping +If a Tax Code on Sales Transactions is mapped to a Tax Group, an error will occur. To fix this, the Tax Code must be mapped to a Tax Code on Purchase Transactions instead. + +To verify if a Tax Code is for Sales or Purchase transactions, view the relevant Tax Code(s). + +**For Australian Taxes:** +Ensure your Tax Groups are mapped correctly: +- **GST 10%** to **NCT-AU** (not the Sales Transaction Tax Code TS-AU) +- **No GST 0%** to **NCF-AU** (not the Sales Transaction Tax Code TFS-AU) + +#### Tax Group Type +Tax Groups can represent different types of taxes. For compatibility with Expensify, ensure the tax type is set to GST/VAT. + +#### Enable Tax Groups +Some subsidiaries require you to enable Tax Groups. Go to **Set Up Taxes** for the subsidiary's country and ensure the Tax Code lists include both Tax Codes and Tax Groups. + + +# ExpensiError NS0023: Employee Does Not Exist in NetSuite (Invalid Employee) + +This can happen if the employee’s subsidiary in NetSuite doesn’t match what’s listed in Expensify. + +## How to Fix ExpensiError NS0023 +1. **Check the Employee's Subsidiary** + - Go to the employee record in NetSuite. + - Confirm the employee's subsidiary matches what’s listed as the subsidiary at the workspace level. + - To find this in Expensify navigate to **Settings > Workspaces > Workspace Name > Connections > Configure**. + - If the subsidiaries don’t match, update the subsidiary in Expensify to match what’s listed in NetSuite. + - Sync the NetSuite connection in Expensify. +2. **Verify Access Restrictions:** + - Go to **Lists > Employees > Employees > [Select Employee] > Edit > Access**. + - Uncheck **Restrict Access to Expensify**. +3. **Additional Checks:** + - Ensure the email on the employee record in NetSuite matches the email address of the report submitter in Expensify. + - In NetSuite, make sure the employee's hire date is in the past and/or the termination date is in the future. +4. **Currency Match for Journal Entries:** + - If exporting as Journal Entries, ensure the currency for the NetSuite employee record, NetSuite subsidiary, and Expensify policy all match. + - In NetSuite, go to the **Human Resources** tab > **Expense Report Currencies**, and add the subsidiary/policy currency if necessary. + + +# ExpensiError NS0024: Invalid Customer or Project Tag + +Employees must be listed as a resource on the customer/project in NetSuite to be able to apply it to an expense. If that isn’t set up in NetSuite, you can run into this error. + +## How to Fix ExpensiError NS0024 + +1. **Ensure Employee Access:** + - In NetSuite, go to **Lists > Relationships > Customer/Projects**. + - Click **Edit** next to the desired Customer/Project. + - Click **Resources**, select the Employee from the drop-down menu, click **Add**, then **Save** your changes. +2. **Sync with Expensify:** + - In Expensify, go to **Settings > Workspaces > [Workspace Name] > NetSuite > Sync Now**. + - Attempt to export again. +3. **Remove Restriction for a Specific Customer/Project (Optional):** + - In NetSuite, edit the customer/project. + - Go to **Preferences**, then uncheck **Limit time and expenses to resources**. +4. **Remove Restrictions for All Projects:** + - In NetSuite, go to **Setup > Accounting > Accounting Preferences > Time & Expenses**. + - Uncheck **Show Projects Only For Time And Expense Entry**. +5. **Enable Cross-Subsidiary Customers/Projects in Expensify (Optional):** + - Go to **Settings > Workspaces > Group > [Workspace Name] > Connections > NetSuite > Configure > Advanced**. + - Enable **Cross-Subsidiary Customers/Projects** to remove the requirement for the employee's subsidiary and the customer's subsidiary to match. + + +# ExpensiError NS0034: This record already exists + +This error occurs when the report in question was already exported to NetSuite. + +## How to fix ExpensiError NS0034 +1. **Check for the existing Report in NetSuite:** + - If the report already exists in NetSuite but has been edited in Expensify, you need to delete the report in NetSuite. +2. **Find the Report ID in Expensify:** + - Locate the Report ID in Expensify under the Details section in the top right corner of the report. +3. **Search for the Report ID in NetSuite:** + - Use the Global Search Box in NetSuite to find the report using the Report ID. +4. **Delete the Report in NetSuite:** + - In NetSuite, click _**Edit > Actions > Delete**_ to remove the report. +5. **Re-export the Report from Expensify to NetSuite:** + - After deleting the report in NetSuite, re-export it from Expensify to NetSuite. + + +# ExpensiError NS0046: Billable Expenses Not Coded with a NetSuite Customer or Billable Project + +NetSuite requires billable expenses to be assigned to a Customer or a Project that is configured as billable to a Customer. If this is not set up correctly in NetSuite, this error can occur. + +## How to Fix ExpensiError NS0046 +1. **Access the Report in Expensify:** + - Go to the Expensify website and open the report. +2. Click the **Details** icon in the upper-left corner of the report. +3. Select the following options (this allows you to easily see which expenses are flagged as billable but don't have a valid customer tagged): + - **View:** Details + - **Group by:** Tag + - **Split report by:** Billable +4. **Check Billable Expenses:** + - Click on each expense in the Billable section of the report. + - Ensure that the Customer or Project tag field is present. + - Verify that there are no violations and that a value has been applied to the field. +5. Make any necessary adjustments to the billable expenses and try the export again. + + +# ExpensiError NS0059: Elimination Settings for X Do Not Match + +This error occurs when an Intercompany Payable account is set as the default in the Default Payable Account field in the NetSuite subsidiary preferences, and the Accounting Approval option is enabled for Expense Reports. + +## How to Fix ExpensiError NS0059 +Set the Default Payable Account for Expense Reports on each subsidiary in NetSuite to ensure the correct payable account is active. +1. Navigate to Subsidiaries: + - Go to Setup > Company > Subsidiaries. +2. Edit Subsidiary Preferences: + - Click Edit for the desired subsidiary. + - Go to the Preferences tab. +3. Set Default Payable Account: + - Choose the preferred account for Default Payable Account for Expense Reports. + +Repeat these steps for each subsidiary to ensure the settings are correct, and then sync Expensify to NetSuite to update the connection. + + +# ExpensiError NS0059: A credit card account has not been selected for corporate card expenses. + +**To resolve this error:** +1. Log into NetSuite as an admin. +2. Type "Page: Subsidiaries" in the global search box and select the subsidiary you will export to. +3. Under the Preferences tab of the subsidiary, locate the field: Default Account for Corporate Card Expenses. + +**If you want to assign different cards to different employees:** +1. Go to each employee record in NetSuite. +2. Under the Human Resources > Expense and Purchasing section, find the field: Default Account for Corporate Card Expenses. + +**For reports containing cash expenses that are not marked as Reimbursable:** +1. Have the approver reject the report. +2. Mark the expenses as Reimbursable. +3. Re-submit the report, approve it, and try to export again. + +For accounts without subsidiaries (non-OneWorld accounts), the default field is in your accounting preferences. + + +# ExpensiError NS0085: Expense Does Not Have Appropriate Permissions for Settings an Exchange Rate in NetSuite + +This error occurs when the exchange rate settings in NetSuite aren't updated correctly. + +## How to Fix ExpensiError NS0085 +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Search for the form type that the report is being exported as (Expense Report, Journal Entry, or Vendor Bill) and click Edit next to the form that has the Preferred checkbox checked. + - **For Expense Reports:** + - Go to Screen Fields > Expenses (the Expenses tab farthest to the right). + - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. + - **For Vendor Bills:** + - Go to Screen Fields > Main. + - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. + - **For Journal Entries:** + - Go to Screen Fields > Lines. + - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. + - Go to Screen Fields > Main and ensure the Show checkbox is checked in the Exchange Rate field under the Description column. +3. Go to **Settings > Workspaces > Group > [Workspace Name] > Connections**. +4. Click Sync Now to sync the NetSuite connection. +5. Export the report(s) again. + + +# ExpensiError NS0079: The Transaction Date is Not Within the Date Range of Your Accounting Period + +The transaction date you specified is not within the date range of your accounting period. When the posting period settings in NetSuite are not configured to allow a transaction date outside the posting period, you can't export a report to the next open period, which is why you’ll run into this error. + +## How to Fix ExpensiError NS0079 +1. In NetSuite, navigate to Setup > Accounting > Accounting Preferences. +2. Under the General Ledger section, ensure the field Allow Transaction Date Outside of the Posting Period is set to Warn. +3. Then, choose whether to export your reports to the First Open Period or the Current Period. + +Additionally, ensure the Export to Next Open Period feature is enabled within Expensify: +1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections > Configure**. +2. Open the **Advanced tab**. +3. Confirm that the setting for **Export to Next Open Period** is enabled. + +If any configuration settings are updated on the NetSuite connection, be sure to sync the connection before trying the export again. + + +# ExpensiError NS0055: The Vendor You are Trying to Export to Does Not Have Access to the Currency X + +This error occurs when a vendor tied to a report in Expensify does not have access to a currency on the report in NetSuite. The vendor used in NetSuite depends on the type of expenses on the report you're exporting. +- For **reimbursable** (out-of-pocket) expenses, this is the report's submitter (the employee who submitted the report). +- For **non-reimbursable** (e.g., company card) expenses, this is the default vendor set via Settings > Workspaces > Group > [Workspace Name] > Connections > NetSuite > Configure. + +## How to Fix ExpensiError NS0055 +To fix this, the vendor needs to be given access to the applicable currency: +1. In NetSuite, navigate to Lists > Relationships > Vendors to access the list of Vendors. +2. Click Edit next to the Vendor tied to the report: + - For reimbursable (out-of-pocket) expenses, this is the report's submitter. + - For non-reimbursable (e.g., company card) expenses, this is the default vendor set via **Settings > Workspaces > Group > [Workspace Name] > Connections > NetSuite > Configure**. +3. Navigate to the Financial tab. +4. Scroll down to the Currencies section and add all the currencies that are on the report you are trying to export. +5. Click Save. + + +# ExpensiError NS0068: You do not have permission to set a value for element - “Created From” + +**To resolve this error:** +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, Vendor Bill, or if the report total is negative, Vendor Credit). +3. Click Edit next to the form that has the Preferred checkbox checked. +4. Go to Screen Fields > Main and ensure the field Created From has the Show checkbox checked. +5. Sync the NetSuite connection under **Settings > Workspaces > Group > [Workspace Name] > Connections > Sync Now**. +6. Export the report(s) again. + +#### For reports with Expensify Card expenses +Expensify Card expenses export as Journal Entries. If you encounter this error when exporting a report with Expensify Card non-reimbursable expenses, ensure the field Created From has the Show checkbox checked for Journal Entries in NetSuite. + + +# ExpensiError NS0037: You do not have permission to set a value for element - “Receipt URL” + +**To resolve this error:** +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, or Vendor Bill). +3. Click Edit next to the form that has the Preferred checkbox checked. + - If the report is being exported as an Expense Report: + - Go to Screen Fields > Expenses (the Expenses tab farthest to the right). + - Ensure the field ReceiptURL has the Show checkbox checked. + - If the report is being exported as a Journal Entry: + - Go to Screen Fields > Lines. + - Ensure the field ReceiptURL has the Show checkbox checked. + - If the report is being exported as a Vendor Bill: + - Go to Screen Fields > Main. + - Ensure the field ReceiptURL has the Show checkbox checked. +4. Click Sync Now to sync the NetSuite connection at **Settings > Workspaces > Group > Workspace Name > Connections**. +5. Export the report(s) again. + + +# ExpensiError NS0042: Error creating vendor - this entity already exists + +This error occurs when a vendor record already exists in NetSuite, but Expensify is still attempting to create a new one. This typically means that Expensify cannot find the existing vendor during export. +- The vendor record already exists in NetSuite, but there may be discrepancies preventing Expensify from recognizing it. +- The email on the NetSuite vendor record does not match the email of the report submitter in Expensify. +- The vendor record might not be associated with the correct subsidiary in NetSuite. + +## How to Fix ExpensiError NS0042 + +Follow these steps to resolve the issue: +1. **Check Email Matching:** + - Ensure the email on the NetSuite vendor record matches the email of the report submitter in Expensify. +2. **Check Subsidiary Association:** + - Ensure the vendor record is associated with the same subsidiary selected in the connection configurations + - You can review this under **Settings > Workspaces > Group > Workspace Name > Connections > Configure**. +3. **Automatic Vendor Creation:** + - If you want Expensify to automatically create vendors, ensure the "Automatically Create Vendor" option is enabled under **Settings > Workspaces > Group > Workspace Name > Connections > Advanced**. + +**Options to Resolve the Error:** +- **Edit the Existing Vendor:** Update the existing vendor record in NetSuite to match the report submitter's email and name. +- **Delete the Existing Vendor:** If appropriate, delete the existing vendor record in NetSuite to allow Expensify to create a new one. +- **Add Email to Existing Vendor:** Add the email address of the report’s submitter to the existing vendor record in NetSuite. + +**Final Steps:** +1. After making the necessary changes, head to **Settings > Workspaces > Group > Workspace Name > Connections** in Expensify. +2. Sync the NetSuite workspace connection. +3. Retry exporting the report. + + +# ExpensiError NS0109: Failed to login to NetSuite, please verify your credentials + +This error indicates a problem with the tokens created for the connection between Expensify and NetSuite. The error message will say, "Login Error. Please check your credentials." + +## How to Fix ExpensiError NS0109 +1. Review the [Connect to NetSuite](https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite) guide and follow steps 1 and 2 exactly as outlined. +2. If you're using an existing token and encounter a problem, you may need to create a new token. + + +# ExpensiError NS0123 Login Error: Please make sure that the Expensify integration is enabled + +This error indicates that the Expensify integration is not enabled in NetSuite. + +## How to Fix ExpensiError NS0123 +1. **Enable the Expensify Integration:** + - In NetSuite, navigate to Setup > Integrations > Manage Integrations. + - Ensure that the Expensify Integration is listed and that the State is Enabled. +2. **If you can't find the Expensify integration:** + - Click "Show Inactives" to see if Expensify is listed as inactive. + - If Expensify is listed, update its state to Enabled. + +Once the Expensify integration is enabled, try syncing the NetSuite connection again. + + +# ExpensiError NS0045: Expenses Not Categorized with a NetSuite Account + +**To resolve this error:** +1. Log into NetSuite +2. Do a global search for the missing record. + - Ensure the expense category is active and correctly named. + - Ensure the category is associated with the correct subsidiary that the Expensify workspace is linked to. +3. In Expensify, head to **Settings > Workspaces > Groups > Workspace Name > Connections** and click **Sync Now** on the NetSuite connection to resync the workspace. +4. Go back to the report, click on the offending expense(s), and re-apply the category in question. +5. Export the report again. + + +# ExpensiError NS0061: Please Enter Value(s) for: Tax Code + +This error typically occurs when attempting to export expense reports to a Canadian subsidiary in NetSuite for the first time and/or if your subsidiary in NetSuite has Tax enabled. + +## How to Fix ExpensiError NS0061 +To fix this, you need to enable Tax in the NetSuite configuration settings. + +1. Go to **Settings > Workspaces > Group > Workspace Name > Connections > NetSuite**. + - Be sure to select posting accounts for GST/HST and PST if you plan on exporting any expenses with taxes on them to Journal Entries. +2. Click **Save** +3. Click **Sync Now** to sync the connection + +**Note:** Expenses created before Tax was enabled might need to have the newly imported taxes applied to them retroactively to be exported. + + +# Error creating employee: Your current role does not have permission to access this record. + +This error indicates that the credentials or role used to connect NetSuite to Expensify do not have the necessary permissions within NetSuite. You can find setup instructions for configuring permissions in NetSuite [here](https://help.expensify.com/articles/expensify-classic/connections/netsuite/Connect-To-NetSuite#step-3-add-expensify-integration-role-to-a-user). + +**To resolve this error:** +1. If permissions are configured correctly, confirm the report submitter exists in the subsidiary set on the workspace and that their Expensify email address matches the email on the NetSuite Employee Record. +2. If the above is true, try toggling off "Automatically create vendors/employees" under the Advanced tab of the NetSuite configuration window. + - Head to **Settings > Workspaces > Group > Workspace Name > Connections > NetSuite > Configure** + - Click on the **Advanced** tab + - Disable **Automatically create vendors/employees** +3. Sync the NetSuite connection in Expensify +4. Export the report again. + + +# Why are reports exporting as `Accounting Approved` instead of `Paid in Full`? + +**This can occur for two reasons:** +- Missing Locations, Classes, or Departments in the Bill Payment Form +- Incorrect Settings in Expensify Workspace Configuration + +## Missing Locations, Classes, or Departments in Bill Payment Form + +If locations, classes, or departments are required in your accounting classifications but are not marked as 'Show' on the preferred bill payment form, this error can occur, and you will need to update the bill payment form in NetSuite: +1. Go to Customization > Forms > Transaction Forms. +2. Find your preferred (checkmarked) Bill Payment form. +3. Click Edit or Customize. +4. Under the Screen Fields > Main tab, check 'Show' near the department, class, and location options. + +## Incorrect Settings in Expensify Workspace Configuration + +To fix this, you'll want to confirm the NetSuite connection settings are set up correctly in Expensify: +1. Head to **Settings > Workspaces > Group > Workspace Name > Connections > NetSuite > Configure > Advanced** +2. **Ensure the following settings are correct:** + - Sync Reimbursed Reports: Enabled and payment account chosen. + - Journal Entry Approval Level: Approved for Posting. + - A/P Approval Account: This must match the current account being used for bill payment. +3. **Verify A/P Approval Account:** + - To ensure the A/P Approval Account matches the account in NetSuite: + - Go to your bill/expense report causing the error. + - Click Make Payment. + - This account needs to match the account selected in your Expensify configuration. +4. **Check Expense Report List:** + - Make sure this is also the account selected on the expense report by looking at the expense report list. + +Following these steps will help ensure that reports are exported as "Paid in Full" instead of "Accounting Approved." + + +# Why are reports exporting as `Pending Approval`? + +If reports are exporting as "Pending Approval" instead of "Approved," you'll need to adjust the approval preferences in NetSuite. + +**Exporting as Journal Entries/Vendor Bills:** +1. In NetSuite, go to Setup > Accounting > Accounting Preferences. +2. On the **General** tab, uncheck **Require Approvals on Journal Entries**. +3. On the **Approval Routing** tab, uncheck Journal Entries/Vendor Bills to remove the approval requirement for Journal Entries created in NetSuite. + +**Note:** This change affects all Journal Entries, not just those created by Expensify. + +**Exporting as Expense Reports:** +1. In NetSuite, navigate to Setup > Company > Enable Features. +2. On the "Employee" tab, uncheck "Approval Routing" to remove the approval requirement for Expense Reports created in NetSuite. Please note that this setting also applies to purchase orders. + + +# How do I Change the Default Payable Account for Reimbursable Expenses in NetSuite? + +NetSuite is set up with a default payable account that is credited each time reimbursable expenses are exported as Expense Reports to NetSuite (once approved by the supervisor and accounting). If you need to change this to credit a different account, follow the below steps: + +**For OneWorld Accounts:** +1. Navigate to Setup > Company > Subsidiaries in NetSuite. +2. Next to the subsidiary you want to update, click Edit. +3. Click the Preferences tab. +4. In the Default Payable Account for Expense Reports field, select the desired payable account. +5. Click Save. + +**For Non-OneWorld Accounts:** +1. Navigate to Setup > Accounting > Accounting Preferences in NetSuite. +2. Click the Time & Expenses tab. +3. Under the Expenses section, locate the Default Payable Account for Expense Reports field and choose the preferred account. +4. Click Save. + + +# Why are my Company Card Expenses Exporting to the Wrong Account in NetSuite? + +If your company card transactions are exporting to the wrong account in your accounting system, there are a couple of factors to check: +1. **Verify Card Mapping:** + - Ensure that the cards are mapped to the correct accounts at the domain level + - This can be viewed under **Settings > Domains > Domain Name > Company Cards**. +2. **Check Default Account Settings:** + - Review the account where the expenses were exported + - It should be the default account under **Settings > Workspaces > Group > Workspace Name > Connections**. + - Click **Configure** to check the default export settings for your non-reimbursable expenses. + +The most common reason expenses export to the default account is that they are not actually imported from the mapped company card. Only company card expenses (notated with the “Card+Lock” icon) can use the export mapping settings configured at the domain level. + +Even if an expense was paid with the company card, it is considered a 'cash' expense unless it merges with a card expense marked with the Card+Lock icon. + +Less commonly, the issue may occur if the company card has been added to the user's personal settings. Expenses imported from a card linked at the individual account level will have a plain card icon. + diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md new file mode 100644 index 000000000000..eda92d41e820 --- /dev/null +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md @@ -0,0 +1,6 @@ +--- +title: Configure Quickbooks Desktop +description: Configure Quickbooks Desktop +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md similarity index 99% rename from docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md rename to docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md index 8fe31f3ec4f4..cd0b23d327d7 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md @@ -1,6 +1,7 @@ --- title: QuickBooks Desktop description: How to connect Expensify to QuickBooks Desktop and troubleshoot issues. +order: 1 --- # Overview QuickBooks Desktop is an accounting package developed by Intuit. It is designed for small and medium-sized businesses to help them manage their financial and accounting tasks. You can connect Expensify to QuickBooks Desktop to make expense management seamless. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md new file mode 100644 index 000000000000..cdbba9ec0b23 --- /dev/null +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md @@ -0,0 +1,6 @@ +--- +title: Quickbooks Desktop Troubleshooting +description: Quickbooks Desktop Troubleshooting +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md new file mode 100644 index 000000000000..99e598adb06a --- /dev/null +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md @@ -0,0 +1,6 @@ +--- +title: Configure Quickbooks Online +description: Configure Quickbooks Online +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Connect-To-QuickBooks-Online.md similarity index 99% rename from docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md rename to docs/articles/expensify-classic/connections/quickbooks-online/Connect-To-QuickBooks-Online.md index 623e5f1dd997..1d536d7bdb66 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Connect-To-QuickBooks-Online.md @@ -1,6 +1,7 @@ --- title: QuickBooks Online description: Everything you need to know about using Expensify's direct integration with QuickBooks Online. +order: 1 --- # Overview diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md new file mode 100644 index 000000000000..7e58ad83a164 --- /dev/null +++ b/docs/articles/expensify-classic/connections/quickbooks-online/Quickbooks-Online-Troubleshooting.md @@ -0,0 +1,6 @@ +--- +title: Quickbooks Online Troubleshooting +description: Quickbooks Online Troubleshooting +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md new file mode 100644 index 000000000000..444825973161 --- /dev/null +++ b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md @@ -0,0 +1,6 @@ +--- +title: Configure Sage Intacct +description: Configure Sage Intacct +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md b/docs/articles/expensify-classic/connections/sage-intacct/Connect-To-Sage-Intacct.md similarity index 99% rename from docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md rename to docs/articles/expensify-classic/connections/sage-intacct/Connect-To-Sage-Intacct.md index 560a65d0d722..369c3aae9fa9 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md +++ b/docs/articles/expensify-classic/connections/sage-intacct/Connect-To-Sage-Intacct.md @@ -1,6 +1,7 @@ --- title: Sage Intacct description: Connect your Expensify workspace with Sage Intacct +order: 1 --- # Overview Expensify’s seamless integration with Sage Intacct allows you to connect using either Role-based permissions or User-based permissions. diff --git a/docs/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting.md b/docs/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting.md new file mode 100644 index 000000000000..db341f87e930 --- /dev/null +++ b/docs/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting.md @@ -0,0 +1,6 @@ +--- +title: Sage Intacct Troubleshooting +description: Sage Intacct Troubleshooting +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/connections/xero/Configure-Xero.md b/docs/articles/expensify-classic/connections/xero/Configure-Xero.md new file mode 100644 index 000000000000..b23216c28401 --- /dev/null +++ b/docs/articles/expensify-classic/connections/xero/Configure-Xero.md @@ -0,0 +1,6 @@ +--- +title: Configure Xero +description: Configure Xero +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md b/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md similarity index 99% rename from docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md rename to docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md index 9dd479e90cf1..3010d11c2ff1 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md +++ b/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md @@ -1,6 +1,7 @@ --- title: The Xero Integration description: Everything you need to know about Expensify's direct integration with Xero +order: 1 --- # About diff --git a/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md b/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md new file mode 100644 index 000000000000..98ae5033db50 --- /dev/null +++ b/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md @@ -0,0 +1,6 @@ +--- +title: Xero Troubleshooting +description: Xero Troubleshooting +--- + +# Coming soon diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md index 043cc4be1e26..b9938b058ef6 100644 --- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md @@ -1,174 +1,134 @@ --- title: Admin Card Settings and Features -description: An in-depth look into the Expensify Card program's admin controls and settings. +description: A deep dive into the available controls and settings for the Expensify Card. --- +# Expensify Visa® Commercial Card Overview +The Expensify Visa® Commercial Card offers various settings to help admins manage expenses and card usage efficiently. Here’s how to use these features: -# Overview +## Smart Limits +Smart Limits allow you to set custom spending limits for each Expensify cardholder or default limits for groups. Setting a Smart Limit activates an Expensify card for your user and issues a virtual card for immediate use. -The Expensify Visa® Commercial Card offers a range of settings and functionality to customize how admins manage expenses and card usage in Expensify. To start, we'll lay out the best way to make these options work for you. +#### Set Limits for Individual Cardholders +As a Domain Admin, you can set or edit Custom Smart Limits for a card: +1. Go to _**Settings > Domains > Domain Name > Company Cards**_. +2. Click **Edit Limit** to set the limit. -Set Smart Limits to control card spend. Smart Limits are spend limits that can be set for individual cards or specific groups. Once a given Smart Limit is reached, the card is temporarily disabled until expenses are approved. +This limit restricts the amount of unapproved (unsubmitted and processing) expenses a cardholder can incur. Once the limit is reached, the cardholder cannot use their card until they submit outstanding expenses and have their card spend approved. If you set the Smart Limit to $0, the user’s card cannot be used. -Monitor spend using your Domain Limit and the Reconciliation Dashboard. -Your Domain Limit is the total Expensify Card limit across your entire organization. No member can spend more than what's available here, no matter what their individual Smart Limit is. A Domain Limit is dynamic and depends on a number of factors, which we'll explain below. +#### Set Default Group Limits +Domain Admins can set or edit custom Smart Limits for a domain group: -Decide the settlement model that works best for your business -Monthly settlement is when your Expensify Card balance is paid in full on a certain day each month. Though the Expensify Card is set to settle daily by default, any Domain Admin can change this setting to monthly. +1. Go to _**Settings > Domains > Domain Name > Groups**_. +2. Click on the limit in-line for your chosen group and amend the value. -Now, let's get into the mechanics of each piece mentioned above. +This limit applies to all members of the Domain Group who do not have an individual limit set via _**Settings > Domains > Domain Name > Company Cards**_. -# How to set Smart Limits -Smart Limits allow you to set a custom spend limit for each Expensify cardholder, or default limits for groups. Setting a Smart Limit is the step that activates an Expensify card for your user (and issues a virtual card for immediate use). +#### Refreshing Smart Limits +To let cardholders continue spending, you can approve their pending expenses via the Reconciliation tab. This frees up their limit, allowing them to use their card again. -## Set limits for individual cardholders -As a Domain Admin, you can set or edit Custom Smart Limits for a card by going to Settings > Domains > Domain Name > Company Cards. Simply click Edit Limit to set the limit. This limit will restrict the amount of unapproved (unsubmitted and Processing) expenses that a cardholder can incur. After the limit is reached, the cardholder won't be able to use their card until they submit outstanding expenses and have their card spend approved. If you set the Smart Limit to $0, the user's card can't be used. -## Set default group limits -Domain Admins can set or edit custom Smart Limits for a domain group by going to Settings > Domains > Domain Name > Groups. Just click on the limit in-line for your chosen group and amend the value. +To check an unapproved card balance and approve expenses: +1. Click on **Reconciliation** and enter a date range. +2. Click on the Unapproved total to see what needs approval. +3. You can add to a new report or approve an existing report from here. -This limit will apply to all members of the Domain Group who do not have an individual limit set via Settings > Domains > Domain Name > Company Cards. +You can also increase a Smart Limit at any time by clicking **Edit Limit**. -## Refreshing Smart Limits -To let cardholders keep spending, you can approve their pending expenses via the Reconciliation tab. This will free up their limit, allowing them to use their card again. +### Understanding Your Domain Limit +To ensure you have the most accurate Domain Limit for your company, follow these steps: -To check an unapproved card balance and approve expenses, click on Reconciliation and enter a date range, then click though the Unapproved total to see what needs approving. You can add to a new report or approve an existing report from here. +1. **Connect Your Bank Account:** Go to _**Settings > Account > Payments > Add Verified Bank Account**_ and connect via Plaid. -You can also increase a Smart Limit at any time by clicking Edit Limit. +2. **Request a Custom Limit:** If your bank isn’t supported or you’re experiencing connection issues, you can request a custom limit at _**Settings > Domains > Domain Name > Company Cards > Request Limit Increase**_. Note that you’ll need to provide three months of unredacted bank statements for review by our risk management team. -# Understanding your Domain Limit +### Factors Affecting Your Domain Limit +Your Domain Limit may fluctuate due to several factors: -To get the most accurate Domain Limit for your company, connect your bank account via Plaid under Settings > Account > Payments > Add Verified Bank Account. +- **Available Funds in Your Verified Business Bank Account:** We regularly monitor balances via Plaid. A sudden decrease in balance within the last 24 hours may impact your limit. For accounts with 'sweep' functionality, maintain a sufficient balance even when sweeping daily. -If your bank isn't supported or you're having connection issues, you can request a custom limit under Settings > Domains > Domain Name > Company Cards > Request Limit Increase. As a note, you'll need to provide three months of unredacted bank statements for review by our risk management team. +- **Pending Expenses:** Check the Reconciliation Dashboard for large pending expenses that could affect your available balance. Your Domain Limit automatically adjusts to include pending expenses. -Your Domain Limit may fluctuate from time to time based on various factors, including: +- **Processing Settlements:** Settlements typically take about three business days to process and clear. Multiple large settlements over consecutive days may affect your Domain Limit, which updates dynamically once settlements are cleared. -- Available funds in your Verified Business Bank Account: We regularly check bank balances via Plaid. A sudden drop in balance within the last 24 hours may affect your limit. For 'sweep' accounts, be sure to maintain a substantial balance even if you're sweeping daily. -- Pending expenses: Review the Reconciliation Dashboard to check for large pending expenses that may impact your available balance. Your Domain Limit will adjust automatically to include pending expenses. -- Processing settlements: Settlements need about three business days to process and clear. Several large settlements over consecutive days may impact your Domain Limit, which will dynamically update when settlements have cleared. +Please note: If your Domain Limit is reduced to $0, cardholders cannot make purchases, even if they have higher Smart Limits set on their individual cards. -As a note, if your Domain Limit is reduced to $0, your cardholders can't make purchases even if they have a larger Smart Limit set on their individual cards. +## Reconciling Expenses and Settlements +Reconciling expenses ensures your financial records are accurate and up-to-date. Follow these steps to review and reconcile expenses associated with your Expensify Cards: -# How to reconcile Expensify Cards -## How to reconcile expenses -Reconciling expenses is essential to ensuring your financial records are accurate and up-to-date. +#### How to Reconcile Expenses: +1. Go to _**Settings > Domains > Domain Name > Company Cards > Reconciliation > Expenses**_. +2. Enter your start and end dates, then click *Run*. +3. The Imported Total will display all Expensify Card transactions for the period. +4. You'll see a list of all Expensify Cards, the total spend on each card, and a snapshot of expenses that have been approved and have not been approved (Approved Total and Unapproved Total, respectively). +5. Click on the amounts to view the associated expenses. -Follow the steps below to quickly review and reconcile expenses associated with your Expensify Cards: +#### How to Reconcile Settlements: +A settlement is the payment to Expensify for purchases made using the Expensify Cards. The program can settle on either a daily or monthly basis. Note that not all transactions in a settlement will be approved when running reconciliation. -1. Go to Settings > Domains > Domain Name > Company Cards > Reconciliation > Expenses -2. Enter your start and end dates, then click Run -3. The Imported Total will show all Expensify Card transactions for the period -4. You'll also see a list of all Expensify Cards, the total spend on each card, and a snapshot of expenses that have and have not been approved (Approved Total and Unapproved Total, respectively) -By clicking on the amounts, you can view the associated expenses +1. Log into the Expensify web app. +2. Click _**Settings > Domains > Domain Name > Company Cards > Reconciliation > `Settlements**_. +3. Use the Search function to generate a statement for the specific period you need. +The search results will include the following info for each entry: +- **Date:** When a purchase was made or funds were debited for payments. +- **Posted Date:** When the purchase transaction is posted. +- **Entry ID:** A unique number grouping card payments and transactions settled by those payments. +- **Amount:** The amount debited from the Business Bank Account for payments. +- **Merchant:** The business where a purchase was made. +- **Card:** Refers to the Expensify Card number and cardholder’s email address. +- **Business Account:** The business bank account connected to Expensify that the settlement is paid from. +- **Transaction ID:** A special ID that helps Expensify support locate transactions if there’s an issue. -## How to reconcile settlements -A settlement is the payment to Expensify for the purchases made using the Expensify Cards. +Review the individual transactions (debits) and the payments (credits) that settled them. Each cardholder will have a virtual and a physical card listed, handled the same way for settlements, reconciliation, and exporting. -The Expensify Card program can settle on either a daily or monthly basis. One thing to note is that not all transactions in a settlement will be approved when running reconciliation. +4. Click **Download CSV** for reconciliation. This will list everything you see on the screen. +5. To reconcile pre-authorizations, use the Transaction ID column in the CSV file to locate the original purchase. +6. Review account payments: You’ll see payments made from the accounts listed under _**Settings > Account > Payments > Bank Accounts**_. Payment data won’t show for deleted accounts. -You can view the Expensify Card settlements under Settings > Domains > Domain Name > Company Cards > Reconciliation > Settlements. +Use the Reconciliation Dashboard to confirm the status of expenses missing from your accounting system. It allows you to view both approved and unapproved expenses within your selected date range that haven’t been exported yet. -By clicking each settlement amount, you can see the transactions contained in that specific payment amount. +### Set a Preferred Workspace +Many customers find it helpful to separate their company card expenses from other types of expenses for easier coding. To do this, create a separate workspace specifically for card expenses. -Follow the below steps to run reconciliation on the Expensify Card settlements: +**Using a Preferred Workspace:** +Combine this feature with Scheduled Submit to automatically add new card expenses to reports connected to your card-specific workspace. -1. Log into the Expensify web app -2. Click Settings > Domains > Domain Name > Company Cards > Reconciliation tab > Settlements -3. Use the Search function to generate a statement for the specific period you need -4. The search results will include the following info for each entry: - - Date: when a purchase was made or funds were debited for payments - - Posted Date: when the purchase transaction posted - - Entry ID: a unique number grouping card payments and transactions settled by those payments - - Amount: the amount debited from the Business Bank Account for payments - - Merchant: the business where a purchase was made - - Card: refers to the Expensify Card number and cardholder's email address - - Business Account: the business bank account connected to Expensify that the settlement is paid from - - Transaction ID: a special ID that helps Expensify support locate transactions if there's an issue +### Change the Settlement Account +You can change your settlement account to any verified business bank account in Expensify. If your current bank account is closing, make sure to set up a replacement as soon as possible. -5. Review the individual transactions (debits) and the payments (credits) that settled them -6. Every cardholder will have a virtual and a physical card listed. They're handled the same way for settlements, reconciliation, and exporting. -7. Click Download CSV for reconciliation -8. This will list everything that you see on screen -9. To reconcile pre-authorizations, you can use the Transaction ID column in the CSV file to locate the original purchase -10. Review account payments -11. You'll see payments made from the accounts listed under Settings > Account > Payments > Bank Accounts. Payment data won't show for deleted accounts. +#### Steps to Select a Different Settlement Account: +1. Go to _**Settings > Domains > Domain Name > Company Cards > Settings**_ tab. +2. Use the Expensify Card settlement account dropdown to select a new account. +3. Click **Save**. -You can use the Reconciliation Dashboard to confirm the status of expenses that are missing from your accounting system. It allows you to view both approved and unapproved expenses within your selected date range that haven't been exported yet. +### Change the Settlement Frequency +By default, Expensify Cards settle daily. However, you can switch to monthly settlements. +#### Monthly Settlement Requirements: + - The settlement account must not have had a negative balance in the last 90 days. + - There will be an initial settlement for any outstanding spending before the switch. + - The settlement date going forward will be the date you switch (e.g., if you switch on September 15th, future settlements will be on the 15th of each month). -# Deep dive -## Set a preferred workspace -Some customers choose to split their company card expenses from other expense types for coding purposes. Most commonly this is done by creating a separate workspace for card expenses. +#### Steps to Change the Settlement Frequency: +1. Go to _**Settings > Domains > Domain Name > Company Cards > Settings**_ tab. +2. Click the **Settlement Frequency** dropdown and select **Monthly**. +3. Click **Save** to confirm the change. -You can use the preferred workspace feature in conjunction with Scheduled Submit to make sure all newly imported card expenses are automatically added to reports connected to your card-specific workspace. +### Declined Expensify Card Transactions +If you have 'Receive real-time alerts' enabled, you'll get a notification explaining why a transaction was declined. To enable alerts: +1. Open the mobile app. +2. Click the three-bar icon in the upper-left corner. +3. Go to Settings. +4. Toggle 'Receive real-time alerts' on. -## How to change your settlement account -You can change your settlement account to any other verified business bank account in Expensify. If your bank account is closing, make sure you set up the replacement bank account in Expensify as early as possible. +If you or your employees notice any unfamiliar purchases or need a new card, go to _**Settings > Account > Credit Card Import**_ and click on **Request a New Card**. -To select a different settlement account: +#### Common Reasons for Declines: +- **Insufficient Card Limit:** If a transaction exceeds your card's limit, it will be declined. Always check your balance under _**Settings > Account > Credit Card Import**_ on the web or mobile app. Approve pending expenses to free up your limit. -1. Go to Settings > Domains > Domain Name > Company Cards > Settings tab -2. Use the Expensify Card settlement account dropdown to select a new account -3. Click Save +- **Card Not Activated or Canceled:** Transactions won't process if the card hasn't been activated or has been canceled. +- **Incorrect Card Information:** Entering incorrect card details, such as the CVC, ZIP, or expiration date, will lead to declines. -## Change the settlement frequency +- **Suspicious Activity:** Expensify may block transactions if unusual activity is detected. This could be due to irregular spending patterns, risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unusual merchants. If further review is needed, Expensify will perform a manual due diligence check and lock your cards temporarily. -By default, the Expensify Cards settle on a daily cadence. However, you can choose to have the cards settle on a monthly basis. - -1. Monthly settlement is only available if the settlement account hasn't had a negative balance in the last 90 days -2. There will be an initial settlement to settle any outstanding spend that happened before switching the settlement frequency -3. The date that the settlement is changed to monthly is the settlement date going forward (e.g. If you switch to monthly settlement on September 15th, Expensify Cards will settle on the 15th of each month going forward) - -To change the settlement frequency: -1. Go to Settings > Domains > Domain Name > Company Cards > Settings tab -2. Click the Settlement Frequency dropdown and select Monthly -3. Click Save to confirm the change - - - -## Declined Expensify Card transactions -As long as you have 'Receive realtime alerts' enabled, you'll get a notification explaining the decline reason. You can enable alerts in the mobile app by clicking on three-bar icon in the upper-left corner > Settings > toggle Receive realtime alerts on. - -If you ever notice any unfamiliar purchases or need a new card, go to Settings > Account > Credit Card Import and click on Request a New Card right away. - -Here are some reasons an Expensify Card transaction might be declined: - -1. You have an insufficient card limit - - If a transaction amount exceeds the available limit on your Expensify Card, the transaction will be declined. It's essential to be aware of the available balance before making a purchase to avoid this - you can see the balance under Settings > Account > Credit Card Import on the web app or mobile app. Submitting expenses and having them approved will free up your limit for more spend. - -2. Your card hasn't been activated yet, or has been canceled - - If the card has been canceled or not yet activated, it won't process any transactions. - -3. Your card information was entered incorrectly. Entering incorrect card information, such as the CVC, ZIP or expiration date will also lead to declines. - -4. There was suspicious activity - - If Expensify detects unusual or suspicious activity, we may block transactions as a security measure. This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unsual merchants and try again. - If the spending looks suspicious, we may do a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. -5. The merchant is located in a restricted country - - Some countries may be off-limits for transactions. If a merchant or their headquarters (billing address) are physically located in one of these countries, Expensify Card purchases will be declined. This list may change at any time, so be sure to check back frequently: Belarus, Burundi, Cambodia, Central African Republic, Democratic Republic of the Congo, Cuba, Iran, Iraq, North Korea, Lebanon, Libya, Russia, Somalia, South Sudan, Syrian Arab Republic, Tanzania, Ukraine, Venezuela, Yemen, and Zimbabwe. - -{% include faq-begin.md %} -## What happens when I reject an Expensify Card expense? -Rejecting an Expensify Card expense from an Expensify report will simply allow it to be reported on a different report. - -If an Expensify Card expense needs to be rejected, you can reject the report or the specific expense so it can be added to a different report. The rejected expense will become Unreported and return to the submitter's Expenses page. - -If you want to dispute a card charge, please message Concierge to start the dispute process. - -If your employee has accidentally made an unauthorised purchase, you will need to work that out with the employee to determine how they will pay back your company. - - -## What happens when an Expensify Card transaction is refunded? - - -The way a refund is displayed in Expensify depends on the status of the expense (pending or posted) and whether or not the employee also submitted an accompanying SmartScanned receipt. Remember, a SmartScanned receipt will auto-merge with the Expensify Card expense. - -- Full refunds: -If a transaction is pending and doesn't have a receipt attached (except for eReceipts), getting a full refund will make the transaction disappear. -If a transaction is pending and has a receipt attached (excluding eReceipts), a full refund will zero-out the transaction (amount becomes zero). -- Partial refunds: -If a transaction is pending, a partial refund will reduce the amount of the transaction. -- If a transaction is posted, a partial refund will create a negative transaction for the refund amount. - -{% include faq-end.md %} +- **Merchant in a Restricted Country:** Transactions will be declined if the merchant is in a restricted country. diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md deleted file mode 100644 index fb84e3484598..000000000000 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: Expensify Card Auto-Reconciliation -description: Everything you need to know about Expensify Card Auto-Reconciliation ---- - - -# Overview -If your company uses the Expensify Visa® Commercial Card, and connects to a direct accounting integration, you can auto-reconcile card spending each month. - -The integrations that auto-reconciliation are available on are: - -- QuickBooks Online -- Xero -- NetSuite -- Sage Intacct - -# How-to Set Up Expensify Card Auto-Reconciliation - -## Auto-Reconciliation Prerequisites - -- Connection: -1. A Preferred Workspace is set. -2. A Reconciliation Account is set and matches the Expensify Card settlement account. -- Automation: -1. Auto-Sync is enabled on the Preferred Workspace above. -2. Scheduled Submit is enabled on the Preferred Workspace above. -- User: -1. A Domain Admin is set as the Preferred Workspace’s Preferred Exporter. - -To set up your auto-reconciliation account with the Expensify Card, follow these steps: -1. Navigate to your Settings. -2. Choose "Domains," then select your specific domain name. -3. Click on "Company Cards." -4. From the dropdown menu, pick the Expensify Card. -5. Head to the "Settings" tab. -6. Select the account in your accounting solution that you want to use for reconciliation. Make sure this account matches the settlement business bank account. - -![Company Card Settings section](https://help.expensify.com/assets/images/Auto-Reconciliaton_Image1.png){:width="100%"} - -That's it! You've successfully set up your auto-reconciliation account. - -## How does Auto-Reconciliation work -Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s go over those! - -### Handling Purchases and Card Balance Payments -**What happens**: When an Expensify Card is used to make purchases, the amount spent is automatically deducted from your company’s 'Settlement Account' (your business checking account). This deduction happens on a daily or monthly basis, depending on your chosen settlement frequency. Don't worry; this settlement account is pre-defined when you apply for the Expensify Card, and you can't accidentally change it. -**Accounting treatment**: After your card balance is settled each day, we update your accounting system with a journal entry. This entry credits your bank account (referred to as the GL account) and debits the Expensify Card Clearing Account. To ensure accuracy, please make sure that the 'bank account' in your Expensify Card settings matches your real-life settlement account. You can easily verify this by navigating to **Settings > Account > Payments**, where you'll see 'Settlement Account' next to your business bank account. To keep track of settlement figures by date, use the Company Card Reconciliation Dashboard's Settlements tab: - -![Company Card Reconciliation Dashboard](https://help.expensify.com/assets/images/Auto-Reconciliation_Image2.png){:width="100%"} - -### Submitting, Approving, and Exporting Expenses -**What happens**: Users submit their expenses on a report, which might occur after some time has passed since the initial purchase. Once the report is approved, it's then exported to your accounting software. -**Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses. - -# Deep Dive -## QuickBooks Online - -### Initial Setup -1. Start by accessing your group workspace linked to QuickBooks Online. On the Export tab, make sure that the user chosen as the Preferred Exporter holds the role of a Workspace Admin and has an email address associated with your Expensify Cards' domain. For instance, if your domain is company.com, the Preferred Exporter's email should be email@company.com. -2. Head over to the Advanced tab and ensure that Auto-Sync is enabled. -3. Now, navigate to **Settings > Domains > *Domain Name* > Company Cards > Settings**. Use the dropdown menu next to "Preferred Workspace" to select the group workspace connected to QuickBooks Online and with Scheduled Submit enabled. -4. In the dropdown menu next to "Expensify Card reconciliation account," choose your existing QuickBooks Online bank account for reconciliation. This should be the same account you use for Expensify Card settlements. -5. In the dropdown menu next to "Expensify Card settlement account," select your business bank account used for settlements (found in Expensify under **Settings > Account > Payments**). - -### How This Works -1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account. -2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement). -3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. - -### Example -- We have card transactions for the day totaling $100, so we create the following journal entry upon sync: -![QBO Journal Entry](https://help.expensify.com/assets/images/Auto-Reconciliation QBO 1.png){:width="100%"} -- The current balance of the Expensify Clearing Account is now $100: -![QBO Clearing Account](https://help.expensify.com/assets/images/Auto-reconciliation QBO 2.png){:width="100%"} -- After transactions are posted in Expensify and the report is approved and exported, a second journal entry is generated: -![QBO Second Journal Entry](https://help.expensify.com/assets/images/Auto-reconciliation QBO 3.png){:width="100%"} -- We reconcile the matching amounts automatically, clearing the balance of the Expensify Clearing Account: -![QBO Clearing Account 2](https://help.expensify.com/assets/images/Auto-reconciliation QBO 4.png){:width="100%"} -- Now, you'll have a debit on your credit card account (increasing the total spent) and a credit on the bank account (reducing the available amount). The Clearing Account balance is $0. -- Each expense will also create a credit card expense, similar to how we do it today, exported upon final approval. This action debits the expense account (category) and includes any other line item data. -- This process occurs daily during the QuickBooks Online Auto-Sync to ensure your card remains reconciled. - -**Note:** If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain Admin can set an export account for individual cards via **Settings > Domains > *Domain Name* > Company Cards > Edit Exports**. The Expensify Card transactions will always export as Credit Card charges in your accounting software, even if the non-reimbursable setting is configured differently, such as a Vendor Bill. - -## Xero - -### Initial Setup -1. Begin by accessing your group workspace linked to Xero. On the Export tab, ensure that the Preferred Exporter is a Workspace Admin with an email address from your Expensify Cards domain (e.g. company.com). -2. Head to the Advanced tab and confirm that Auto-Sync is enabled. -3. Now, go to **Settings > Domains > *Domain Name* > Company Cards > Settings**. From the dropdown menu next to "Preferred Workspace," select the group workspace connected to Xero with Scheduled Submit enabled. -4. In the dropdown menu for "Expensify Card settlement account," pick your settlement business bank account (found in Expensify under **Settings > Account > Payments**). -5. In the dropdown menu for "Expensify Card reconciliation account," select the corresponding GL account from Xero for your settlement business bank account from step 4. - -### How This Works -1. During the first overnight Auto Sync after enabling Continuous Reconciliation, Expensify will create a Liability Account (Bank Account) on your Xero Dashboard. If you've opted for Daily Settlement, an additional Clearing Account will be created in your General Ledger. Two Contacts —Expensify and Expensify Card— will also be generated: -![Xero Contacts](https://help.expensify.com/assets/images/Auto-reconciliation Xero 1.png){:width="100%"} -2. The bank account for Expensify Card transactions is tied to the Liability Account Expensify created. Note that this doesn't apply to other cards or non-reimbursable expenses, which follow your workspace settings. - -### Daily Settlement Reconciliation -- If you've selected Daily Settlement, Expensify uses entries in the Clearing Account to reconcile the daily settlement. This is because Expensify bills on posted transactions, which you can review via **Settings > Domains > *Domain Name* > Company Cards > Reconciliation > Settlements**. -- At the end of each day (or month on your settlement date), the settlement charge posts to your Business Bank Account. Expensify assigns the Clearing Account (or Liability Account for monthly settlement) as a Category to the transaction, posting it in your GL. The charge is successfully reconciled. - -### Bank Transaction Reconciliation -- Expensify will pay off the Liability Account with the Clearing Account balance and reconcile bank transaction entries to the Liability Account with your Expense Accounts. -- When transactions are approved and exported from Expensify, bank transactions (Receive Money) are added to the Liability Account, and coded to the Clearing Account. Simultaneously, Spend Money transactions are created and coded to the Category field. If you see many Credit Card Misc. entries, add commonly used merchants as Contacts in Xero to export with the original merchant name. -- The Clearing Account balance is reduced, paying off the entries to the Liability Account created in Step 1. Each payment to and from the Liability Account should have a corresponding bank transaction referencing an expense account. Liability Account Receive Money payments appear with "EXPCARD-APPROVAL" and the corresponding Report ID from Expensify. -- You can run a Bank Reconciliation Summary displaying entries in the Liability Account referencing individual payments, as well as entries that reduce the Clearing Account balance to unapproved expenses. -- **Important**: To bring your Liability Account balance to 0, enable marking transactions as reconciled in Xero. When a Spend Money bank transaction in the Liability Account has a matching Receive Transaction, you can mark both as Reconciled using the provided hyperlink. - -**Note**: If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain Admin can set an export account for individual cards via **Settings > Domains > *Domain Name* > Company Cards > Edit Exports**. The Expensify Card transactions will always export as a Credit Card charge in your accounting software, regardless of the non-reimbursable setting in their accounting configuration. - -## NetSuite - -### Initial Setup -1. Start by accessing your group workspace connected to NetSuite and click on "Configure" under **Connections > NetSuite**. -2. On the Export tab, ensure that the Preferred Exporter is a Workspace Admin with an email address from your Expensify Cards' domain. For example, if your domain is company.com, the Preferred Exporter's email should be email@company.com. -3. Head over to the Advanced tab and make sure Auto-Sync is enabled. -4. Now, go to **Settings > Domains > *Domain Name* > Company Cards > Settings**. From the dropdown menu next to "Preferred Workspace," select the group workspace connected to NetSuite with Scheduled Submit enabled. -5. In the dropdown menu next to "Expensify Card reconciliation account," choose your existing NetSuite bank account used for reconciliation. This account must match the one set in Step 3. -6. In the dropdown menu next to "Expensify Card settlement account," select your daily settlement business bank account (found in Expensify under **Settings > Account > Payments**). - -### How This Works with Daily Settlement -1. After setting up the card and running the first auto-sync, we'll create the Expensify Card Liability account and the Expensify Clearing Account within your NetSuite subsidiary general ledger. -2. During the same sync, if there are newly posted transactions, we'll create a journal entry totaling all posted transactions for the day. This entry will credit the selected bank account and debit the new Expensify Clearing account. -3. Once transactions are approved in Expensify, the report will be exported to NetSuite, with each line recorded as individual credit card expenses. Additionally, another journal entry will be generated, crediting the Expensify Clearing Account and debiting the Expensify Card Liability account. - -### How This Works with Monthly Settlement -1. After the first monthly settlement, during Auto-Sync, Expensify creates a Liability Account in NetSuite (without a clearing account). -2. Each time the monthly settlement occurs, Expensify calculates the total purchase amount since the last settlement and creates a Journal Entry that credits the settlement bank account (GL Account) and debits the Expensify Liability Account in NetSuite. -3. As expenses are approved and exported to NetSuite, Expensify credits the Liability Account and debits the correct expense categories. - -**Note**: By default, the Journal Entries created by Expensify are set to the approval level "Approved for posting," so they will automatically credit and debit the appropriate accounts. If you have "Require approval on Journal Entries" enabled in your accounting preferences in NetSuite (**Setup > Accounting > Accounting Preferences**), this will override that default. Additionally, if you have set up Custom Workflows (**Customization > Workflow**), these can also override the default. In these cases, the Journal Entries created by Expensify will post as "Pending approval." You will need to approve these Journal Entries manually to complete the reconciliation process. - -### Example -- Let's say you have card transactions totaling $100 for the day. -- We create a journal entry: -![NetSuite Journal Entry](https://help.expensify.com/assets/images/Auto-reconciliation NS 1.png){:width="100%"} -- After transactions are posted in Expensify, we create the second Journal Entry(ies): -![NetSuite Second Journal Entry](https://help.expensify.com/assets/images/Auto-reconciliation NS 2.png){:width="100%"} -- We then reconcile the matching amounts automatically, clearing the balance of the Expensify Clearing Account. -- Now, you'll have a debit on your Credit Card account (increasing the total spent) and a credit on the bank account (reducing the amount available). The clearing account has a $0 balance. -- Each expense will also create a Journal Entry, just as we do today, exported upon final approval. This entry will debit the expense account (category) and contain any other line item data. -- This process happens daily during the NetSuite Auto-Sync to keep your card reconciled. - -**Note**: Currently, only Journal Entry export is supported for auto-reconciliation. You can set other export options for all other non-reimbursable spend in the **Configure > Export** tab. Be on the lookout for Expense Report export in the future! - -If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain Admin can set an export account for individual Expensify Cards via **Settings > Domains > Company Cards > Edit Exports**. The Expensify Card transactions will always export as a Credit Card charge in your accounting software, regardless of the non-reimbursable setting in their accounting configuration. - -## Sage Intacct - -### Initial Setup -1. Start by accessing your group workspace connected to Sage Intacct and click on "Configure" under **Connections > Sage Intacct**. -2. On the Export tab, ensure that you've selected a specific entity. To enable Expensify to create the liability account, syncing at the entity level is crucial, especially for multi-entity environments. -3. Still on the Export tab, confirm that the user chosen as the Preferred Exporter is a Workspace Admin, and their email address belongs to the domain used for Expensify Cards. For instance, if your domain is company.com, the Preferred Exporter's email should be email@company.com. -4. Head over to the Advanced tab and make sure Auto-Sync is enabled. -5. Now, go to **Settings > Domains > *Domain Name* > Company Cards > Settings**. From the dropdown menu next to "Preferred Workspace," select the group workspace connected to Sage Intacct with Scheduled Submit enabled. -6. In the dropdown menu next to "Expensify Card reconciliation account" pick your existing Sage Intacct bank account used for daily settlement. This account must match the one set in the next step. -7. In the dropdown menu next to "Expensify Card settlement account" select your daily settlement business bank account (found in Expensify under **Settings > Account > Payments**). -8. Use the dropdown menus to select your cash-only and accrual-only journals. If your organization operates on a cash-only or accrual-only basis, choose "No Selection" for the journals as needed. If your organization uses both cash and accrual methods, please select both a cash-only and an accrual-only journal. Don't forget to save your settings! - -### How This Works with Daily Settlement -1. After setting up the card and running the first auto-sync, we'll create the Expensify Card Expensify Clearing Account within your Sage Intacct general ledger. Once the first card transaction is exported, we'll create a Liability Account. -2. In the same sync, if there are newly posted transactions from your Expensify Cards, we'll then create a journal entry totaling all posted transactions for the day. This entry will credit the business bank account (set in Step 4 above) and debit the new Expensify Clearing account. -3. Once Expensify Card transactions are approved in Expensify, the report will be exported to Sage Intacct, with each line recorded as individual credit card expenses. Additionally, another journal entry will be generated, crediting the Expensify Clearing Account and debiting the Expensify Card Liability Account. - -### How This Works with Monthly Settlement -1. After the initial export of a card transaction, Expensify establishes a Liability Account in Intacct (without a clearing account). -2. Each time a monthly settlement occurs, Expensify calculates the total purchase amount since the last settlement and creates a Journal Entry. This entry credits the settlement bank account (GL Account) and debits the Expensify Liability Account in Intacct. -3. As expenses are approved and exported to Intacct, Expensify credits the Liability Account and debits the appropriate expense categories. - -{% include faq-begin.md %} - -## What are the timeframes for auto-reconciliation in Expensify? -We offer either daily or monthly auto-reconciliation: -- Daily Settlement: each day, as purchases are made on your Expensify Cards, the posted balance is withdrawn from your Expensify Card Settlement Account (your business bank account). -- Monthly Settlement: each month, on the day of the month that you enabled Expensify Cards (or switched from Daily to Monthly Settlement), the posted balance of all purchases since the last settlement payment is withdrawn from your Expensify Card Settlement Account (your business bank account). - -## Why is my Expensify Card auto-reconciliation not working with Xero? -When initially creating the Liability and Bank accounts to complete the auto-reconciliation process, we rely on the system to match and recognize those accounts created. You can't make any changes or we will not “find” those accounts. - -If you have changed the accounts. It's an easy fix, just rename them! -- Internal Account Code: must be **ExpCardLbl** -- Account Type: must be **Bank** - -## My accounting integration is not syncing. How will this affect the Expensify Card auto-reconciliation? -When you receive a message that your accounting solution’s connection failed to sync, you will also receive an email or error message with the steps to correct the sync issue. If you do not, please contact Support for help. When your accounting solution’s sync reconnects and is successful, your auto-reconciliation will resume. - -If your company doesn't have auto-reconciliation enabled for its Expensify Cards, you can still set up individual export accounts. Here's how: - -1. Make sure you have Domain Admin privileges. -2. Navigate to **Settings > Domains** -3. Select 'Company Cards' -4. Find the Expensify Card you want to configure and choose 'Edit Exports.' -5. Pick the export account where you want the Expensify Card transactions to be recorded. -6. Please note that these transactions will always be exported as Credit Card charges in your accounting software. This remains the case even if you've configured non-reimbursable settings as something else, such as a Vendor Bill. - -These simple steps will ensure your Expensify Card transactions are correctly exported to the designated account in your accounting software. - -## Why does my Expensify Card Liability Account have a balance? -If you’re using the Expensify Card with auto-reconciliation, your Expensify Card Liability Account balance should always be $0 in your accounting system. - -If you see that your Expensify Card Liability Account balance isn’t $0, then you’ll need to take action to return that balance to $0. - -If you were using Expensify Cards before auto-reconciliation was enabled for your accounting system, then any expenses that occurred prior will not be cleared from the Liability Account. -You will need to prepare a manual journal entry for the approved amount to bring the balance to $0. - -To address this, please follow these steps: -1. Identify the earliest date of a transaction entry in the Liability Account that doesn't have a corresponding entry. Remember that each expense will typically have both a positive and a negative entry in the Liability Account, balancing out to $0. -2. Go to the General Ledger (GL) account where your daily Expensify Card settlement withdrawals are recorded, and locate entries for the dates identified in Step 1. -3. Adjust each settlement entry so that it now posts to the Clearing Account. -4. Create a Journal Entry or Receive Money Transaction to clear the balance in the Liability Account using the funds currently held in the Clearing Account, which was set up in Step 2. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md index f24ed57dc655..38686462a1c2 100644 --- a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md @@ -3,78 +3,55 @@ title: Cardholder Settings and Features description: Expensify Card Settings for Employees --- -# How to use your Expensify Visa® Commercial Card -Once you receive your card, you can start using it right away. +# Using Your Expensify Visa® Commercial Card -First, you'll want to take note of the Smart Limit tied to your card – this is listed in your card settings via **Settings > Account > Credit Card Import**. This limit represents the total amount of unapproved expenses you can have on the card. +### Activate Your Card +You can start using your card immediately upon receipt by logging into your Expensify account, heading to your Home tab, and following the prompts on the _**Activate your Expensify Card**_ task. -It's crucial to continuously submit your expenses promptly, as that'll ensure they can be approved and restore your full limit. You can always chat with your admin if you need your limit adjusted. +### Review your Card's Smart Limit +Check your card’s Smart Limit via _**Settings > Account > Credit Card Import**_: +- This limit is the total amount of unapproved expenses you can have on the card. +- If a purchase is more than your card's Smart Limit, it will be declined. -You can swipe your Expensify Card like you would with any other card. As you make purchases, you'll get instant alerts on your phone letting you know if you need to SmartScan receipts. Any SmartScanned receipts should merge with the card expense automatically. +## Managing Expenses +- **Submit Expenses Promptly**: Submit your expenses regularly to restore your full limit. Contact your admin if you need a limit adjustment. +- **Using Your Card**: Swipe your Expensify Card like any other card. You’ll receive instant alerts on your phone for SmartScan receipts. SmartScanned receipts will merge automatically with card expenses. +- **eReceipts**: If your organization doesn’t require itemized receipts, Expensify will generate IRS-compliant eReceipts for all non-lodging transactions. +- **Reporting Expenses**: Report and submit Expensify Card expenses as usual. Approved expenses refresh your Smart Limit. -If your organization doesn't require itemized receipts, you can rely on eReceipts instead. As long as the expense isn't lodging-related, Expensify will automatically generate an IRS-compliant eReceipt for every transaction. +## Enabling Notifications +Download the Expensify mobile app and enable push notifications to stay updated on spending activity and potential fraud. -You can report and submit Expensify Card expenses just like any other expenses. As they're approved, your Smart Limit will be refreshed accordingly, allowing you to keep making purchases. +#### For iPhone: +1. Open the Expensify app and tap the three-bar icon in the upper-left corner. +2. Tap _**Settings > enable Receive real-time alerts**_. +3. Accept the confirmation to access your iPhone’s notification settings for Expensify. +4. Turn on **Allow Notifications** and select your notification types. -## Enable Notifications -Download the Expensify mobile app and enable push notifications to stay current on your spending activity. Your card is connected to your Expensify account, so each transaction on your card will trigger a push notification. We'll also send you a push notification if we detect potentially fraudulent activity and allow you to confirm your purchase. - -Follow the steps below to enable real-time alerts on your mobile device. - -**If you have an iPhone**: -1. Open the Expensify app and tap the three-bar icon in the upper-left corner -2. Tap **Settings** and enable **Receive realtime alerts** -3. Accept the confirmation dialogue to go to your iPhone's notification settings for Expensify. Turn on Allow Notifications, and choose the notification types you’d like! - -**If you have an Android**: -1. Go to Settings and open 'Apps and Notifications'. +#### For Android: +1. Go to _**Settings > Apps and Notifications**_. 2. Find and open Expensify and enable notifications. -3. Customize your alerts. Depending on your phone model, you may have extra options to customize the types of notifications you receive. - -## Your virtual card -Once you're assigned a limit, you'll be able to use your virtual card immediately. You can view your virtual card details via **Settings > Account > Credit Card Import > Show Details**. Keep in mind that your virtual card and physical card share a limit. - -The virtual Expensify Card includes a card number, expiration date, and security code (CVC). You can use the virtual card for online purchases, in-app transactions, and in-person payments once it's linked to a mobile wallet (Apple Pay or Google Pay). - -## How to access your virtual card details -Here's how to access your virtual card details via the Expensify mobile app: -1. Tap the three-bar icon in the upper-left corner -2. Tap **Settings > Connected Cards** -3. Under **Virtual Card**, tap **Show Details** - -From there, you can view your virtual card's number, CVV, expiration date, and billing address. - -Here's how to access your virtual card details via the Expensify web app: -1. Head to **Settings > Account > Credit Card Import** -2. Under **Virtual Card**, click **Show Details** - -From there, you can view your virtual card's card number, CVV, expiration date, and billing address. - -## How to add your virtual card to a digital wallet (Apple Pay or Google Pay) - -To use the Expensify Card for contactless payment, add it to your digital wallet from the mobile app: -1. Tap the three-bar icon in the upper-left corner -2. Tap **Settings > Connected Cards** -3. Depending on your device, tap **Add to Apple Wallet** or **Add to Gpay** -4. Complete the remaining steps - -## Expensify Card declines -As long as you've enabled 'Receive real-time alerts', you'll get a notification explaining the reason for each decline. You can enable alerts in the mobile app by clicking on the three-bar icon in the upper-left corner > **Settings** > toggle **Receive real-time alerts**. - -Here are some reasons an Expensify Card transaction might be declined: - -- You have an insufficient card limit - - If a transaction exceeds your Expensify Card's available limit, the transaction will be declined. You can see the remaining limit in the mobile app under **Settings > Connected Cards** or in the web app under **Settings > Account > Credit Card Import**. - - Submitting expenses and getting them approved will free up your limit for more spending. - -- Your card isn't active yet or it was disabled by your Domain Admin -- Your card information was entered incorrectly with the merchant. Entering incorrect card information, such as the CVC, ZIP, or expiration date, will also lead to declines. -There was suspicious activity -- If Expensify detects unusual or suspicious activity, we may block transactions as a security measure - - This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions. - - Check your Expensify Home page to approve unusual merchants and try again. - - If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. -- The merchant is located in a restricted country +3. Customize your alerts based on your phone model. + +## Using Your Virtual Card +- **Access Details**: You can view your virtual card details (card number, expiration date, CVC) via _**Settings > Account > Credit Card Import > Show Details**_. The virtual and physical cards share the same limit. +- **Purchases**: Use the virtual card for online, in-app, and in-person payments when linked to a mobile wallet (Apple Pay or Google Pay). + +#### Adding to a Digital Wallet +To add your Expensify Card to a digital wallet, follow the steps below: + 1. Tap the three-bar icon in the upper-left corner. + 2. Tap _**Settings > Connected Cards**_. + 3. Tap **Add to Apple Wallet** or **Add to Gpay**, depending on your device. + 4. Complete the steps as prompted. + +## Handling Declines +- **Real-Time Alerts**: Enable real-time alerts in the mobile app (_**Settings > toggle Receive real-time alerts**_) to get notifications for declines. +- **Common Decline Reasons**: + - **Insufficient Limit**: Transactions exceeding the available limit will be declined. You can check your limit in _**Settings > Connected Cards**_ or _**Settings > Account > Credit Card Import**_. + - **Inactive or Disabled Card**: Ensure your card is active and not disabled by your Domain Admin. + - **Incorrect Information**: Entering incorrect card details (CVC, ZIP, expiration date) will result in declines. + - **Suspicious Activity**: Transactions may be blocked for unusual or suspicious activity. Check the Expensify Home page to approve unusual merchants. Suspicious spending may prompt a manual due diligence check, during which your cards will be locked. + - **Restricted Country**: Transactions from restricted countries will be declined. {% include faq-begin.md %} ## Can I use Smart Limits with a free Expensify account? diff --git a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md index 19972b79d5e0..cb86c340dc81 100644 --- a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md +++ b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md @@ -1,50 +1,50 @@ --- title: Expensify Card - Transaction Disputes & Fraud -description: Learn how to dispute an Expensify Card transaction. +description: Understand how to dispute an Expensify Card transaction. --- -# Overview -When using your Expensify Visa® Commercial Card, you may come across transaction errors, which can include things like: -- Unrecognized, unauthorized, or fraudulent charges. -- Transactions of an incorrect amount. +# Disputing Expensify Card Transactions +While using your Expensify Visa® Commercial Card, you might encounter transaction errors, such as: +- Unauthorized transaction activity +- Incorrect transaction amounts. - Duplicate charges for a single transaction. -- Missing a promised merchant refund. +- Missing merchant refunds. -You’ll find all the relevant information on handling these below. +When that happens, you may need to file a dispute for one or more transactions. -# How to Navigate the Dispute Process ## Disputing a Transaction - -If you spot an Expensify Card transaction error, please contact us immediately at [concierge@expensify.com](mailto:concierge@expensify.com). After that, we'll ask a few questions to better understand the situation. If the transaction has already settled in your account (no longer pending), we can file a dispute with our card processor on your behalf. - -If you suspect fraud on your Expensify Card, don't hesitate to cancel it by heading to Settings > Account > Credit Card Import > Request A New Card. Better safe than sorry! - -Lastly, if you haven’t enabled Two-Factor Authentication (2FA) yet, please do so ASAP to add an additional layer of security to your account. +If you notice a transaction error on your Expensify Card, contact us immediately at concierge@expensify.com. We will ask a few questions to understand the situation better, and file a dispute with our card processor on your behalf. ## Types of Disputes +The most common types of disputes are: +- Unauthorized or fraudulent disputes +- Service disputes -There are two main dispute types: +### Unauthorized or fraudulent disputes +- Charges made after your card was lost or stolen. +- Unauthorized charges while your card is in your possession (indicating compromised information). +- Continued charges for a canceled recurring subscription. -1. Unauthorized charges/fraud disputes, which include: - - Charges made with your card after it was lost or stolen. - - Unauthorized charges while your card is still in your possession (indicating compromised card information). - - Continued charges for a canceled recurring subscription. +**If there are transactions made with your Expensify Card you don't recognize, you'll want to do the following right away:** +1. Cancel your card by going to _**Settings > Account > Credit Card Import > Request A New Card**_. +2. Enable Two-Factor Authentication (2FA) for added security under _**Settings > Account > Account Details > Two Factor Authentication**_. -2. Service disputes, which include: - - Received damaged or defective merchandise. - - Charged for merchandise but never received it. - - Double-charged for a purchase made with another method (e.g., cash). - - Made a return but didn't receive a timely refund. - - Multiple charges for a single transaction. - - Charges settled for an incorrect amount. +### Service Disputes +- Received damaged or defective merchandise. +- Charged for merchandise that was never received. +- Double-charged for a purchase made with another method (e.g., cash). +- Made a return but didn't receive a refund. +- Multiple charges for a single transaction. +- Charges settled for an incorrect amount. -You don't need to categorize your dispute; we'll handle that. However, this may help you assess if a situation warrants a dispute. In most cases, the initial step for resolving a dispute should be contacting the merchant, as they can often address the issue promptly. +For service disputes, contacting the merchant is often the quickest way to resolve the dispute. ## Simplifying the Dispute Process - -To ensure the dispute process goes smoothly, please: -- Provide detailed information about the disputed charge, including why you're disputing it, what occurred, and any steps you've taken to address the issue. -- If you recognize the merchant but not the charge, and you've transacted with them before, contact the merchant directly, as it may be a non-fraudulent error. -- Include supporting documentation like receipts or cancellation confirmations when submitting your dispute to enhance the likelihood of a favorable resolution (not required but highly recommended). +To ensure a smooth dispute process, please: +- Provide detailed information about the disputed charge, including why you're disputing it and any steps you've taken to address the issue. +- If you recognize the merchant but not the charge, contact the merchant directly. +- Include supporting documentation (e.g., receipts, cancellation confirmations) when submitting your dispute to increase the chances of a favorable resolution (recommended but not required). +- Make sure the transaction isn't pending (pending transactions cannot be disputed). + {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md new file mode 100644 index 000000000000..81eae56fa774 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md @@ -0,0 +1,108 @@ +--- +title: Expensify Card reconciliation +description: Reconcile expenses from Expensify Cards +--- + +
      + +To handle unapproved Expensify Card expenses that are left after you close your books for the month, you can set up auto-reconciliation with an accounting integration, or you can manually reconcile the expenses. + +# Set up automatic reconciliation + +Auto-reconciliation automatically deducts Expensify Card purchases from your company’s settlement account on a daily or monthly basis. + +{% include info.html %} +You must link a business bank account as your settlement account before you can complete this process. +{% include end-info.html %} + +1. Hover over Settings, then click **Domains**. +2. Click the desired domain name. +3. On the Company Cards tab, click the dropdown under the Imported Cards section to select the desired Expensify Card. +4. To the right of the dropdown, click the **Settings** tab. +5. Click the Expensify Card settlement account dropdown and select your settlement business bank account. + - To verify which account is your settlement account: Hover over Settings, then click **Account**. Click the **Payments** tab on the left and verify the bank account listed as the Settlement Account. If these accounts do not match, repeat the steps above to select the correct bank account. +6. Click **Save**. + +If your workspace is connected to a QuickBooks Online, Xero, NetSuite, or Sage Intacct integration, complete the following additional steps. + +1. Click the Expensify Card Reconciliation Account dropdown and select the GL account from your integration for your Settlement Account. Then click **Save**. +2. (Optional) If using the Sage Intacct integration, select your cash-only and accrual-only journals. If your organization operates on a cash-only or accrual-only basis, choose **No Selection** for the journals that do not apply. +3. Click the **Advanced** tab and ensure Auto-Sync is enabled. Then click **Save** +4. Hover over **Settings**, then click **Workspaces**. +5. Open the workspace linked to the integration. +6. Click the **Connections** tab. +7. Next to the desired integration, click **Configure**. +8. Under the Export tab, ensure that the Preferred Exporter is also a Workspace Admin and has an email address associated with your Expensify Cards' domain. For example, if your domain is company.com, the Preferred Exporter's email should be name@company.com. + +# Manually reconcile expenses + +To manually reconcile Expensify Card expenses, + +1. Hover over Settings, then click **Domains**. +2. Click the desired domain name. +3. On the Company Cards tab, click the dropdown under the Imported Cards section to select the desired Expensify Card. +4. To the right of the dropdown, click the **Reconciliation** tab. +5. For the Reconcile toggle, ensure Expenses is selected. +6. Select the start and end dates, then click **Run**. +7. Use the Imported, Approved, and Unapproved totals to manually reconcile your clearing account in your accounting system. + - The Unapproved total should match the final clearing account balance. Depending on your accounting policies, you can use this balance to book an accrual entry by debiting the appropriate expense and crediting the offsetting clearing account in your accounting system. + +## Troubleshooting + +Use the steps below to do additional research if: +- The amounts vary to a degree that needs further investigation. +- The Reconciliation tab was not run when the accounts payable (AP) was closed. +- Multiple subsidiaries within the accounting system closed on different dates. +- There are foreign currency implications in the accounting system. + +To do a more in-depth reconciliation, + +1. In your accounting system, lock your AP. + +{% include info.html %} +It’s best to do this step at the beginning or end of the day. Otherwise, expenses with the same export date may be posted in different accounting periods. +{% include end-info.html %} + +2. In Expensify, click the **Reports** tab. +3. Set the From date filter to the first day of the month or the date of the first applicable Expensify Card expense, and set the To date filter to today’s date. +4. Set the other filters to show **All**. +5. Select all of the expense reports by clicking the checkbox to the top left of the list. If you have more than 50 expense reports, click **Select All**. +6. In the top right corner of the page, click **Export To** and select **All Data - Expense Level Export**. This will generate and send a CSV report to your email. +7. Click the link from the email to automatically download a copy of the report to your computer. +8. Open the report and apply the following filters (or create a pivot with these filters) depending on whether you want to view the daily or monthly settlements: + - Daily settlements: + - Date = the month you are reconciling + - Bank = Expensify Card + - Posted Date = the month you are reconciling + - [Accounting system] Export Non Reimb = blank/after your AP lock date + - Monthly settlements: + - Date = the month you are reconciling + - Bank = Expensify Card + - Posted Date = The first date after your last settlement until the end of the month + - [Accounting system] Export Non Reimb = the current month and new month until your AP lock date + - To determine your total Expensify Card liability at the end of the month, set this filter to blank/after your AP lock date. + +This filtered list should now only include Expensify Card expenses that have a settlement/card payment entry in your accounting system but don’t have a corresponding expense entry (because they have not yet been approved in Expensify). The sum is shown at the bottom of the sheet. + +The sum of the expenses should equal the balance in your Expensify Clearing or Liability Account in your accounting system. + +# Tips + +- Enable [Scheduled Submit](https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit) to ensure that expenses are submitted regularly and on time. +- Expenses that remain unapproved for several months can complicate the reconciliation process. If you're an admin in Expensify, you can communicate with all employees who have an active Expensify account by going to [new.expensify.com](http://new.expensify.com) and using the #announce room to send a message. This way, you can remind employees to ensure their expenses are submitted and approved before the end of each month. +- Keep in mind that although Expensify Card settlements/card payments will post to your general ledger on the date it is recorded in Expensify, the payment may not be withdrawn from your bank account until the following business day. +- Based on your internal policies, you may want to accrue for the Expensify Cards. + +{% include faq-begin.md %} + +**Why is the amount in my Expensify report so different from the amount in my accounting system?** + +If the Expensify report shows an amount that is significantly different to your accounting system, there are a few ways to identify the issues: +- Double check that the expenses posted to the GL are within the correct month. Filter out these expenses to see if they now match those in the CSV report. +- Use the process outlined above to export a report of all the transactions from your Clearing (for Daily Settlement) or Liability (for monthly settlement) account, then create a pivot table to group the transactions into expenses and settlements. + - Run the settlements report in the “settlements” view of the Reconciliation Dashboard to confirm that the numbers match. + - Compare “Approved” activity to your posted activity within your accounting system to confirm the numbers match. + +{% include faq-end.md %} + +
      diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index 724745f458ef..1f412665fc2f 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -2,45 +2,36 @@ title: Request the Card description: Details on requesting the Expensify Card as an employee --- -# Overview - -Once your organization is approved for the Expensify Visa® Commercial Card, you can request a card! - -This article covers how to request, activate, and replace your physical and virtual Expensify Cards. - -# How to get your first Expensify Card - -An admin in your organization must first enable the Expensify Cards before you can receive a card. After that, an admin may assign you a card by setting a limit. You can think of setting a card limit as “unlocking” access to the card. - -If you haven’t been assigned a limit yet, look for the task on your account's homepage that says, “Ask your admin for the card!” This task allows you to message your admin team to make that request. - -Once you’re assigned a card limit, we’ll notify you via email to let you know you can request a card. A link within the notification email will take you to your account’s homepage, where you can provide your shipping address for the physical card. Enter your address, and we’ll ship the card to arrive within 3-5 business days. - -Once your physical card arrives in the mail, activate it in Expensify by entering the last four digits of the card in the activation task on your account’s homepage. - -# Virtual Card - -Once assigned a limit, a virtual card is available immediately. You can view the virtual card details via **Settings > Account > Credit Card Import > Show Details**. Feel free to begin transacting with the virtual card while your physical card is in transit – your virtual card and physical card share a limit. - -Please note that you must enable two-factor authentication on your account if you want to have the option to dispute transactions made on your virtual card. - -# Notifications - -To stay up-to-date on your card’s limit and spending activity, download the Expensify mobile app and enable push notifications. Your card is connected to your Expensify account, so each transaction on your card will trigger a push notification. We’ll also send you a push notification if we detect potentially fraudulent activity and allow you to confirm your purchase. - -# How to request a replacement Expensify Card - -You can request a new card anytime if your Expensify Card is lost, stolen, or damaged. From your Expensify account on the web, head to **Settings > Account > Credit Card Import** and click **Request a New Card**. Confirm the shipping information, complete the prompts, and your new card will arrive in 2 - 3 business days. - -Selecting the “lost” or “stolen” options will deactivate your current card to prevent potentially fraudulent activity. However, choosing the “damaged” option will leave your current card active so you can use it while the new one is shipped to you. - -If you need to cancel your Expensify Card and cannot access the website or mobile app, call our interactive voice recognition phone service (available 24/7). Call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally). - -It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. - -# Card Expiration Date - -If you notice that your card expiration date is soon, it's time for a new Expensify card. Expensify will automatically input a notification in your account's Home (Inbox) tab. This notice will ask you to input your address, but this is more if you have changed your address since your card was issued to you. You can ignore it and do nothing; the new Expensify card will ship to your address on file. The new Expensify card will have a new, unique card number and will not be associated with the old one. +To start using the Expensify Card, do the following: +1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card. +2. **Request the Card:** + - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” + - Completing that task will send an in-product notification to your admin team that you requested the card. + - Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage. + - Enter your address, and the physical card will be shipped within 3-5 business days. +3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage. + +### Virtual Cards +Once you've been assigned a limit, a virtual card is available immediately. You can view its details via _**Settings > Account > Credit Card Import > Show Details**_. + +To protect your account and card spend, enable two-factor authentication under _**Settings > Account > Account Details**_. + +### Notifications +- Download the Expensify mobile app and enable push notifications to stay updated on your card’s limit and spending. +- Each transaction triggers a push notification. +- You’ll also get notifications for potentially fraudulent activity, allowing you to confirm or dispute charges. + +## Request a Replacement Expensify Card +### If the card is lost, stolen, or damaged Card: + - Go to _**Settings > Account > Credit Card Import** and click **Request a New Card**_. + - Confirm your shipping information and complete the prompts. The new card will arrive in 2-3 business days. + - Selecting “lost” or “stolen” deactivates your current card to prevent fraud. Choosing “damaged” keeps the current card active until the new one arrives. + - If you can’t access the website or app, call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally) to cancel your card. + +### If the card is expiring +- If your card is about to expire, Expensify will notify you via your account’s Home (Inbox) tab. +- Enter your address if it has changed; otherwise, do nothing, and the new card will ship to your address on file. +- The new card will have a unique number and will not be linked to the old one. {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Statements.md b/docs/articles/expensify-classic/expensify-card/Statements.md index 894dfa3d8b9a..eb797f0cee4b 100644 --- a/docs/articles/expensify-classic/expensify-card/Statements.md +++ b/docs/articles/expensify-classic/expensify-card/Statements.md @@ -1,73 +1,62 @@ --- title: — Expensify Card Statements and Settlements -description: Learn how the Expensify Card statement and settlements work! +description: Understand how to access your Expensify Card Statement --- -# Overview -Expensify offers several settlement types and a statement that provides a detailed view of transactions and settlements. We discuss specifics on both below. +## Expensify Card Statements +Expensify offers several settlement types and a detailed statement of transactions and settlements. -# How to use Expensify Visa® Commercial Card Statement and Settlements -## Using the statement -If your domain uses the Expensify Card and you have a validated Business Bank Account, access the Expensify Card statement at Settings > Domains > Company Cards > Reconciliation Tab > Settlements. +### Accessing the Statement +- If your domain uses the Expensify Card and you have a validated Business Bank Account, access the statement at _**Settings > Domains > Company Cards > Reconciliation Tab > Settlements**_. +- The statement shows individual transactions (debits) and their corresponding settlements (credits). -The Expensify Card statement displays individual transactions (debits) and their corresponding settlements (credits). Each Expensify Cardholder has a Digital Card and a Physical Card, which are treated the same in settlement, reconciliation, and exporting to your accounting system. - -Here's a breakdown of crucial information in the statement: -- **Date:** For card payments, it shows the debit date; for card transactions, it displays the purchase date. -- **Entry ID:** This unique ID groups card payments and transactions together. -- **Withdrawn Amount:** This applies to card payments, matching the debited amount from the Business Bank Account. -- **Transaction Amount:** This applies to card transactions, matching the expense purchase amount. -- **User email:** Applies to card transactions, indicating the cardholder's Expensify email address. -- **Transaction ID:** A unique ID for locating transactions and assisting Expensify Support in case of issues. Transaction IDs are handy for reconciling pre-authorizations. To find the original purchase, locate the Transaction ID in the Settlements tab of the reconciliation dashboard, download the settlements as a CSV, and search for the Transaction ID within it. +### Key Information in the Statement +- **Date:** Debit date for card payments; purchase date for transactions. +- **Entry ID:** Unique ID grouping card payments and transactions. +- **Withdrawn Amount:** Amount debited from the Business Bank Account for card payments. +- **Transaction Amount:** Expense purchase amount for card transactions. +- **User Email:** Cardholder’s Expensify email address. +- **Transaction ID:** Unique ID for locating transactions and assisting support. ![Expanded card settlement that shows the various items that make up each card settlement.](https://help.expensify.com/assets/images/ExpensifyHelp_SettlementExpanded.png){:width="100%"} -The Expensify Card statement only shows payments from existing Business Bank Accounts under Settings > Account > Payments > Business Accounts. If a Business Account is deleted, the statement won't contain data for payments from that account. - -## Exporting your statement -When using the Expensify Card, you can export your statement to a CSV with these steps: +**Note:** The statement only includes payments from existing Business Bank Accounts under **Settings > Account > Payments > Business Accounts**. Deleted accounts' payments won't appear. - 1. Login to your account on the web app and click on Settings > Domains > Company Cards. - 2. Click the Reconciliation tab at the top right, then select Settlements. - 3. Enter your desired statement dates using the Start and End fields. - 4. Click Search to access the statement for that period. - 5. You can view the table or select Download to export it as a CSV. +## Exporting Statements +1. Log in to the web app and go to **Settings > Domains > Company Cards**. +2. Click the **Reconciliation** tab and select **Settlements**. +3. Enter the start and end dates for your statement. +4. Click **Search** to view the statement. +5. Click **Download** to export it as a CSV. ![Click the Download CSV button in the middle of the page to export your card settlements.](https://help.expensify.com/assets/images/ExpensifyHelp_SettlementExport.png){:width="100%"} ## Expensify Card Settlement Frequency -Paying your Expensify Card balance is simple with automatic settlement. There are two settlement frequency options: - - **Daily Settlement:** Your Expensify Card balance is paid in full every business day, meaning you’ll see an itemized debit each business day. - - **Monthly Settlement:** Expensify Cards are settled monthly, with your settlement date determined during the card activation process. With monthly, you’ll see only one itemized debit per month. (Available for Plaid-connected bank accounts with no recent negative balance.) +- **Daily Settlement:** Balance paid in full every business day with an itemized debit each day. +- **Monthly Settlement:** Balance settled monthly on a predetermined date with one itemized debit per month (available for Plaid-connected accounts with no recent negative balance). -## How settlement works -Each business day (Monday through Friday, excluding US bank holidays) or on your monthly settlement date, we calculate the total of posted Expensify Card transactions since the last settlement. The settlement amount represents what you must pay to bring your Expensify Card balance back to $0. +## How Settlement Works +- Each business day or on your monthly settlement date, the total of posted transactions is calculated. +- The settlement amount is withdrawn from the Verified Business Bank Account linked to the primary domain admin, resetting your card balance to $0. +- To change your settlement frequency or bank account, go to _**Settings > Domains > [Domain Name] > Company Cards**_, click the **Settings** tab, and select the new options from the dropdown menu. Click **Save** to confirm. -We'll automatically withdraw this settlement amount from the Verified Business Bank Account linked to the primary domain admin. You can set up this bank account in the web app under Settings > Account > Payments > Bank Accounts. +![Change your card settlement account or settlement frequency via the dropdown menus in the middle of the screen.](https://help.expensify.com/assets/images/ExpensifyHelp_CardSettings.png){:width="100%"} -Once the payment is made, your Expensify Card balance will be $0, and the transactions are considered "settled." -To change your settlement frequency or bank account, go to Settings > Domains > [Domain Name] > Company Cards. On the Company Cards page, click the Settings tab, choose a new settlement frequency or account from the dropdown menu, and click Save to confirm the change. +# FAQ -![Change your card settlement account or settlement frequency via the dropdown menus in the middle of the screen.](https://help.expensify.com/assets/images/ExpensifyHelp_CardSettings.png){:width="100%"} +## Can you pay your balance early if you’ve reached your Domain Limit? +- For Monthly Settlement, use the “Settle Now” button to manually initiate settlement. +- For Daily Settlement, balances settle automatically with no additional action required. -# Expensify Card Statement and Settlements FAQs -## Can you pay your balance early if you've reached your Domain Limit? -If you've chosen Monthly Settlement, you can manually initiate settlement using the "Settle Now" button. We'll settle the outstanding balance and then perform settlement again on your selected predetermined monthly settlement date. - -If you opt for Daily Settlement, the Expensify Card statement will automatically settle daily through an automatic withdrawal from your business bank account. No additional action is needed on your part. - ## Will our domain limit change if our Verified Bank Account has a higher balance? -Your domain limit may fluctuate based on your cash balance, spending patterns, and history with Expensify. Suppose you've recently transferred funds to the business bank account linked to Expensify card settlements. In that case, you should expect a change in your domain limit within 24 hours of the transfer (assuming your business bank account is connected through Plaid). - +Domain limits may change based on cash balance, spending patterns, and history with Expensify. If your bank account is connected through Plaid, expect changes within 24 hours of transferring funds. + ## How is the “Amount Owed” figure on the card list calculated? -The amount owed consists of all Expensify Card transactions, both pending and posted, since the last settlement date. The settlement amount withdrawn from your designated Verified Business Bank Account only includes posted transactions. - -Your amount owed decreases when the settlement clears. Any pending transactions that don't post timely will automatically expire, reducing your amount owed. - -## **How do I view all unsettled expenses?** -To view unsettled expenses since the last settlement, use the Reconciliation Dashboard's Expenses tab. Follow these steps: - 1. Note the dates of expenses in your last settlement. - 2. Switch to the Expenses tab on the Reconciliation Dashboard. - 3. Set the start date just after the last settled expenses and the end date to today. - 4. The Imported Total will show the outstanding amount, and you can click through to view individual expenses. +It includes all pending and posted transactions since the last settlement date. The settlement amount withdrawn only includes posted transactions. + +## How do I view all unsettled expenses? +1. Note the dates of expenses in your last settlement. +2. Go to the **Expenses** tab on the Reconciliation Dashboard. +3. Set the start date after the last settled expenses and the end date to today. +4. The **Imported Total** shows the outstanding amount, and you can click to view individual expenses. diff --git a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md index 239da6518be7..fdbc178737e1 100644 --- a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md +++ b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md @@ -66,6 +66,8 @@ There are two different limit types that are best suited for their intended purp - _Fixed limit_ spend cards are ideal for one-time expenses or providing employees access to a card for a designated purchase. - _Monthly_ limit spend cards are perfect for managing recurring expenses such as subscriptions and memberships. +A virtual card with either of these limit types doesn't share its limit with any other cards, including the cardholder's smart limit cards. + **Where can employees see their virtual cards?** Employees can see their assigned virtual cards by navigating to **Settings** > **Account** > [**Credit Cards Import**](https://www.expensify.com/settings?param=%7B%22section%22:%22creditcards%22%7D) in their account. diff --git a/docs/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings.md b/docs/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings.md new file mode 100644 index 000000000000..aea84d338934 --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings.md @@ -0,0 +1,55 @@ +--- +title: Configure Reimbursement Settings +description: Set up direct or indirect reimbursements for your workspace. +--- + + +Reimbursing employees in Expensify is quick, easy, and completely free. Let Expensify do the tedious work for you by taking advantage of the features available to automate employee reimbursement. + +# Configure a Workspace's Reimbursement Settings +There are a few ways to reimburse employees in Expensify. The option that's best suited for you and your business will depend on a few different factors: +- **Direct Reimbursement**: For companies with a business bank account located in the US that reimburse employees within the US. +- **Indirect Reimbursement**: This option is available to all members, and connecting a bank account to Expensify is not required. Indirect reimbursement indicates that all reports are reimbursed outside of Expensify. +- **Global Reimbursement**: If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you can reimburse employees directly in nearly any country worldwide. + +## Set Up Direct Reimbursement + +Once a [business bank account is connected to Expensify](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD#how-to-add-a-verified-business-bank-account), a workspace admin can enable indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Direct**. + +#### Additional features available with Direct Reimbursement: +- **Select a default reimburser for the Workspace from the dropdown menu**: + - The default reimburser will receive notifications to reimburse reports in Expensify. + - Any workspace admin who also has access to the business bank account can be added as a default reimburser. +- **Set a default withdrawal account for the Workspace**: + - The default bank account is used to reimburse all of the reports submitted on the corresponding workspace. +- **Set a manual reimbursement threshold to automate reimbursement**: + - If the total of a given report is less than the threshold set, reimbursement will occur automatically upon final approval. + - If the total of a given report is more than the threshold, it will need to be reimbursed manually. + +## Set Up Indirect Reimbursement + +A Workspace admin can enable indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Indirect**. + +**Additional features under Reimbursement > Indirect:** +If you reimburse through a separate system or through payroll, Expensify can collect and export employee bank account details for you. Reach out to your Account Manager or Concierge to have the Reimbursement Details Export format added to the account. + +## Set Up Global Reimbursement + +Once [a business bank account is connected to Expensify](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD#how-to-add-a-verified-business-bank-account), a workspace admin can enable indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Direct > Enable Global Reimbursements**. + +More information on setting up global reimbursements can be found **[here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Global-Reimbursements)**. + +{% include faq-begin.md %} + +## How do I export employee bank account details once the Reimbursement Details Export format is added to my account? + +Employee bank account details can be exported from the Reports page by selecting the relevant Approved reports and then clicking **Export to > Reimbursement Details Export**. + +## Is it possible to change the name of a verified business bank account in Expensify? + +Bank account names can be updated by going to _**Settings > Accounts > Payments**_ and clicking the pencil icon next to the bank account name. + +## What is the benefit of setting a default reimburser? + +Setting a default reimburser on the Workspace ensures that all outstanding reports are reimbursed as this member will receive notifications alerting them to reports that require their action. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Expenses.md b/docs/articles/expensify-classic/workspaces/Expense-Settings.md similarity index 61% rename from docs/articles/expensify-classic/workspaces/Expenses.md rename to docs/articles/expensify-classic/workspaces/Expense-Settings.md index 4a2dc56c430f..c3a8ab31394d 100644 --- a/docs/articles/expensify-classic/workspaces/Expenses.md +++ b/docs/articles/expensify-classic/workspaces/Expense-Settings.md @@ -2,19 +2,29 @@ title: Expensify Workspace Expense Settings description: Expense Settings --- -# Overview +Expensify offers multiple ways to customize how expenses are created and managed at the workspace level. Whether you’re using an individual workspace or managing expenses in a group workspace, there are various expense settings you can customize. -Expensify offers multiple ways to customize how expenses are created in your workspace. In this doc, you’ll learn how to set up expense basics, distance expenses, and time expenses. +# Set up the expense settings on a workspace -Whether you’re flying solo with your Individual workspace or submitting with a team on your Group workspace, we have settings to support how you use Expensify. +You can manage the expense settings on a workspace under **Settings** > **Workspaces** > **Individual** or **Group** > [_Workspace Name_] > **Expenses**. From here you can customize the following expense-level settings: +- **Violations**: When enabled, employee expenses that fall outside of workspace preferences are flagged as violations. +- **Preferences**: Configure the reimbursable and billable settings for the expenses submitted to the corresponding workspace. +- **Distance**: This is where you can set the reimbursable mileage rates for yourself or your employees. +- **Time**: Set an hourly billable rate so members of the workspace can create time expenses for reimbursement. -# How to manage expense settings in your workspace +## Violations +A workspace admin can customize the following parameters at the expense level: +- **Max Expense Age (Days)** +- **Max Expense Amount** +- **Receipt Required Amount** -Let’s cover the expense basics first! In the following sections, we’ll go through each part of managing expense settings in your workspace. +If an expense is submitted that falls outside of those parameters, Expensify will automatically detect it as a violation and alert both the expense creator and reviewer that it needs to be corrected. -## Controlling cash expenses +More information on violations can be found [**here**](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations). -A cash expense is any expense created manually or by uploading a receipt for SmartScan; a cash expense does not mean the expense was paid for with cash. The other type of expense you’ll most commonly see is credit card expenses, which means the expenses imported from a credit card or bank connection. +## Preferences + +A cash expense is any expense created manually or by uploading a receipt for SmartScan; it does not mean the expense was paid for with cash. The other type of expense you’ll most commonly see is credit card expenses, which are expenses imported from a credit card or bank connection. There are four options for cash expenses: @@ -23,7 +33,7 @@ There are four options for cash expenses: - **Forced always reimbursable** - All cash expenses are forced to be reimbursable; they cannot be marked as non-reimbursable. - **Forced always non-reimbursable** - All cash expenses are forced to be non-reimbursable; they cannot be marked as reimbursable. -## Setting up billable expenses +### Billable expenses Billable expenses refer to expenses you or your employees incur that need to be re-billed to a specific client or vendor. @@ -37,7 +47,7 @@ Under Expense Basics, you can choose the setting that is best for you. If your Group workspace is connected to Xero, QuickBooks Online, NetSuite, or Sage Intacct, you can export billable expenses to be invoiced to customers. To set this up, go to the Coding tab in the connection configuration settings. -## Using eReceipts +### eReceipts eReceipts are full digital replacements of their paper equivalents for purchases of $75 or less. @@ -46,65 +56,57 @@ Click the toggle to your preferred configuration. - **Enabled** - All imported credit card expenses in US dollars of $75 or less will have eReceipts in the receipt image. - **Disabled** - No expenses will generate an eReceipt. -Note: _We will not generate an eReceipt for lodging expenses._ +Note: Expensify will not generate an eReceipt for lodging expenses. -## Securing receipt images +### Secure receipt images Whether you’re sharing your receipts with your accountant, having an auditor review exported expenses, or simply wanting to export to keep a hard copy for yourself, receipt visibility will be an essential consideration. Under _Public Receipt Visibility_, you can determine who can view receipts on your workspace. - **Enabled** means receipts are viewable by anyone with the URL. They don't need to be an Expensify user or a workspace member to view receipts. -- **Disabled** means receipts are viewable by users of Expensify, who would have access to view the receipt in the application. You must be an Expensify user with access to the report a receipt is on and logged into your account to view a receipt image via URL. +- **Disabled** means receipts are viewable by Expensify users, who would have access to view the receipt in the application. You must be an Expensify user with access to the report a receipt is on and logged into your account to view a receipt image via URL. -## Track mileage expenses +## Distance Expenses +How to set up distance expenses: +1. Select whether you want to capture _miles_ or _kilometers_, +2. Set the default category to be used on distance expenses, +3. Click **Add A Mileage Rate** to add as many rates as you need, +4. Set the reimbursable amount per mile or kilometer. -Whether using the Individual or Group workspace, you can create distance rates to capture expenses in miles or kilometers. +**Note:** If a rate is toggled off it is immediately disabled. This means that users are no longer able to select it when creating a new distance expense. If only one rate is available then that rate will be toggled on by default. -Preliminary setup steps include: +### Track tax on mileage expenses +If you’re tracking tax in Expensify you can also track tax on distance expenses. The first step is to enable tax in the workspace. You can do this by going to **Settings** > **Workspaces** > **Individual** or **Group** > [_Workspace Name_] > **Tax**. -1. Selecting whether you want to capture _miles_ or _kilometers_, -2. Setting the default category to be used on distance expenses, -3. Click **Add A Mileage Rate** to add as many rates as you need, -4. Set the reimbursable amount per mile or kilometer. +Once tax is enabled on a workspace level you will see a toggle to _Track Tax_ in the Distance section of the workspace settings. If tax is disabled on the workspace the Track Tax toggle will not display. -Note: _If a rate is toggled off it is immediately disabled. This means that users are no longer able to select it when creating a new distance expense. If only one rate is available then that rate will be toggled on by default._ +When Track Tax is enabled, you will need to enter additional information about the rates you have set. This includes the _Tax Reclaimable on_ and _Tax Rate_ fields. With that information, Expensify will work out the correct tax reclaim for each expense. -## Set an hourly rate +If you enable tax but don’t select a tax rate or enter a tax reclaimable amount, we will not calculate any tax amount for that rate. If, at any point, you switch the tax rate or enter a different reclaimable portion for an existing distance rate, the mileage rate will need to be re-selected on expenses for the tax amount to update according to the new values. -Using Expensify you can track time-based expenses to bill your clients at an hourly rate or allow employees to claim an hourly stipend. +**Note:** Expensify won’t automatically track cumulative mileage. If you need to track cumulative mileage per employee, we recommend building a mileage report using our custom export formulas. -Click the toggle under the _Time_ section to enable the feature and set a default hourly rate. After that, you and your users will be able to create time-based expenses from the [**Expenses**](https://expensify.com/expenses) page of the account. +## Time Expenses -# Deep dives +Using Expensify you can track time-based expenses to bill your clients at an hourly rate or allow employees to claim an hourly stipend. -## What is Concierge Receipt Audit for the Control Plan? +Click the toggle under the _Time_ section to enable the feature and set a default hourly rate. Then, you and your users can create time-based expenses from the [**Expenses**](https://expensify.com/expenses) page of the account. -Concierge Receipt Audit is a real-time audit and compliance of receipts submitted by employees and workspace users. Concierge checks every receipt for accuracy and compliance, flagging any expenses that seem fishy before expense reports are even submitted for approval. All risky expenses are highlighted for manual review, leaving you with more control over and visibility into expenses. When a report is submitted and there are risky expenses on it, you will be immediately prompted to review the risky expenses and determine the next steps. +## Concierge Receipt Audit -**Why you should use Concierge Receipt Audit** +Concierge Receipt Audit is a real-time audit and compliance of receipts submitted by employees and workspace users. Concierge checks every receipt for accuracy and compliance, flagging any expenses that seem fishy before expense reports are even submitted for approval. All risky expenses are highlighted for manual review, leaving you with more control over and visibility into expenses. When a report is submitted and there are risky expenses on it, you will be immediately prompted to review the risky expenses and determine the next steps. +**Benefits of Concierge Receipt Audit** - To make sure you don't miss any risky expenses that need human oversight. - To avoid needing to manually review all your company receipts. - It's included at no extra cost with the [Control Plan](https://www.expensify.com/pricing). - Instead of paying someone to audit your company expenses or being concerned that your expenses might be audited by a government agency. -- It's easy to use! Concierge will alert you to the risky expense and present it to you in an easy-to-follow review tutorial. +- It's easy -- Concierge will alert you to the risky expense and present it to you in an easy-to-follow review tutorial. - In addition to the risky expense alerts, Expensify will include a Note with audit details on every report. -Note: _If a report has audit alerts on it, you'll need to Review the report and Accept the alerts before it can be approved._ - -## Tracking tax on mileage expenses - -If you’re tracking tax in Expensify you can also track tax on distance expenses. The first step is to enable tax in the workspace. You can do this by going to **Settings** > **Workspaces** > **Individual** or **Group** > [_Workspace Name_] > **Tax**. - -Once tax is enabled on a workspace level you will see a toggle to _Track Tax_ in the Distance section of the workspace settings. If tax is disabled on the workspace the Track Tax toggle will not display. - -When Track Tax is enabled you will need to enter additional information to the rates you have set, this includes the _Tax Reclaimable on_ and _Tax Rate_ fields. With that information, Expensify will work out the correct tax reclaim for each expense. - -If you enable tax but don’t select a tax rate or enter a tax reclaimable amount, we will not calculate any tax amount for that rate. If, at any point, you switch the tax rate or enter a different reclaimable portion for an existing distance rate, the mileage rate will need to be re-selected on expenses for the tax amount to update according to the new values. - -Note: _Expensify won’t automatically track cumulative mileage. If you need to track cumulative mileage per employee, we recommend building a mileage report using our custom export formulas._ +**Note:** If a report has audit alerts on it, you'll need to Review the report and Accept the alerts before it can be approved. {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/workspaces/Invoicing.md b/docs/articles/expensify-classic/workspaces/Invoicing.md deleted file mode 100644 index f692f8f8d62e..000000000000 --- a/docs/articles/expensify-classic/workspaces/Invoicing.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Expensify Invoicing -description: Expensify Invoicing supports your business with unlimited invoice sending and receiving, payments, and status tracking in one single location. ---- -# Overview -Expensify Invoicing lets you create and send invoices, receive payments, and track the status of your invoices with Expensify, regardless of whether your customer has an Expensify account. Invoicing is included with all Expensify subscriptions, no matter the plan — just pay the processing fee (2.9%) per transaction. - -# How to Set Up Expensify Invoicing - -**If you have a Group Workspace:** - -1. Log into your Expensify account from the web (not the mobile app) -3. Head to **Settings** > **Workspaces** > **Group** > [_Workspace Name_] > [**Invoices**](https://expensify.com/policy?param={"policyID":"20AB6A03EB9CE54D"}#invoices). - -**If you have an Individual Workspace:** - -1. Log into your Expensify account from the web (not the mobile app) -2. Head to **Settings** > **Workspaces** > **Individual** > [_Workspace Name_]> [**Invoices**](https://expensify.com/policy?param={"policyID":"BD5FB746D3B220D6"}#invoices). - -Here, you’ll be able to create a markup or add a payment account. Don’t forget you need a verified bank account to send or accept invoice payments via ACH. - -# Deep Dive - -To help your invoice stand out and look more professional, you can: - -- Add your logo -- Set your workspace currency to add default report-level fields -- Create additional report-level fields to display more details - -## Add a Logo - -From your Expensify account on the web (not the mobile app), go to **Settings** > **Account** > **Account Details**. Then click **Edit Photo** under _Your Details_ to upload your logo. - -## Set the Workspace Currency - -To set your currency, head to **Settings** > **Workspaces** > **Individual** or **Group** > **Reports**. This will add default report-level fields to your invoices. You can see these at the bottom of your [**Reports**](https://expensify.com/reports) page. - -Here are the default report-level fields based on common currencies: - -- GBP: VAT Number & Supplier Address (your company address) -- EUR: VAT Number & Supplier Address (your company address) -- AUD: ABN Number & Supplier Address (your company address) -- NZD: GST Number & Supplier Address (your company address) -- CAD: Business Number & Supplier Address (your company address) - -## Adding Additional Fields to Your Invoices - -In addition to the default report-level fields, you can create custom invoice fields. - -At the bottom of the same Reports page, under the _Add New Field_ section, you’ll have multiple options. - -- **Field Title**: This is the name of the field as displayed on your invoice. -- **Type**: You have the option to select a _text-based_ field, a _dropdown_ of selections, or a _date_ selector. -- **Report Type**: Select _Invoice_ to add the field to your invoices. - -Don’t forget to click the **Add** button once you’ve set your field parameters! - -For example, you may want to add a PO number, business address, website, or any other custom fields. - -_Please check the regulations in your local jurisdiction to ensure tax and business compliance._ - -## Removing Fields from Your Invoices - -If you want to delete a report field, click the red trashcan on the field in your **Workspace** > **Individual** or **Group** > **Report** settings to remove it from all future invoices. Unsent invoices will have a red **X** next to the report field, which you can click to remove before sending the invoice to your customer. diff --git a/docs/articles/expensify-classic/workspaces/Reimbursement.md b/docs/articles/expensify-classic/workspaces/Reimbursement.md deleted file mode 100644 index ed2384d12006..000000000000 --- a/docs/articles/expensify-classic/workspaces/Reimbursement.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Reimbursement -description: Enable reimbursement and reimburse expense reports ---- - - -# Overview -Reimbursement in Expensify is quick, easy, and completely free. Let Expensify do the tedious work for you by taking advantage of features to automate employee reimbursement. - -# How to Enable Reimbursement -There are several options for reimbursing employees in Expensify. The options available will depend on which country your business bank account is domiciled in. - -## Direct Reimbursement - -Direct reimbursement is available to companies who have a verified US bank account and are reimbursing employees within the US. To use direct reimbursement, you must have a US business bank account verified in Expensify. - -A Workspace admin can enable direct reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Direct**. - -**Additional features under Reimbursement > Direct:** - - Select a **default reimburser** for the Workspace from the dropdown menu. The default reimburser is the person who will receive notifications to reimburse reports in Expensify. You’ll be able to choose among all Workspace Admins who have access to the business bank account. - - Set a **default withdrawal account** for the Workspace. This will set a default bank account that report reimbursements are withdrawn from. - - Set a **manual reimbursement threshold** to automate reimbursement. Reports whose total falls under the manual reimbursement threshhold will be reimbursed automatocally upon final approval; reports whose total falls above the threshhold will need to be reimbursed manually by the default reimburser. - -Expensify also offers direct global reimbursement to some companies with verified bank accounts in USD, GBP, EUR and AUD who are reimbursing employees internationally. For more information about Global Reimbursement, see LINK - -## Indirect Reimbursement - -Indirect reimbursement is available to all companies in Expensify and no bank account is required. Indirect reimbursement indicates that the report will be reimbursed outside of Expensify. - -A Workspace admin can enanble indirect reimbursement via **Settings > Workspaces > Workspace Name > Reimbursement > Indirect**. - -**Additional features under Reimbursement > Indirect:** -If you reimburse through a seperate system or through payroll, Expensify can collect and export employee bank account details for you. Just reach out to your Account Manager or concierge@expensify.com for us to add the Reimbursement Details Export format to the account. - -{% include faq-begin.md %} - -## How do I export employee bank account details once the Reimbursement Details Export format is added to my account? - -Employee bank account details can be exported from the Reports page by selecting the relevant Approved reports and then clicking **Export to > Reimbursement Details Export**. - -## Is it possible to change the name of a verified business bank account in Expensify? - -Bank account names can be updated via **Settings > Accounts > Payments** and clicking the pencil icon next to the bank account name. - -## What is the benefit of setting a default reimburser? - -The main benefit of being defined as the "reimburser" in the Workspace settings is that this user will receive notifications on their Home page alerting them when reports need to be reimbursed. -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Set-Up-Invoicing.md b/docs/articles/expensify-classic/workspaces/Set-Up-Invoicing.md new file mode 100644 index 000000000000..8ec279da29a6 --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Set-Up-Invoicing.md @@ -0,0 +1,51 @@ +--- +title: Expensify Invoicing +description: Expensify Invoicing offers the ability to send, receive, and track the status of payments in one location. +--- +Invoicing lets you create and send invoices, receive payments, and track the status of your invoices, regardless of whether the customer has an Expensify account. This feature is included with all Expensify subscriptions, no matter the plan — you'll just pay the processing fee (2.9%) per transaction. + +# Set Up Expensify Invoicing +Before using the invoice feature, you'll need to [connect a business bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Business-Bank-Accounts-USD) to Expensify. + +Then, do the following: +1. Log into your Expensify account from the web (not the mobile app) +2. Head to _**Settings > Workspaces > Workspace Name > [Invoices](https://expensify.com/policy?param={"policyID":"20AB6A03EB9CE54D"}#invoices)**_. + +Here, you’ll be able to create a markup or add a payment account. + +## Add a Logo + +From your Expensify account on the web, go to _**Settings > Account > Account Details**_. Then click **Edit Photo** under _Your Details_ to upload your company logo. + +## Set the Workspace Currency + +To set the currency, head to _**Settings** > **Workspaces** > **Reports**_. This will add default report-level fields to your invoices. You can see these at the bottom of the [**Reports**](https://expensify.com/reports) page. + +Below are the default report-level fields based on common currencies: +- GBP: VAT Number & Supplier Address (your company address) +- EUR: VAT Number & Supplier Address (your company address) +- AUD: ABN Number & Supplier Address (your company address) +- NZD: GST Number & Supplier Address (your company address) +- CAD: Business Number & Supplier Address (your company address) + +## Adding Additional Fields to Invoices + +In addition to the default report-level fields, you can create custom invoice fields. + +At the bottom of the same Reports page, under the _Add New Field_ section, you’ll have multiple options. + +- **Field Title**: This is the name of the field as displayed on your invoice. +- **Type**: You have the option to select a _text-based_ field, a _dropdown_ of selections, or a _date_ selector. +- **Report Type**: Select _Invoice_ to add the field to your invoices. + +Don’t forget to click the **Add** button once you’ve set your field parameters! + +For example, you may want to add a PO number, business address, website, or any other custom fields. + +_Please check the regulations in your local jurisdiction to ensure tax and business compliance._ + +## Removing Fields from Invoices + +If you want to delete a report field, click the red trashcan on the field under _**Settings** > **Workspaces** > **Reports**_. This will remove that field from all future invoices. + +Unsent invoices will have a red **X** next to the report field, which you can click to remove before sending the invoice to your customer. diff --git a/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md b/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md new file mode 100644 index 000000000000..5c6678e068be --- /dev/null +++ b/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md @@ -0,0 +1,375 @@ +--- +title: Set up NetSuite connection +description: Integrate NetSuite with Expensify +--- +
      + +# Connect to NetSuite + +## Overview +Expensify’s integration with NetSuite allows you to sync data between the two systems. Before you start connecting Expensify with NetSuite, there are a few things to note: + +- You must use NetSuite administrator credentials to initiate the connection +- A Control Plan in Expensify is required to integrate with NetSuite +- Employees don’t need NetSuite access or a NetSuite license to submit expense reports and sync them to NetSuite +- Each NetSuite subsidiary must be connected to a separate Expensify workspace +- The workspace currency in Expensify must match the NetSuite subsidiary's default currency + +## Step 1: Install the Expensify Bundle in NetSuite +1. While logged into NetSuite as an administrator, go to **Customization > SuiteBundler > Search & Install Bundles**, then search for “Expensify” +2. Click on the Expensify Connect bundle (Bundle ID 283395) +3. Click **Install** +4. If you already have the Expensify Connect bundle installed, head to **Customization > SuiteBundler > Search & Install Bundles > List**, and update it to the latest version +5. Select "Show on Existing Custom Forms" for all available fields + +## Step 2: Enable Token-Based Authentication +1. In NetSuite, go to **Setup > Company > Enable Features > SuiteCloud > Manage Authentication** +2. Make sure “Token Based Authentication” is enabled +3. Click **Save** + + +## Step 3: Add Expensify Integration Role to a User +1. In NetSuite, head to **Lists > Employees**, and find the user who you would like to add the Expensify Integration role to. The user you select must at least have access to the permissions included in the Expensify Integration Role, and Admin access works too, but Admin access is not required. +2. Click **Edit > Access**, then find the Expensify Integration role in the dropdown and add it to the user +3. Click **Save** + +Remember that Tokens are linked to a User and a Role, not solely to a User. It’s important to note that you cannot establish a connection with tokens using one role and then switch to another role afterward. Once you’ve initiated a connection with tokens, you must continue using the same token/user/role combination for all subsequent sync or export actions. + +## Step 4: Create Access Tokens +1. In NetSuite, enter “page: tokens” in the Global Search +2. Click **New Access Token** +3. Select Expensify as the application (this must be the original Expensify integration from the bundle) +4. Select the role Expensify Integration +5. Click **Save** +6. Copy and paste the token and token ID to a saved location on your computer (this is the only time you will see these details) + +## Step 5: Confirm Expense Reports are enabled in NetSuite +Expense Reports must be enabled in order to use Expensify’s integration with NetSuite. + +1. In NetSuite, go to **Setup > Company > Enable Features > Employees** +2. Confirm the checkbox next to "Expense Reports" is checked +3. If not, click the checkbox and then click **Save** to enable Expense Reports + +## Step 6: Confirm Expense Categories are set up in NetSuite +Once Expense Reports are enabled, Expense Categories can be set up in NetSuite. Expense Categories are synced to Expensify as Categories. Each Expense Category is an alias mapped to a General Ledger account so that employees can more easily categorize expenses. + +1. In NetSuite, go to **Setup > Accounting > Expense Categories** (a list of Expense Categories should show) +2. If no Expense Categories are visible, click **New** to create new ones + +## Step 7: Confirm Journal Entry Transaction Forms are Configured Properly +1. In NetSuite, go to **Customization > Forms > Transaction Forms** +2. Click **Customize** or **Edit** next to the Standard Journal Entry form +3. Click **Screen Fields > Main**. Please verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal" +4. Click the sub-header **Lines** and verify that the “Show” column for “Receipt URL” is checked +5. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the journal type have this same configuration + +## Step 8: Confirm Expense Report Transaction Forms are Configured Properly +1. In NetSuite, go to **Customization > Forms > Transaction Forms** +2. Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main** +3. Verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal" +4. Click the second sub-header, **Expenses**, and verify that the "Show" column for "Receipt URL" is checked +5. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the expense report type have this same configuration + +## Step 9: Confirm Vendor Bill Transactions Forms are Configured Properly +1. In NetSuite, go to **Customization > Forms > Transaction Forms** +2. Click **Customize** or **Edit** next to your preferred Vendor Bill form +3. Click **Screen Fields > Main** and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked +4. Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class +5. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the vendor bill type have this same configuration + +## Step 10: Confirm Vendor Credit Transactions Forms are Configured Properly +1. In NetSuite, go to **Customization > Forms > Transaction Forms** +2. Click **Customize** or **Edit** next to your preferred Vendor Credit form, then click **Screen Fields > Main** and verify that the “Created From” label has “Show” checked and that Departments, Classes, and Locations have the “Show” label unchecked +3. Under the **Expenses** sub-header (make sure to click the “Expenses” sub-header at the very bottom and not “Expenses & Items”), ensure “Show” is checked for Receipt URL, Department, Location, and Class +4. Go to **Customization > Forms > Transaction Forms** and ensure that all other transaction forms with the vendor credit type have this same configuration + +## Step 11: Set up Tax Groups (only applicable if tracking taxes) +Expensify imports NetSuite Tax Groups (not Tax Codes), which you can find in NetSuite under **Setup > Accounting > Tax Groups**. + +Tax Groups are an alias for Tax Codes in NetSuite and can contain one or more Tax Codes (Please note: for UK and Ireland subsidiaries, please ensure your Tax Groups do not have more than one Tax Code). We recommend naming Tax Groups so your employees can easily understand them, as the name and rate will be displayed in Expensify. + +To set up Tax Groups in NetSuite: + +1. Go to **Setup > Accounting > Tax Groups** +2. Click **New** +3. Select the country for your Tax Group +4. Enter the Tax Name (this is what employees will see in Expensify) +5. Select the subsidiary for this Tax Group +6. Select the Tax Code from the table you wish to include in this Tax Group +7. Click **Add** +8. Click **Save** +9. Create one NetSuite Tax Group for each tax rate you want to show in Expensify + +Ensure Tax Groups can be applied to expenses by going to **Setup > Accounting > Set Up Taxes** and setting the Tax Code Lists Include preference to “Tax Groups And Tax Codes” or “Tax Groups Only.” If this field does not display, it’s not needed for that specific country. + +## Step 12: Connect Expensify to NetSuite +1. Log into Expensify as a workspace admin +2. Click your profile image or icon in the bottom left menu +3. Scroll down and click **Workspaces** in the left menu +4. Select the workspace you want to connect to NetSuite +5. Click **More features** in the left menu +6. Scroll down to the Integrate section and enable Accounting +7. Click **Accounting** in the left menu +8. Click **Set up** next to NetSuite +9. Click **Next** until you reach setup step 5 (If you followed the instructions above, then the first four setup steps will be complete) +10. On setup step 5, enter your NetSuite Account ID, Token ID, and Token Secret (the NetSuite Account ID can be found in NetSuite by going to **Setup > Integration > Web Services Preferences**) +11. Click **Confirm** to complete the setup + +After completing the setup, the NetSuite connection will sync. It can take 1-2 minutes to sync with NetSuite. + +Once connected, all reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration). + +## FAQ +### What type of Expensify plan is required to connect to NetSuite? +You need a Control workspace to integrate with NetSuite. If you have a Collect workspace, you will need to upgrade to Control. + +### Page size +Make sure your page size is set to 1000 in NetSuite for importing your customers and vendors. Go to **Setup > Integration > Web Services Preferences** and search **Page Size** to determine your page size. + + +# Configure NetSuite integration +## Step 1: Configure import settings + +The following section will help you determine how data will be imported from NetSuite into Expensify. To change your import settings, navigate to the Accounting settings for your workspace, then click **Import** under the NetSuite connection. + +### Expense Categories +Your NetSuite Expense Categories are automatically imported into Expensify as categories. This cannot be amended, and any new categories you'd like to add must be added as Expense Categories in NetSuite. + +Once imported, you can turn specific Categories on or off under **Settings > Workspaces > [Workspace Name] > Categories**. + +### Departments, Classes, and Locations +The NetSuite integration allows you to import departments, classes, and locations from NetSuite into Expensify as Tags, Report Fields, or using the NetSuite Employee Default. + +- **NetSuite Employee Default:** If default Department, Class, and Locations have been configured on NetSuite employee records, then you can choose to have the NetSuite employee default applied upon export from Expensify to NetSuite. With this selection, employees will not make a selection in Expensify. +- **Tags:** Employees can select the department, class, or location on each individual expense. If the employee's NetSuite employee record has a default value, then each expense will be defaulted to that tag upon creation, with the option for the employee to select a different value on each expense. +- **Report Fields:** Employees can select one department/class/location for each expense report. + + +New departments, classes, and locations must be added in NetSuite. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**. + +### Customers and Projects +The NetSuite integration allows you to import customers and projects into Expensify as Tags or Report Fields. + +- **Tags:** Employees can select the customer or project on each individual expense. +- **Report Fields:** Employees can select one department/class/location for each expense report. + +New customers and projects must be added in NetSuite. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**. + +When importing customers or projects, you can also choose to enable **Cross-subsidiary customers/projects**. This setting allows you to import Customers and Projects across all NetSuite subsidiaries to a single Expensify workspace. This setting requires you to enable “Intercompany Time and Expense” in NetSuite. To enable that feature in NetSuite, go to **Setup > Company > Setup Tasks: Enable Features > Advanced Features**. + +### Tax +The NetSuite integration allows users to apply a tax rate and amount to each expense for non-US NetSuite subsidiaries. To do this, import Tax Groups from NetSuite: + +1. In NetSuite, head to **Setup > Accounting > Tax Groups** +2. Once imported, go to the NetSuite connection configuration page in Expensify (under **Settings > Workspaces > [Workspace Name] > Accounting > NetSuite > Import**) +3. Enable Tax +4. Go back to the Accounting screen, click the three dots next to NetSuite, and click **Sync now** +5. All Tax Groups for the connected NetSuite subsidiary will be imported to Expensify as taxes. +6. After syncing, go to **Settings > Workspace > [Workspace Name] > Tax** to see the tax groups imported from NetSuite + +### Custom Segments +You can import one or more Custom Segments from NetSuite for selection in Expensify. To add a Custom Segment to your Expensify workspace: + +1. Go to **Settings > Workspaces > [Workspace Name] > Accounting** +2. Click **Import** under NetSuite +3. Click **Custom segments/records** +4. Click **Add custom segment/record** + +From there, you'll walk through a simple setup wizard. You can find detailed instructions below for each setup step. + +1. In Step 1, you'll select whether you'd like to import a custom segment or a custom record. For a Custom Segment, continue. We have separate instructions for [Custom Records](link) and [Custom Lists](link). +2. **Segment Name** + a. Log into NetSuite as an administrator + b. Go to **Customization > Lists, Records, & Fields > Custom Segments** + c. You’ll see the Segment Name on the Custom Segments page +3. Internal ID + a. Ensure you have internal IDs enabled in NetSuite under **Home > Set Preferences** + b. Navigate back to the **Custom Segments** page + c. Click the **Custom Record Type** link + d. You’ll see the Internal ID on the Custom Record Type page +4. **Script ID/Field ID** + a. If configuring Custom Segments as Report Fields, use the Field ID on the Transactions tab (under **Custom Segments > Transactions**). If no Field ID is shown, use the unified ID (just called “ID” right below the “Label”). + b. If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under **Custom Segments > Transaction Columns**). If no Field ID is shown, use the unified ID (just called “ID” right below the “Label”). + c. Note that as of 2019.1, any new custom segments that you create automatically use the unified ID, and the "Use as Field ID" box is not visible. If you are editing a custom segment definition that was created before 2019.1, the "Use as Field ID" box is available. To use a unified ID for the entire custom segment definition, check the "Use as Field ID" box. When the box is checked, no field ID fields or columns are shown on the Application & Sourcing subtabs because one ID is used for all fields. +5. Select whether you'd like to import the custom segment as Tags or Report Fields +6. Finally, confirm that all the details look correct + +**Note:** Don’t use the “Filtered by” feature available for Custom Segments. Expensify can’t make these dependent on other fields. If you do have a filter selected, we suggest switching that filter in NetSuite to “Subsidiary” and enabling all subsidiaries to ensure you don’t receive any errors upon exporting reports. + +### Custom Records +You can import one or more Custom Records from NetSuite for selection in Expensify. To add a Custom Record to your Expensify workspace: + +1. Go to **Settings > Workspaces > [Workspace Name] > Accounting** +2. Click **Import** under NetSuite +3. Click **Custom segments/records** +4. Click **Add custom segment/record** + +From there, you'll walk through a simple setup wizard. You can find detailed instructions below for each setup step. + +1. In Step 1, you'll select whether you'd like to import a custom segment or a custom record. For a Custom Record, continue. We have separate instructions for [Custom Segments](link) and [Custom Lists](link). +2. **Segment Name** + a. Log into NetSuite as an administrator + b. Go to **Customization > Lists, Records, & Fields > Custom Segments** + c. You’ll see the Custom Record Name on the Custom Segments page +3. **Internal ID** + a. Make sure you have Internal IDs enabled in NetSuite under **Home > Set Preferences** + b. Navigate back to the **Custom Segment** page + c. Click the **Custom Record Type** hyperlink + d. You’ll see the Internal ID on the Custom Record Type page +4. **Transaction Column ID** + a. If configuring Custom Records as Report Fields, use the Field ID on the Transactions tab (under **Custom Segments > Transactions**). + b. If configuring Custom Records as Tags, use the Field ID on the Transaction Columns tab (under **Custom Segments > Transaction Columns**). +5. Select whether you'd like to import the custom record as Tags or Report Fields +6. Finally, confirm that all the details look correct + +### Custom Lists +You can import one or more Custom Lists from NetSuite for selection in Expensify. To add a Custom List to your Expensify workspace: + +1. Go to **Settings > Workspaces > [Workspace Name] > Accounting** +2. Click **Import** under NetSuite +3. Click **Custom list** +4. Click **Add custom list** + +From there, you'll walk through a simple setup wizard. You can find detailed instructions below for each setup step. + +1. In Step 1, you'll select which Custom List you'd like to import from a pre-populated list +2. **Transaction Line Field ID** + a. Log into NetSuite as an admin + b. Search **“Transaction Line Fields”** in the global search + c. Click into the desired Custom List + d. You'll find the transaction Line Field ID along the left-hand side of the page +3. Select whether you'd like to import the custom list as Tags or Report Fields +4. Finally, confirm that all the details look correct + +From there, you should see the values for the Custom Lists under the Tag or Report Field settings in Expensify. +## Step 2: Configure export settings +There are numerous options for exporting data from Expensify to NetSuite. To access these settings, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Export** under NetSuite. + +### Preferred Exporter +Any workspace admin can export reports to NetSuite. For auto-export, Concierge will export on behalf of the preferred exporter. The preferred exporter will also be notified of any expense reports that fail to export to NetSuite due to an error. + +### Date +You can choose which date to use for the records created in NetSuite. There are three date options: + +1. **Date of last expense:** This will use the date of the previous expense on the report +2. **Submitted date:** The date the employee submitted the report +3. **Exported date:** The date you export the report to NetSuite + +### Export out-of-pocket expenses as +**Expense Reports** +Out-of-pocket expenses will be exported to NetSuite as expense reports, which will be posted to the payables account designated in NetSuite. + +**Vendor Bills** +Out-of-pocket expenses will be exported to NetSuite as vendor bills. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills. + +**Journal Entries** +Out-of-pocket expenses will be exported to NetSuite as journal entries. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries. + +Note: By default, journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option. Also, The credit line and header level classifications are pulled from the employee record. + +### Export company card expenses as +**Expense Reports** +To export company card expenses as expense reports, you will need to configure your default corporate cards in NetSuite. To do this, you must select the correct card on the NetSuite employee records (for individual accounts) or the subsidiary record (If you use a non-One World account, the default is found in your accounting preferences). + +To update your expense report transaction form in NetSuite: + +1. Go to **Customization > Forms > Transaction Forms** +2. Click **Edit** next to the preferred expense report form +3. Go to the **Screen Fields > Main** tab +4. Check “Show” for "Account for Corporate Card Expenses" +5. Go to the **Screen Fields > Expenses** tab +6. Check “Show” for "Corporate Card" + +You can also select the default account on your employee record to use individual corporate cards for each employee. Make sure you add this field to your employee entity form in NetSuite. If you have multiple cards assigned to a single employee, you cannot export to each account. You can only have a single default per employee record. + +**Vendor Bills** +Company card expenses will be posted as a vendor bill payable to the default vendor specified in your workspace Accounting settings. You can also set an approval level in NetSuite for the bills. + + +**Journal Entries** +Company Card expenses will be posted to the Journal Entries posting account selected in your workspace Accounting settings. + +Important Notes: + +- Expensify Card expenses will always export as Journal Entries, even if you have Expense Reports or Vendor Bills configured for non-reimbursable expenses on the Export tab +- Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option +- The credit line and header level classifications are pulled from the employee record + +### Export invoices to +Select the Accounts Receivable account where you want your Invoice reports to export. In NetSuite, the invoices are linked to the customer, corresponding to the email address where the invoice was sent. + +### Export foreign currency amount +Enabling this feature allows you to send the original amount of the expense rather than the converted total when exporting to NetSuite. This option is only available when exporting out-of-pocket expenses as Expense Reports. + +### Export to next open period +When this feature is enabled and you try exporting an expense report to a closed NetSuite period, we will automatically export to the next open period instead of returning an error. + + +## Step 3: Configure advanced settings +To access the advanced settings of the NetSuite integration, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Advanced** under NetSuite. + + +Let’s review the different advanced settings and how they interact with the integration. + +### Auto-sync +We strongly recommend enabling auto-sync to ensure that the information in NetSuite and Expensify is always in sync. The following will occur when auto-sync is enabled: + +**Daily sync from NetSuite to Expensify:** Once a day, Expensify will sync any changes from NetSuite into Expensify. This includes any new, updated, or removed departments/classes/locations/projects/etc. + +**Auto-export:** When an expense report reaches its final state in Expensify, it will be automatically exported to NetSuite. The final state will either be reimbursement (if you reimburse members through Expensify) or final approval (if you reimburse members outside of Expensify). + +**Reimbursement-sync:** If Sync Reimbursed Reports (more details below) is enabled, then we will sync the reimbursement status of reports between Expensify and NetSuite. + +### Sync reimbursed reports +When Sync reimbursed reports is enabled, the reimbursement status will be synced between Expensify and NetSuite. + +**If you reimburse members through Expensify:** Reimbursing an expense report will trigger auto-export to NetSuite. When the expense report is exported to NetSuite, a corresponding bill payment will also be created in NetSuite. + +**If you reimburse members outside of Expensify:** Expense reports will be exported to NetSuite at time of final approval. After you mark the report as paid in NetSuite, the reimbursed status will be synced back to Expensify the next time the integration syncs. + +To ensure this feature works properly for expense reports, make sure that the reimbursement account you choose within the settings matches the default account for Bill Payments in NetSuite. When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting. + +### Invite employees and set approvals +Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will send them an email letting them know they’ve been added to a workspace. + +In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic: + +- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. +- **Manager Approval (default):** Two levels of approval route reports first to an employee’s NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page. +- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace’s People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > [Workspace Name] > People** page. + +### Auto-create employees/vendors +With this feature enabled, Expensify will automatically create a new employee or vendor in NetSuite (if one doesn’t already exist) using the name and email of the report submitter. + +### Enable newly imported categories +With this feature enabled, anytime a new Expense Category is created in NetSuite, it will be imported into Expensify as an enabled category. If the feature is disabled, then new Expense Categories will be imported into Expensify as disabled. + +### Setting approval levels +You can set the NetSuite approval level for each different export type: + +- **Expense report approval level:** Choose from "NetSuite default preference," “Only supervisor approved,” “Only accounting approved,” or “Supervisor and accounting approved.” +- **Vendor bill approval level and Journal entry approval level:** Choose from "NetSuite default preference," “Pending approval,” or “Approved for posting.” + +If you have Approval Routing selected in your accounting preference, this will override the selections in Expensify. If you do not wish to use Approval Routing in NetSuite, go to **Setup > Accounting > Accounting Preferences > Approval Routing** and ensure Vendor Bills and Journal Entries are not selected. + +### Custom form ID +By default, Expensify will create entries using the preferred transaction form set in NetSuite. Alternatively, you have the option to designate a specific transaction form to be used. + + + +## FAQ + +### How does Auto-sync work with reimbursed reports? +If a report is reimbursed via ACH or marked as reimbursed in Expensify and then exported to NetSuite, the report is automatically marked as paid in NetSuite. + +If a report is exported to NetSuite, then marked as paid in NetSuite, the report will automatically be marked as reimbursed in Expensify during the next sync. + +### Will enabling auto-sync affect existing approved and reimbursed reports? +Auto-sync will only export newly approved reports to NetSuite. Any reports that were approved or reimbursed before enabling auto-sync will need to be manually exported in order to sync them to NetSuite. + + +### When using multi-currency features in NetSuite, can expenses be exported with any currency? +When using multi-currency features with NetSuite, remember these points: + +**Employee/Vendor currency:** The currency set for a NetSuite vendor or employee record must match the subsidiary currency for whichever subsidiary you export that user's reports to. A currency mismatch will cause export errors. +**Bank Account Currency:** When synchronizing bill payments, your bank account’s currency must match the subsidiary’s currency. Failure to do so will result in an “Invalid Account” error. diff --git a/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md b/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md new file mode 100644 index 000000000000..1f5d9662bb4f --- /dev/null +++ b/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md @@ -0,0 +1,317 @@ +--- +title: Set up Sage Intacct connection +description: Integrate Sage Intacct with Expensify +--- +
      + +# Connect to Sage Intacct + +## Overview +Expensify’s integration with Sage Intacct allows you to connect using either role-based permissions or user-based permissions and exporting either expense reports or vendor bills. + +Checklist of items to complete: + +1. Create a web services user and configure permissions +1. Enable the T&E module (only required if exporting out-of-pocket expenses as Expense Reports) +1. Set up Employees in Sage Intacct (only required if exporting expenses as Expense Reports) +1. Set up Expense Types (only required if exporting expenses as Expense Reports) +1. Enable Customization Services +1. Download the Expensify Package +1. Upload the Expensify Package in Sage Intacct +1. Add web services authorization +1. Enter credentials and connect Expensify and Sage Intacct +1. Configure integration sync options + +## Step 1a: Create a web services user (Connecting with User-based permissions) +Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, follow the steps [here]. + +To connect to Sage Intacct, you’ll need to create a special web services user (please note that Sage Intacct does not charge extra for web services users). + +1. Go to **Company > Web Services Users > New**. +2. Configure the user as outlined below: + - **User ID**: “xmlgateway_expensify” + - **Last Name and First Name:** “Expensify” + - **Email Address:** Your shared accounting team email + - **User Type:** “Business” + - **Admin Privileges:** “Full” + - **Status:** “Active” + +Next, configure correct permissions for the new web services user. + +1. Go to the subscription link for this user in the user list +1. Click on the checkbox next to the Application/Module +1. Click **Permissions** + +These are the permissions required for this integration when exporting out-of-pocket expenses as vendor bills: + +- **Administration (All)** +- **Company (Read-only)** +- **Cash Management (All)** +- **General Ledger (All)** +- **Time & Expense (All)** - Only required if exporting out-of-pocket expenses as expense reports +- **Projects (Read-only)** - Only required if using Projects or Customers +- **Accounts Payable (All)** - Only required if exporting any expenses expenses as vendor bills + +## Step 1b: Create a web services user (Connecting with Role-based permissions) +Note: If the steps in this section look different in your Sage Intacct instance, you likely use role-based permissions. If that’s the case, follow the steps [here]. + +**First, you'll need to create the new role:** + +1. In Sage Intacct, click **Company**, then click on the **+ button** next to Roles +1. Name the role "Expensify", then click **Save** +1. Go to **Roles > Subscriptions** and find the “Expensify” role you just created +1. Configure correct permissions for this role by clicking the checkbox and then clicking on the Permissions hyperlink. These are the permissions required for this integration when exporting out-of-pocket expenses as vendor bills: + - **Administration (All)** + - **Company (Read-only)** + - **Cash Management (All)** + - **General Ledger (All)** + - **Time & Expense (All)** - Only required if exporting out-of-pocket expenses as expense reports + - **Projects (Read-only)** - Only required if using Projects or Customers + - **Accounts Payable (All)** - Only required if exporting any expenses expenses as vendor bills + +**Next, you’ll create a web services user and assign the role to that user:** + +1. Go to **Company > Web Services Users > New** +2. Set up the user as described below: + - **User ID:** “xmlgateway_expensify” + - **Last name and First name:** “Expensify” + - **Email address:** your shared accounting team email + - **User type:** “Business” + - **Admin privileges:** “Full” + - **Status:** “Active” +3. Assign the role to that user: click the **+ button**, then select the “Expensify” role and click **Save** + + +## Step 2: Enable and configure the Time & Expenses Module +**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.** + +Enabling the T&E module is a paid subscription through Sage Intacct and the T&E module is often included in your Sage Intacct instance. For information on the costs of enabling this module, please contact your Sage Intacct account manager. + +**To enable the Time & Expenses module:** + +In Sage Intacct, go to **Company menu > Subscriptions > Time & Expenses** and toggle the switch to subscribe. + +**After enabling T&E, configure it as follows:** + +1. Ensure that **Expense types** is checked +2. Under **Auto-numbering sequences** set the following: + - **Expense Report:** EXP + - **Employee:** EMP + - **Duplicate Numbers:** Select “Do not allow creation” + - To create the EXP sequence, click on the down arrow on the expense report line and select **Add: + 1. **Sequence ID:** EXP + 1. **Print Title:** EXPENSE REPORT + 1. **Starting Number:** 1 + 1. **Next Number:** 2 +3. Select **Advanced Settings** and configure the following: + a. **Fixed Number Length:** 4 + b. **Fixed Prefix:** EXP +4. Click **Save** +5. Under Expense Report approval settings, ensure that **Enable expense report approval** is unchecked +6. Click **Save** to confirm your configuration + + +## Step 3: Set up Employees in Sage Intacct +**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.** + +To set up employees in Sage Intacct: + +1. Navigate to Time & Expenses and click the **plus button** next to Employees. + - If you don’t see the Time & Expense option in the top ribbon, you may need to adjust your settings. Go to **Company > Roles > Time & Expenses** and enable all permissions. +2. To create an employee, you’ll need to provide the following information: + - Employee ID + - Primary contact name + - Email address + 1. In the "Primary contact name" field, click the dropdown arrow. + 1. Select the employee if they’ve already been created. + 1. Otherwise, click **+ Add** to create a new employee. + 1. Fill in their Primary Email Address along with any other required information + + +## Step 4: Set up Expense Types in Sage Intacct +**Note: This step is only required if exporting out-of-pocket expenses from Expensify to Sage Intacct as Expense Reports.** + +Expense Types provide a user-friendly way to display the names of your expense accounts to your employees. To set up expense types in Sage Intacct: + +1. **Setup Your Chart of Accounts** + - Before configuring Expense Types, ensure your Chart of Accounts is set up. You can set up accounts in bulk by going to **Company > Open Setup > Company Setup Checklist** and clicking **Import**. +2. **Set up Expense Types** + - Go to Time & Expense + - Open Setup and click the **plus button** next to Expense Types +3. For each Expense Type, provide the following information: + - **Expense Type** + - **Description** + - **Account Number** (from your General Ledger) + +## Step 5: Enable Customization Services +**Note:** If you already have Platform Services enabled, you can skip this step. + +To enable Customization Services, go to **Company > Subscriptions > Customization Services**. + + +## Step 6: Download the Expensify Package +1. In Expensify, go to Settings > Workspaces +1. Click into the workspace where you'd like to connect to Sage Intacct + - If you already use Expensify, you can optionally create a test workspace by clicking **New Workspace** at the top-right of the Workspaces page. A test workspace allows you to have a sandbox environment for testing before implementing the integration live. +1. Go to **Connections > Sage Intacct > Connect to Sage Intacct** +1. Select **Download Package** (You only need to download the file; we’ll upload it from your Downloads folder later) + +## Step 7: Upload Package in Sage Intacct +If you use Customization Services: + +1. Go to **Customization Services > Custom Packages > New Package** +1. Click on **Choose File** and select the Package file from your downloads folder +1. Click **Import** + +If you use Platform Services: + +1. Go to **Platform Services > Custom Packages > New Package** +1. Click on **Choose File** and select the Package file from your downloads folder +1. Click **Import** + + +## Step 8: Add Web Services Authorization +1. Go to **Company > Company Info > Security** in Sage Intacct and click **Edit** +2. Scroll down to **Web Services Authorizations** and add “expensify” (all lower case) as a Sender ID + +## Step 9: Enter Credentials and Connect Expensify and Sage Intacct +1. In Expensify, go to **Settings > Workspaces > [Workspace Name] > Accounting** +1. Click **Set up** next to Sage Intacct +1. Enter the credentials you set for your web services user in Step 1 +1. Click **Confirm** + + + +# Configure Sage Intacct integration + +## Step 1: Select entity (multi-entity setups only) +If you have a multi-entity setup in Sage Intacct, you will be able to select in Expensify which Sage Intacct entity to connect each workspace to. Each Expensify workspace can either be connected to a single entity or connected at the Top Level. + +To select or change the Sage Intacct entity that your Expensify workspace is connected to, navigate to the Accounting settings for your workspace and click **Entity** under the Sage Intacct connection. + +## Step 2: Configure import settings +The following section will help you determine how data will be imported from Sage Intacct into Expensify. To change your import settings, navigate to the Accounting settings for your workspace, then click **Import** under the Sage Intacct connection. + +### Expense Types / Chart of Accounts +The categories in Expensify depend on how you choose to export out-of-pocket expenses: + +- If you choose to export out-of-pocket expenses as Expense Reports, your categories in Expensify will be imported from your Sage Intacct Expense Types +- If you choose to export out-of-pocket expenses as Vendor Bills, your categories will be imported directly from your Chart of Accounts (also known as GL Codes or Account Codes). + +You can disable unnecessary categories in Expensify by going to **Settings > Workspaces > [Workspace Name] > Categories**. Note that every expense must be coded with a Category, or it will fail to export. + +### Billable Expenses +Enabling billable expenses allows you to map your expense types or accounts to items in Sage Intacct. To do this, you’ll need to enable the correct permissions on your Sage Intacct user or role. This may vary based on the modules you use in Sage Intacct, so you should enable read-only permissions for relevant modules such as Projects, Purchasing, Inventory Control, and Order Entry. + +Once permissions are set, you can map categories to specific items, which will then export to Sage Intacct. When an expense is marked as Billable in Expensify, users must select the correct billable Category (Item), or there will be an error during export. + + +### Standard dimensions: Departments, Classes, and Locations +The Sage Intacct integration allows you to import standard dimensions into Expensify as tags, report fields, or using the Sage Intacct employee default. + +- **Sage Intacct Employee default:** This option is only available when exporting as expense reports. When this option is selected, nothing will be imported into Expensify - instead, the employee default will be applied to each expense upon export. +- **Tags:** Employees can select the department, class, or location on each individual expense. If the employee's Sage Intacct employee record has a default value, then each expense will default to that tag, with the option for the employee to select a different value on each expense. +- **Report Fields:** Employees can select one department/class/location for each expense report. + +New departments, classes, and locations must be added in Sage Intacct. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**. + +Please note that when importing departments as tags, expense reports may show the tag name as "Tag" instead of "Department." + +### Customers and Projects +The Sage Intacct integration allows you to import customers and projects into Expensify as Tags or Report Fields. + +- **Tags:** Employees can select the customer or project on each individual expense. +- **Report Fields:** Employees can select one department/class/location for each expense report. + +New customers and projects must be added in Sage Intacct. Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**. + + +### Tax +The Sage Intacct integration supports native VAT and GST tax. To enable this feature, go to **Settings > Workspaces > [Workspace Name] > Accounting**, click **Import** under Sage Intacct, and enable Tax. Enabling this option will import your native tax rates from Sage Intacct into Expensify. From there, you can select default rates for each category under **Settings > Workspaces > [Workspace Name] > Categories**. + +For older Sage Intacct connections that don't show the Tax option, simply resync the connection by going to **Settings > Workspaces > [Workspace Name] > Accounting** and clicking the three dots next to Sage Intacct, and the tax toggle will appear. + +### User-Defined Dimensions +You can add User-Defined Dimensions (UDDs) to your workspace by locating the “Integration Name” in Sage Intacct. Please note that you must be logged in as an administrator in Sage Intacct to find the required fields. + +To find the Integration Name in Sage Intacct: + +1. Go to **Platform Services > Objects > List** +1. Set “filter by application” to “user-defined dimensions” +1. In Expensify, go to **Settings > Workspaces > [Workspace Name] > Accounting** and click **Import** under Sage Intacct +1. Enable User Defined Dimensions +1. Enter the “Integration name” and choose whether to import it into Expensify as expense-level tags or as report fields +1. Click **Save** + +Once imported, you can turn specific tags on or off under **Settings > Workspaces > [Workspace Name] > Tags**. You can turn specific report fields on or off under **Settings > Workspaces > [Workspace Name] > Report Fields**. + + +## Step 5: Configure export settings +To access export settings, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Export** under Sage Intacct. + +### Preferred exporter +Any workspace admin can export reports to Sage Intacct. For auto-export, Concierge will export on behalf of the preferred exporter. The preferred exporter will also be notified of any expense reports that fail to export to Sage Intacct due to an error. + +### Export date +You can choose which date to use for the records created in Sage Intacct. There are three date options: + +1. **Date of last expense:** This will use the date of the previous expense on the report +1. **Export date:** The date you export the report to Sage Intacct +1. **Submitted date:** The date the employee submitted the report + +### Export out-of-pocket expenses as +Out-of-pocket expenses can be exported to Sage Intacct as **expense reports** or as **vendor bills**. If you choose to export as expense reports, you can optionally select a **default vendor**, which will apply to reimbursable expenses that don't have a matching vendor in Sage Intacct. + +### Export company card expenses as +Company Card expenses are exported separately from out-of-pocket expenses, and can be exported to Sage Intacct as credit card charges** or as **vendor bills**. + +- **Credit card charges:** When exporting as credit card charges, you must select a credit card account. You can optionally select a default vendor, which will apply to company card expenses that don't have a matching vendor in Sage Intacct. + - Credit card charges cannot be exported to Sage Intacct at the top-level if you have multi-currency enabled, so you will need to select an individual entity if you have this setup. +- **Vendor bills:** When exporting as vendor bills, you can select a default vendor, which will apply to company card expenses that don't have a matching vendor in Sage Intacct. + +If you centrally manage your company cards through Domains in Expensify Classic, you can export expenses from each individual card to a specific account in Sage Intacct in the Expensify Company Card settings. + +### 6. Configure advanced settings +To access the advanced settings of the Sage Intacct integration, head to **Settings > Workspaces > [Workspace name] > Accounting** and click **Advanced** under Sage Intacct. + + +Let’s review the different advanced settings and how they interact with the integration. + +### Auto-sync +We strongly recommend enabling auto-sync to ensure that the information in Sage Intacct and Expensify is always in sync. The following will occur when auto-sync is enabled: + +**Daily sync from Sage Intacct to Expensify:** Once a day, Expensify will sync any changes from Sage Intacct into Expensify. This includes any changes or additions to your Sage Intacct dimensions. + +**Auto-export:** When an expense report reaches its final state in Expensify, it will be automatically exported to Sage Intacct. The final state will either be reimbursement (if you reimburse members through Expensify) or final approval (if you reimburse members outside of Expensify). + +**Reimbursement-sync:** If Sync Reimbursed Reports (more details below) is enabled, then we will sync the reimbursement status of reports between Expensify and Sage Intacct. + +### Invite employees +Enabling this feature will invite all employees from the connected Sage Intacct entity to your Expensify workspace. Once imported, each employee who has not already been invited to that Expensify workspace will receive an email letting them know they’ve been added to the workspace. + +In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic: + +- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. +- **Manager Approval (default):** Each user submits to their manager, who is imported from Sage Intacct. You can optionally select a final approver who each manager forwards to for second-level approval. +- **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured in Expensify. If you enable this setting, you can configure approvals by going to **Settings > Workspaces > [Workspace Name] > People**. + + +### Sync reimbursed reports +When Sync reimbursed reports is enabled, the reimbursement status will be synced between Expensify and Sage Intacct. + +**If you reimburse employees through Expensify:** Reimbursing an expense report will trigger auto-export to Sage Intacct. When the expense report is exported to Sage Intacct, a corresponding bill payment will also be created in Sage Intacct in the selected Cash and Cash Equivalents account. If you don't see the account you'd like to select in the dropdown list, please confirm that the account type is Cash and Cash Equivalents. + +**If you reimburse employees outside of Expensify:** Expense reports will be exported to Sage Intacct at time of final approval. After you mark the report as paid in Sage Intacct, the reimbursed status will be synced back to Expensify the next time the integration syncs. + +To ensure this feature works properly for expense reports, make sure that the account you choose within the settings matches the default account for Bill Payments in NetSuite. When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting. +## FAQ + +### Why wasn't my report automatically exported to Sage Intacct? +There are a number of factors that can cause auto-export to fail. If this happens, you will find the specific export error in the report comments for the report that failed to export. Once you’ve resolved any errors, you can manually export the report to Sage Intacct. + +### Will enabling auto-sync affect existing approved and reimbursed reports? +Auto-sync will only export newly approved reports to Sage Intacct. Any reports that were approved or reimbursed before enabling auto-sync will need to be manually exported in order to sync them to Sage Intacct. + + +### Can I export negative expenses to Sage Intacct? +Yes, you can export negative expenses to Sage Intacct. If you are exporting out-of-pocket expenses as expense reports, then the total of each exported report cannot be negative. diff --git a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md index 155512866a8f..79d5b17055f7 100644 --- a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md +++ b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md @@ -52,6 +52,12 @@ Log in to QuickBooks Online and ensure all of your employees are setup as either ![The QuickBooks Online Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-3.png){:width="100%"} +![The QuickBooks Online Connect Accounting button]({{site.url}}/assets/images/ExpensifyHelp-QBO-4.png){:width="100%"} + +![The QuickBooks Online Connect Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-5.png){:width="100%"} + + + # Step 3: Configure import settings The following steps help you determine how data will be imported from QuickBooks Online to Expensify. diff --git a/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md similarity index 100% rename from docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md rename to docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md diff --git a/docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md similarity index 100% rename from docs/articles/new-expensify/expenses/Connect-a-Business-Bank-Account.md rename to docs/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account.md diff --git a/docs/articles/new-expensify/expenses/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md similarity index 100% rename from docs/articles/new-expensify/expenses/Create-an-expense.md rename to docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md diff --git a/docs/articles/new-expensify/expenses/Distance-Requests.md b/docs/articles/new-expensify/expenses-&-payments/Distance-Requests.md similarity index 100% rename from docs/articles/new-expensify/expenses/Distance-Requests.md rename to docs/articles/new-expensify/expenses-&-payments/Distance-Requests.md diff --git a/docs/articles/new-expensify/expenses-&-payments/Duplicate-detection.md b/docs/articles/new-expensify/expenses-&-payments/Duplicate-detection.md new file mode 100644 index 000000000000..d7fb0b1bfa71 --- /dev/null +++ b/docs/articles/new-expensify/expenses-&-payments/Duplicate-detection.md @@ -0,0 +1,120 @@ +--- +title: Duplicate Detection +description: Identify and manage duplicate expense requests +--- + +
      + +Duplicate Detection helps prevent duplicate expense requests within a member’s account. By identifying and flagging potential duplicates, it ensures better oversight and control over expenses, enhances fraud prevention, and eases the approval process. This feature is available exclusively for paid plans (Collect & Control). + +# What is a Duplicate? + +A duplicate is an expense request with the same date and amount as another request in an individual member's account. When detected, duplicates are flagged as with a violation and put on “hold”. + +# Surfacing Potential Duplicates + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Identify the red dot indicator in the Left-Hand Navigation (LHN) or workspace chat, which signifies a potential duplicate. +2. Click on the flagged request to open it. +3. Review the system message indicating the request is on hold due to a potential duplicate. +4. Click the green **Review duplicates** button in the request header to navigate to the resolve duplicates page. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Identify the red dot indicator in the LHN or workspace chat, which signifies a potential duplicate. +2. Tap on the flagged request to open it. +3. Review the system message indicating the request is on hold due to a potential duplicate. +4. Tap the green **Review duplicates** button in the request header to navigate to the resolve duplicates page. +{% include end-option.html %} + +{% include end-selector.html %} + +# Resolving Duplicates + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. On the resolve duplicates page, review the chronological list of potential duplicates. +2. Choose an action: + - **Keep all**: Dismiss the duplicates and remove the hold. + - **Keep this one**: Merge duplicates, keeping one request and discarding the rest. +3. If discrepancies exist (e.g., category, tags), choose which details to keep using the one-by-one flow. +4. Confirm your selection to merge requests or keep all. +5. The hold is removed, and system messages are updated accordingly. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. On the resolve duplicates page, review the chronological list of potential duplicates. +2. Choose an action: + - **Keep all**: Dismiss the duplicates and remove the hold. + - **Keep this one**: Merge duplicates, keeping one request and discarding the rest. +3. If discrepancies exist (e.g., category, tags), choose which details to keep using the one-by-one flow. +4. Confirm your selection to merge requests or keep all. +5. The hold is removed, and system messages are updated accordingly. +{% include end-option.html %} + +{% include end-selector.html %} + +# Approver Review + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Identify the red dot and "Duplicate" indicators in the expense report. +2. Click the request with the duplicate indicator. +3. Click the green **Review duplicates** button to navigate to the review duplicates page. +4. Choose an action: + - **Keep all**: Confirm to keep all requests and resolve duplicates. + - **Keep this one**: Navigate through the one-by-one flow to choose the details to keep, followed by confirmation. +5. Confirm your choices to finalize the action and update system messages accordingly. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Identify the red dot and "Duplicate" indicators in the expense report. +2. Tap the request with the duplicate indicator. +3. Tap the green **Review duplicates** button to navigate to the review duplicates page. +4. Choose an action: + - **Keep all**: Confirm to keep all requests and resolve duplicates. + - **Keep this one**: Navigate through the one-by-one flow to choose the details to keep, followed by confirmation. +5. Confirm your choices to finalize the action and update system messages accordingly. +{% include end-option.html %} + +{% include end-selector.html %} + +# Next Steps + +- **For members**: Once resolved, the request is automatically unheld, and a system message indicates the resolution. +- **For approvers**: After confirming the resolution, system messages are updated, and the request status is appropriately adjusted. + +{% include faq-begin.md %} +**Can I review a dismissed duplicate later?** + +Yes, approvers can review dismissed duplicates to ensure accuracy and prevent fraud. + +**What happens if I choose to keep all duplicates?** + +Choosing to keep all duplicates will remove the hold from the requests, and system messages will be updated to reflect this action. + +**Can I edit a duplicate request once resolved?** + +Yes, you can edit the details of a duplicate request once it has been resolved, but the hold must be removed first. + +**What if there are discrepancies in the duplicate requests?** + +You will be guided through a one-by-one flow to choose which details to keep from each request. + +**If two expenses are SmartScanned on the same day for the same amount, will they be flagged as duplicates?** + +Yes, they will be flagged as duplicates unless: +- The expenses were split from a single expense, +- The expenses were imported from a credit card, or +- Matching email receipts sent to receipts@expensify.com were received with different timestamps. + +**What happens if Concierge flags a receipt as a duplicate?** + +If Concierge lets you know it has flagged a receipt as a duplicate, scanning the receipt again will trigger the same duplicate flagging. +{% include faq-end.md %} + +
      diff --git a/docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Resolve-Errors-Adding-a-Bank-Account.md similarity index 100% rename from docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md rename to docs/articles/new-expensify/expenses-&-payments/Resolve-Errors-Adding-a-Bank-Account.md diff --git a/docs/articles/new-expensify/expenses/Send-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md similarity index 95% rename from docs/articles/new-expensify/expenses/Send-an-invoice.md rename to docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md index 588f0da20154..6546f57073ee 100644 --- a/docs/articles/new-expensify/expenses/Send-an-invoice.md +++ b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md @@ -38,6 +38,9 @@ Once an invoice is sent, the customer receives an automated email or text messag 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. + +![A photo of the pay button]({{site.url}}/assets/images/ExpensifyHelp-Invoice-1.png){:width="100%"} + # FAQs diff --git a/docs/articles/new-expensify/expenses/Set-up-your-wallet.md b/docs/articles/new-expensify/expenses-&-payments/Set-up-your-wallet.md similarity index 100% rename from docs/articles/new-expensify/expenses/Set-up-your-wallet.md rename to docs/articles/new-expensify/expenses-&-payments/Set-up-your-wallet.md diff --git a/docs/articles/new-expensify/expenses/Split-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Split-an-expense.md similarity index 100% rename from docs/articles/new-expensify/expenses/Split-an-expense.md rename to docs/articles/new-expensify/expenses-&-payments/Split-an-expense.md diff --git a/docs/articles/new-expensify/expenses/Track-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md similarity index 100% rename from docs/articles/new-expensify/expenses/Track-expenses.md rename to docs/articles/new-expensify/expenses-&-payments/Track-expenses.md diff --git a/docs/articles/new-expensify/expenses/Unlock-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Unlock-a-Business-Bank-Account.md similarity index 100% rename from docs/articles/new-expensify/expenses/Unlock-a-Business-Bank-Account.md rename to docs/articles/new-expensify/expenses-&-payments/Unlock-a-Business-Bank-Account.md diff --git a/docs/articles/new-expensify/expenses/Validate-a-Business-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Validate-a-Business-Bank-Account.md similarity index 100% rename from docs/articles/new-expensify/expenses/Validate-a-Business-Bank-Account.md rename to docs/articles/new-expensify/expenses-&-payments/Validate-a-Business-Bank-Account.md diff --git a/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md new file mode 100644 index 000000000000..ceb40254c607 --- /dev/null +++ b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md @@ -0,0 +1,8 @@ +--- +title: Expensify Travel demo video +description: Check out a demo of Expensify Travel +--- + +Check out a video of how Expensify Travel works below: + + diff --git a/docs/assets/images/ExpensifyHelp-Invoice-1.png b/docs/assets/images/ExpensifyHelp-Invoice-1.png index e4a042afef82..a6dda9fdca92 100644 Binary files a/docs/assets/images/ExpensifyHelp-Invoice-1.png and b/docs/assets/images/ExpensifyHelp-Invoice-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-QBO-1.png b/docs/assets/images/ExpensifyHelp-QBO-1.png index 2aa80e954f1b..e20a5e4222d0 100644 Binary files a/docs/assets/images/ExpensifyHelp-QBO-1.png and b/docs/assets/images/ExpensifyHelp-QBO-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-QBO-2.png b/docs/assets/images/ExpensifyHelp-QBO-2.png index 23419b86b6aa..66b71b8d8ec8 100644 Binary files a/docs/assets/images/ExpensifyHelp-QBO-2.png and b/docs/assets/images/ExpensifyHelp-QBO-2.png differ diff --git a/docs/assets/images/ExpensifyHelp-QBO-3.png b/docs/assets/images/ExpensifyHelp-QBO-3.png index c612cb760d58..f96550868bbd 100644 Binary files a/docs/assets/images/ExpensifyHelp-QBO-3.png and b/docs/assets/images/ExpensifyHelp-QBO-3.png differ diff --git a/docs/assets/images/ExpensifyHelp-QBO-4.png b/docs/assets/images/ExpensifyHelp-QBO-4.png index 7fbc99503f2e..c7b85a93b04b 100644 Binary files a/docs/assets/images/ExpensifyHelp-QBO-4.png and b/docs/assets/images/ExpensifyHelp-QBO-4.png differ diff --git a/docs/assets/images/ExpensifyHelp-QBO-5.png b/docs/assets/images/ExpensifyHelp-QBO-5.png index 600a5903c05f..99b83b8be2d1 100644 Binary files a/docs/assets/images/ExpensifyHelp-QBO-5.png and b/docs/assets/images/ExpensifyHelp-QBO-5.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png new file mode 100644 index 000000000000..8d8222bc76b5 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_01.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png new file mode 100644 index 000000000000..7fb0afc5d8ff Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_02.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png new file mode 100644 index 000000000000..68a1f3e65be6 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_03.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png new file mode 100644 index 000000000000..c62146e2ed43 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_04.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png new file mode 100644 index 000000000000..d3cfafa4e6a7 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_05.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png new file mode 100644 index 000000000000..347551a85c59 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_06.png differ diff --git a/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png new file mode 100644 index 000000000000..83071f380e6e Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-WorkspaceFeeds_07.png differ diff --git a/docs/expensify-classic/hubs/connect-credit-cards/business-bank-accounts.html b/docs/expensify-classic/hubs/bank-accounts-and-payments/payments.html similarity index 100% rename from docs/expensify-classic/hubs/connect-credit-cards/business-bank-accounts.html rename to docs/expensify-classic/hubs/bank-accounts-and-payments/payments.html diff --git a/docs/expensify-classic/hubs/connect-credit-cards/deposit-accounts.html b/docs/expensify-classic/hubs/connections/accelo.html similarity index 100% rename from docs/expensify-classic/hubs/connect-credit-cards/deposit-accounts.html rename to docs/expensify-classic/hubs/connections/accelo.html diff --git a/docs/expensify-classic/hubs/expenses/expenses.html b/docs/expensify-classic/hubs/connections/certinia.html similarity index 100% rename from docs/expensify-classic/hubs/expenses/expenses.html rename to docs/expensify-classic/hubs/connections/certinia.html diff --git a/docs/expensify-classic/hubs/integrations/index.html b/docs/expensify-classic/hubs/connections/index.html similarity index 100% rename from docs/expensify-classic/hubs/integrations/index.html rename to docs/expensify-classic/hubs/connections/index.html diff --git a/docs/expensify-classic/hubs/expenses/reports.html b/docs/expensify-classic/hubs/connections/netsuite.html similarity index 100% rename from docs/expensify-classic/hubs/expenses/reports.html rename to docs/expensify-classic/hubs/connections/netsuite.html diff --git a/docs/expensify-classic/hubs/getting-started/approved-accountants.html b/docs/expensify-classic/hubs/connections/quickbooks-desktop.html similarity index 100% rename from docs/expensify-classic/hubs/getting-started/approved-accountants.html rename to docs/expensify-classic/hubs/connections/quickbooks-desktop.html diff --git a/docs/expensify-classic/hubs/getting-started/support.html b/docs/expensify-classic/hubs/connections/quickbooks-online.html similarity index 100% rename from docs/expensify-classic/hubs/getting-started/support.html rename to docs/expensify-classic/hubs/connections/quickbooks-online.html diff --git a/docs/expensify-classic/hubs/getting-started/tips-and-tricks.html b/docs/expensify-classic/hubs/connections/sage-intacct.html similarity index 100% rename from docs/expensify-classic/hubs/getting-started/tips-and-tricks.html rename to docs/expensify-classic/hubs/connections/sage-intacct.html diff --git a/docs/expensify-classic/hubs/integrations/HR-integrations.html b/docs/expensify-classic/hubs/connections/xero.html similarity index 100% rename from docs/expensify-classic/hubs/integrations/HR-integrations.html rename to docs/expensify-classic/hubs/connections/xero.html diff --git a/docs/expensify-classic/hubs/integrations/accounting-integrations.html b/docs/expensify-classic/hubs/integrations/accounting-integrations.html deleted file mode 100644 index 86641ee60b7d..000000000000 --- a/docs/expensify-classic/hubs/integrations/accounting-integrations.html +++ /dev/null @@ -1,5 +0,0 @@ ---- -layout: default ---- - -{% include section.html %} diff --git a/docs/expensify-classic/hubs/integrations/other-integrations.html b/docs/expensify-classic/hubs/integrations/other-integrations.html deleted file mode 100644 index 86641ee60b7d..000000000000 --- a/docs/expensify-classic/hubs/integrations/other-integrations.html +++ /dev/null @@ -1,5 +0,0 @@ ---- -layout: default ---- - -{% include section.html %} diff --git a/docs/expensify-classic/hubs/integrations/travel-integrations.html b/docs/expensify-classic/hubs/integrations/travel-integrations.html deleted file mode 100644 index 86641ee60b7d..000000000000 --- a/docs/expensify-classic/hubs/integrations/travel-integrations.html +++ /dev/null @@ -1,5 +0,0 @@ ---- -layout: default ---- - -{% include section.html %} diff --git a/docs/expensify-classic/hubs/workspaces/reports.html b/docs/expensify-classic/hubs/workspaces/reports.html deleted file mode 100644 index 86641ee60b7d..000000000000 --- a/docs/expensify-classic/hubs/workspaces/reports.html +++ /dev/null @@ -1,5 +0,0 @@ ---- -layout: default ---- - -{% include section.html %} diff --git a/docs/new-expensify/hubs/expenses/index.html b/docs/new-expensify/hubs/expenses-&-payments/index.html similarity index 100% rename from docs/new-expensify/hubs/expenses/index.html rename to docs/new-expensify/hubs/expenses-&-payments/index.html diff --git a/docs/redirects.csv b/docs/redirects.csv index 1c849e0aabdc..9e87328e6733 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -203,3 +203,50 @@ https://help.expensify.com/articles/new-expensify/chat/Expensify-Chat-For-Admins https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account https://help.expensify.com/articles/expensify-classic/travel/Coming-Soon,https://help.expensify.com/expensify-classic/hubs/travel/ https://help.expensify.com/articles/new-expensify/expenses/Manually-submit-reports-for-approval,https://help.expensify.com/new-expensify/hubs/expenses/ +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursements.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-Payments +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Pay-Bills.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports.html,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills +https://help.expensify.com/articles/expensify-classic/expensify-card/Auto-Reconciliation,https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation.md +https://help.expensify.com/articles/new-expensify/expenses/Approve-and-pay-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses +https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account +https://help.expensify.com/articles/new-expensify/expenses/Create-an-expense,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Create-an-expense +https://help.expensify.com/articles/new-expensify/expenses/Distance-Requests,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Distance-Requests +https://help.expensify.com/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Resolve-Errors-Adding-a-Bank-Account +https://help.expensify.com/articles/new-expensify/expenses/Send-an-invoice,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Send-an-invoice +https://help.expensify.com/articles/new-expensify/expenses/Set-up-your-wallet,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet +https://help.expensify.com/articles/new-expensify/expenses/Split-an-expense,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Split-an-expense +https://help.expensify.com/articles/new-expensify/expenses/Track-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Track-expenses +https://help.expensify.com/articles/new-expensify/expenses/Unlock-a-Business-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Unlock-a-Business-Bank-Account +https://help.expensify.com/expensify-classic/hubs/integrations/HR-integrations,https://help.expensify.com/expensify-classic/hubs/connections +https://help.expensify.com/expensify-classic/hubs/integrations/accounting-integrations,https://help.expensify.com/expensify-classic/hubs/connections +https://help.expensify.com/expensify-classic/hubs/integrations/other-integrations,https://help.expensify.com/expensify-classic/hubs/connections +https://help.expensify.com/expensify-classic/hubs/integrations/travel-integrations,https://help.expensify.com/expensify-classic/hubs/connections +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP,https://help.expensify.com/articles/expensify-classic/connections/ADP +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Accelo,https://help.expensify.com/expensify-classic/hubs/connections/accelo +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Certinia,https://help.expensify.com/expensify-classic/hubs/connections/certinia +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Egencia,https://help.expensify.com/articles/expensify-classic/connections/Egencia +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Global-VaTax,https://help.expensify.com/articles/expensify-classic/connections/Global-VaTax +https://help.expensify.com/articles/expensify-classic/integrations/other-integrations/Google-Apps-SSO,https://help.expensify.com/articles/expensify-classic/connections/Google-Apps-SSO +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Greenhouse,https://help.expensify.com/articles/expensify-classic/connections/Greenhouse +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Gusto,https://help.expensify.com/articles/expensify-classic/connections/Gusto +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Indirect-Accounting-Integrations +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Lyft,https://help.expensify.com/articles/expensify-classic/connections/Lyft +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Navan,https://help.expensify.com/articles/expensify-classic/connections/Navan +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite,https://help.expensify.com/expensify-classic/hubs/connections/netsuite +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time,https://help.expensify.com/articles/expensify-classic/connections/QuickBooks-Time +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop,https://help.expensify.com/expensify-classic/hubs/connections/quickbooks-desktop +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online,https://help.expensify.com/expensify-classic/hubs/connections/quickbooks-online +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Rippling,https://help.expensify.com/articles/expensify-classic/connections/Rippling +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct,https://help.expensify.com/expensify-classic/hubs/connections/sage-intacct +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/TravelPerk,https://help.expensify.com/articles/expensify-classic/connections/TravelPerk +https://help.expensify.com/articles/expensify-classic/integrations/travel-integrations/Uber,https://help.expensify.com/articles/expensify-classic/connections/Uber +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Workday,https://help.expensify.com/articles/expensify-classic/connections/Workday +https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Xero,https://help.expensify.com/expensify-classic/hubs/connections/xero +https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Zenefits,https://help.expensify.com/articles/expensify-classic/connections/Zenefits +https://help.expensify.com/articles/expensify-classic/workspaces/tax-tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking +https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows,https://help.expensify.com/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees +https://help.expensify.com/articles/expensify-classic/settings/Notification-Troubleshooting,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications +https://help.expensify.com/articles/new-expensify/expenses/Validate-a-Business-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Validate-a-Business-Bank-Account +https://help.expensify.com/articles/expensify-classic/workspaces/Expenses,https://help.expensify.com/articles/expensify-classic/workspaces/Expense-Settings +https://help.expensify.com/articles/expensify-classic/workspaces/Reimbursement,https://help.expensify.com/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings diff --git a/fastlane/Fastfile b/fastlane/Fastfile index af9e798d2343..2560e48728c5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -10,6 +10,8 @@ # https://docs.fastlane.tools/plugins/available-plugins # +require 'ostruct' + skip_docs opt_out_usage diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 32ed6ba30059..d5f3c582327d 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 5712b0d86b19..8300bd34ef76 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 f86952ca7aca..164f8485129b 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -388,6 +388,7 @@ FBC7D704E4E9CC08E91D7919 /* [CP] Copy Pods Resources */, 9FF963998EFF771D82D473D2 /* [CP-User] [RNFB] Core Configuration */, A2BE84E8C8EFD6C81A2B41F1 /* [CP-User] [RNFB] Crashlytics Configuration */, + 498240F82C49553900C15857 /* Run Fullstory Asset Uploader */, ); buildRules = ( ); @@ -620,6 +621,24 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 498240F82C49553900C15857 /* Run Fullstory Asset Uploader */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Fullstory Asset Uploader"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\n"; + }; 5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 57a582d2a22b..8ef960cc349c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.3 + 9.0.10 CFBundleSignature ???? CFBundleURLTypes @@ -40,11 +40,13 @@ CFBundleVersion - 9.0.3.2 + 9.0.10.2 FullStory OrgId o-1WN56P-na1 + RecordOnStart + ITSAppUsesNonExemptEncryption diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 1ba368709984..95022fd78c43 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.3 + 9.0.10 CFBundleSignature ???? CFBundleVersion - 9.0.3.2 + 9.0.10.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index bb9a344b3314..8b09b94feb3f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.3 + 9.0.10 CFBundleVersion - 9.0.3.2 + 9.0.10.2 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 35dccc2de393..54fcd1e83993 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -282,7 +282,7 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) - - Onfido (29.7.1) + - Onfido (29.7.2) - onfido-react-native-sdk (10.6.0): - glog - hermes-engine @@ -303,7 +303,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - Plaid (5.2.1) + - Plaid (5.6.0) - PromisesObjC (2.4.0) - RCT-Folly (2022.05.16.00): - boost @@ -1243,7 +1243,13 @@ PODS: - react-native-config (1.5.0): - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): - - React-Core + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen + - React-RCTFabric + - ReactCommon/turbomodule/core - react-native-document-picker (9.1.1): - RCT-Folly - RCTRequired @@ -1251,7 +1257,7 @@ PODS: - React-Codegen - React-Core - ReactCommon/turbomodule/core - - react-native-geolocation (3.2.1): + - react-native-geolocation (3.3.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1394,10 +1400,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-plaid-link-sdk (11.5.0): + - react-native-plaid-link-sdk (11.11.0): - glog - hermes-engine - - Plaid (~> 5.2.0) + - Plaid (~> 5.6.0) - RCT-Folly (= 2022.05.16.00) - RCTRequired - RCTTypeSafety @@ -1433,7 +1439,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-release-profiler (0.1.6): + - react-native-release-profiler (0.2.1): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1865,7 +1871,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.88): + - RNLiveMarkdown (0.1.105): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1883,9 +1889,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.88) + - RNLiveMarkdown/common (= 0.1.105) - Yoga - - RNLiveMarkdown/common (0.1.88): + - RNLiveMarkdown/common (0.1.105): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1929,7 +1935,7 @@ PODS: - ReactCommon/turbomodule/core - Turf - Yoga - - RNPermissions (3.9.3): + - RNPermissions (3.10.1): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1974,7 +1980,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.30.1): + - RNScreens (3.32.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1988,13 +1994,14 @@ PODS: - React-ImageManager - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-rendererdebug - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 3.30.1) + - RNScreens/common (= 3.32.0) - Yoga - - RNScreens/common (3.30.1): + - RNScreens/common (3.32.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2008,6 +2015,7 @@ PODS: - React-ImageManager - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-rendererdebug - React-utils - ReactCommon/turbomodule/bridging @@ -2026,7 +2034,7 @@ PODS: - RNSound/Core (= 0.11.2) - RNSound/Core (0.11.2): - React-Core - - RNSVG (14.1.0): + - RNSVG (15.4.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2044,9 +2052,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 14.1.0) + - RNSVG/common (= 15.4.0) - Yoga - - RNSVG/common (14.1.0): + - RNSVG/common (15.4.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2523,9 +2531,9 @@ SPEC CHECKSUMS: MapboxMaps: 87ef0003e6db46e45e7a16939f29ae87e38e7ce2 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062 + Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2 - Plaid: 7829e84db6d766a751c91a402702946d2977ddcb + Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: ab7f915c15569f04a49669e573e6e319a53f9faa @@ -2552,9 +2560,9 @@ SPEC CHECKSUMS: react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 - react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 + react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 - react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 + react-native-geolocation: 580c86eb531c0aaf7a14bc76fd2983ce47ca58aa react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 react-native-keyboard-controller: 47c01b0741ae5fc84e53cf282e61cfa5c2edb19b @@ -2563,9 +2571,9 @@ SPEC CHECKSUMS: react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa react-native-pdf: 762369633665ec02ac227aefe2f4558b92475c23 react-native-performance: fb21ff0c9bd7a10789c69d948f25b0067d29f7a9 - react-native-plaid-link-sdk: 2a91ef7e257ae16d180a1ca14ba3041ae0836fbf + react-native-plaid-link-sdk: ba40d1b13cca4b946974fafd9ae278e0fb697d87 react-native-quick-sqlite: e3ab3e0a29d8c705f47a60aaa6ceaa42eb6a9ec1 - react-native-release-profiler: 14ccdc0eeb03bedf625cf68d53d80275a81b19dd + react-native-release-profiler: a77d4f291b92e48d3d4a574deed19bd1b513ac98 react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 9d79895b60b8be151fdf6faef9d2d0591eeecc63 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -2606,16 +2614,16 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: e33d2c97863d5480f8f4b45f8b25f801cc43c7f5 + RNLiveMarkdown: bae86068a0b80cda3d4875ac2a5a398955a94922 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c - RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 + RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe - RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 + RNScreens: abd354e98519ed267600b7ee64fdcb8e060b1218 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 - RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f + RNSVG: 8c067e7203053d4c82f456cbeab1fe509ac797dd SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 SDWebImageAVIFCoder: 8348fef6d0ec69e129c66c9fe4d74fbfbf366112 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c @@ -2623,7 +2631,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14 diff --git a/jest/setup.ts b/jest/setup.ts index f11a8a4ed631..c1a737c5def8 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -1,6 +1,8 @@ import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; +import type * as RNKeyboardController from 'react-native-keyboard-controller'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; +import type Animated from 'react-native-reanimated'; import 'setimmediate'; import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; @@ -20,6 +22,16 @@ jest.mock('react-native-onyx/dist/storage', () => mockStorage); // Mock NativeEventEmitter as it is needed to provide mocks of libraries which include it jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter'); +// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest +jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + ignoreLogs: jest.fn(), + ignoreAllLogs: jest.fn(), + }, +})); + // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params: string[]) => { if (params[0].startsWith('Timing:')) { @@ -54,5 +66,10 @@ jest.mock('react-native-share', () => ({ default: jest.fn(), })); -// eslint-disable-next-line @typescript-eslint/no-unsafe-return -jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + createAnimatedPropAdapter: jest.fn, + useReducedMotion: jest.fn, +})); + +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts index 9edfccab9441..eae3ea1f51bd 100644 --- a/jest/setupMockFullstoryLib.ts +++ b/jest/setupMockFullstoryLib.ts @@ -15,7 +15,7 @@ export default function mockFSLibrary() { return { FSPage(): FSPageInterface { return { - start: jest.fn(), + start: jest.fn(() => {}), }; }, default: Fullstory, diff --git a/package-lock.json b/package-lock.json index 40805e7a2fa2..a5f41540d008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.3-2", + "version": "9.0.10-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.3-2", + "version": "9.0.10-2", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.88", + "@expensify/react-native-live-markdown": "0.1.105", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -26,12 +26,12 @@ "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", - "@kie/act-js": "^2.6.0", + "@kie/act-js": "^2.6.2", "@kie/mock-github": "2.0.1", "@onfido/react-native-sdk": "10.6.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", - "@react-native-community/geolocation": "3.2.1", + "@react-native-community/geolocation": "3.3.0", "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.19", + "expensify-common": "2.0.49", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -79,7 +79,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.13", + "react-fast-pdf": "1.0.14", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -102,23 +102,23 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.54", + "react-native-onyx": "2.0.56", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", - "react-native-permissions": "^3.9.3", + "react-native-permissions": "^3.10.0", "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-plaid-link-sdk": "11.11.0", + "react-native-qrcode-svg": "git+https://github.com/Expensify/react-native-qrcode-svg", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.8.0", - "react-native-release-profiler": "^0.1.6", + "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "3.30.1", + "react-native-screens": "3.32.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", - "react-native-svg": "14.1.0", + "react-native-svg": "15.4.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", @@ -201,7 +201,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "^0.0.0-experimental-c23de8d-20240515", + "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", @@ -213,14 +213,14 @@ "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "^29.4.1", - "electron-builder": "24.13.2", + "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-expensify": "^2.0.52", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-react-compiler": "^0.0.0-experimental-53bb89e-20240515", + "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -235,9 +235,11 @@ "onchange": "^7.1.0", "openai": "^4.47.2", "patch-package": "^8.0.0", + "peggy": "^4.0.3", "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", + "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", @@ -250,7 +252,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "type-fest": "^4.10.2", + "type-fest": "4.20.0", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", @@ -2982,8 +2984,9 @@ }, "node_modules/@develar/schema-utils": { "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" @@ -2998,8 +3001,9 @@ }, "node_modules/@develar/schema-utils/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3013,16 +3017,18 @@ }, "node_modules/@develar/schema-utils/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/@develar/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -3107,9 +3113,10 @@ } }, "node_modules/@electron/asar": { - "version": "3.2.8", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz", + "integrity": "sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==", "dev": true, - "license": "MIT", "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", @@ -3124,8 +3131,9 @@ }, "node_modules/@electron/asar/node_modules/commander": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } @@ -3188,9 +3196,10 @@ } }, "node_modules/@electron/notarize": { - "version": "2.2.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.3.2.tgz", + "integrity": "sha512-zfayxCe19euNwRycCty1C7lF7snk9YwfRpB5M8GLr1a4ICH63znxaPNAubrMvj0yDvVozqfgsdYpXVUnpWBDpg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", @@ -3201,9 +3210,10 @@ } }, "node_modules/@electron/osx-sign": { - "version": "1.0.5", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.0.tgz", + "integrity": "sha512-TEXhxlYSDRr9JWK5nWdOv5MtuUdaZ412uxIIEQ0hLt80o0HYWtQJBlW5QmrQDMtebzATaOjKG9UfCzLyA90zWQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", @@ -3222,8 +3232,9 @@ }, "node_modules/@electron/osx-sign/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3235,8 +3246,9 @@ }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8.0.0" }, @@ -3244,21 +3256,172 @@ "url": "https://github.com/sponsors/gjtorikian/" } }, + "node_modules/@electron/rebuild": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.0.tgz", + "integrity": "sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw==", + "dev": true, + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@electron/rebuild/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@electron/rebuild/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@electron/rebuild/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@electron/universal": { - "version": "1.5.1", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", "dev": true, - "license": "MIT", "dependencies": { - "@electron/asar": "^3.2.1", - "@malept/cross-spawn-promise": "^1.1.0", + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", - "dir-compare": "^3.0.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" }, "engines": { - "node": ">=8.6" + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/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/@electron/universal/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { @@ -3785,9 +3948,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.88.tgz", - "integrity": "sha512-78X5ACV+OL+aL6pfJAXyHkNuMGUc4Rheo4qLkIwLpmUIAiAxmY0B2lch5XHSNGf1a5ofvVbdQ6kl84+4E6DwlQ==", + "version": "0.1.105", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.105.tgz", + "integrity": "sha512-4cecVQFQQjdszUHtkHVT7sMwKntGCfxH62DJvbpa6H3eTuz3RCHrzWOZBQGRDVpn0aM3nwVgcWZSvNsDvT8Ziw==", "workspaces": [ "parser", "example", @@ -6124,28 +6287,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/console/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/console/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@jest/console/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -6249,28 +6390,6 @@ } } }, - "node_modules/@jest/core/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@jest/core/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -6339,86 +6458,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/@types/yargs": { - "version": "17.0.31", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@jest/create-cache-key-function/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "license": "MIT", @@ -6432,86 +6471,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/environment/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/environment/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/environment/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/environment/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/environment/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/environment/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@jest/environment/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/environment/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/expect": { "version": "29.6.2", "license": "MIT", @@ -6548,86 +6507,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/fake-timers/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/fake-timers/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/fake-timers/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/fake-timers/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/fake-timers/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@jest/fake-timers/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/fake-timers/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/globals": { "version": "29.5.0", "license": "MIT", @@ -6641,86 +6520,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/globals/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/globals/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/globals/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/globals/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/globals/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@jest/globals/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/globals/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/reporters": { "version": "29.4.1", "license": "MIT", @@ -6762,28 +6561,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@jest/reporters/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -6903,86 +6680,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/test-result/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/test-result/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/test-result/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/test-result/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/test-result/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@jest/test-result/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/test-result/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/test-sequencer": { "version": "29.4.1", "license": "MIT", @@ -7020,28 +6717,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@jest/transform/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -7119,10 +6794,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -7137,10 +6809,7 @@ }, "node_modules/@jest/types/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -7153,10 +6822,7 @@ }, "node_modules/@jest/types/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7170,10 +6836,7 @@ }, "node_modules/@jest/types/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -7183,27 +6846,18 @@ }, "node_modules/@jest/types/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@jest/types/node_modules/has-flag": { "version": "4.0.0", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=8" } }, "node_modules/@jest/types/node_modules/supports-color": { "version": "7.2.0", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7285,9 +6939,10 @@ } }, "node_modules/@kie/act-js": { - "version": "2.6.0", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.6.2.tgz", + "integrity": "sha512-i366cfWluUi55rPZ6e9/aWH4tnw3Q6W1CKh9Gz6QjTvbAtS4KnUUy33I9aMXS6uwa0haw6MSahMM37vmuFCVpQ==", "hasInstallScript": true, - "license": "SEE LICENSE IN LICENSE", "dependencies": { "@kie/mock-github": "^2.0.0", "adm-zip": "^0.5.10", @@ -7417,7 +7072,9 @@ } }, "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, "funding": [ { @@ -7429,18 +7086,18 @@ "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" } ], - "license": "Apache-2.0", "dependencies": { "cross-spawn": "^7.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 12.13.0" } }, "node_modules/@malept/flatpak-bundler": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", @@ -7886,14 +7543,41 @@ "react-native": ">=0.70.0 <1.0.x" } }, + "node_modules/@peggyjs/from-mem": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@peggyjs/from-mem/-/from-mem-1.3.0.tgz", + "integrity": "sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==", + "dev": true, + "dependencies": { + "semver": "7.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@peggyjs/from-mem/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@perf-profiler/android": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@perf-profiler/android/-/android-0.12.1.tgz", - "integrity": "sha512-t4E2tfj9UdJw5JjhFPLMzrsu3NkKSyiZyeIyd70HX9d3anWqNK47XuQV+qkDPMjWaoU+CTlj1SuNnIOqEkCpSA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@perf-profiler/android/-/android-0.13.0.tgz", + "integrity": "sha512-4lUQjJNHFAYB5npts5JLrPaPNpIOEAAjfpeTQOOgBNLT1NW50WWSGuvV2pAdnMi7T28cXs3aUziJJ30cNrSvNg==", "dev": true, "dependencies": { "@perf-profiler/logger": "^0.3.3", - "@perf-profiler/profiler": "^0.10.10", + "@perf-profiler/profiler": "^0.10.11", "@perf-profiler/types": "^0.8.0", "commander": "^12.0.0", "lodash": "^4.17.21" @@ -7912,24 +7596,24 @@ } }, "node_modules/@perf-profiler/ios": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@perf-profiler/ios/-/ios-0.3.2.tgz", - "integrity": "sha512-2jYyHXFO3xe5BdvU1Ttt+Uw2nAf10B3/mcx4FauJwSdJ+nlOAKIvxmZDvMcipCZZ63uc+HWsYndhziJZVQ7VUw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@perf-profiler/ios/-/ios-0.3.3.tgz", + "integrity": "sha512-dbb9lVKOyj1VjinuxrnbfI3FT0+uhH3xclqLQH7rQFA4d93dusjC/s3RzlnCXRNPFPDy5TTmkiIg3xM/6P3/2g==", "dev": true, "dependencies": { - "@perf-profiler/ios-instruments": "^0.3.2", + "@perf-profiler/ios-instruments": "^0.3.3", "@perf-profiler/logger": "^0.3.3", "@perf-profiler/types": "^0.8.0" } }, "node_modules/@perf-profiler/ios-instruments": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@perf-profiler/ios-instruments/-/ios-instruments-0.3.2.tgz", - "integrity": "sha512-uox5arQscpRuGWfzBrTpsn6eJq0ErdjPlU0FMbN4Cv5akQC11ejKWmgV6y4FR/0YIET9uiiXMtnwyEBgUunYGQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@perf-profiler/ios-instruments/-/ios-instruments-0.3.3.tgz", + "integrity": "sha512-e3UmlWuNUOuNbJPWg6aLOVd9wRKe3RYCqwwUgxMNIAwa5QBxaVYBf2pt3+HRsYReh2qm1yvqW7LU9zP+AJ7/7g==", "dev": true, "dependencies": { "@perf-profiler/logger": "^0.3.3", - "@perf-profiler/profiler": "^0.10.10", + "@perf-profiler/profiler": "^0.10.11", "@perf-profiler/types": "^0.8.0", "commander": "^12.0.0", "fast-xml-parser": "^4.2.7" @@ -7970,13 +7654,13 @@ } }, "node_modules/@perf-profiler/profiler": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/@perf-profiler/profiler/-/profiler-0.10.10.tgz", - "integrity": "sha512-kvVC6VQ7pBdthcWEcLTua+iDj0ZkcmYYL9gXHa9Dl7jYkZI4cOeslJZ1vuGfIcC168JwAVrB8UYhgoSgss/MWQ==", + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/@perf-profiler/profiler/-/profiler-0.10.11.tgz", + "integrity": "sha512-nu/zakhG5wRi0tCw4SjTCZJh9e/x9YABAOChh3lGI6CESsFzc1Gi2Vrr+2sytN8dpiTDYCCbECC2EalD7ZKvtg==", "dev": true, "dependencies": { - "@perf-profiler/android": "^0.12.1", - "@perf-profiler/ios": "^0.3.2", + "@perf-profiler/android": "^0.13.0", + "@perf-profiler/ios": "^0.3.3", "@perf-profiler/types": "^0.8.0" } }, @@ -9051,9 +8735,9 @@ } }, "node_modules/@react-native-community/cli-server-api/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -9561,9 +9245,9 @@ "license": "MIT" }, "node_modules/@react-native-community/geolocation": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.2.1.tgz", - "integrity": "sha512-/+HNzuRl4UCMma7KK+KYL8k2nxAGuW+DGxqmqfpiqKBlCkCUbuFHaZZdqVD6jpsn9r/ghe583ECLmd9SV9I4Bw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.3.0.tgz", + "integrity": "sha512-7DFeuotH7m7ImoXffN3TmlGSFn1XjvsaphPort0XZKipssYbdHiKhVVWG+jzisvDhcXikUc6nbUJgddVBL6RDg==", "engines": { "node": ">=18.0.0" }, @@ -17633,8 +17317,9 @@ }, "node_modules/@types/debug": { "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/ms": "*" } @@ -17721,8 +17406,9 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -17909,8 +17595,9 @@ }, "node_modules/@types/ms": { "version": "0.7.34", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true }, "node_modules/@types/node": { "version": "20.11.5", @@ -17965,8 +17652,9 @@ }, "node_modules/@types/plist": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "@types/node": "*", @@ -18174,9 +17862,10 @@ "dev": true }, "node_modules/@types/verror": { - "version": "1.10.9", + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/@types/webpack": { @@ -18212,10 +17901,7 @@ "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "@types/yargs-parser": "*" } @@ -18949,8 +18635,9 @@ }, "node_modules/7zip-bin": { "version": "5.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true }, "node_modules/abab": { "version": "2.0.6", @@ -18958,8 +18645,8 @@ }, "node_modules/abbrev": { "version": "1.1.1", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -19309,29 +18996,32 @@ } }, "node_modules/app-builder-bin": { - "version": "4.0.0", - "dev": true, - "license": "MIT" + "version": "5.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.4.tgz", + "integrity": "sha512-4MitKmOtfTdMONrtRoiaqJ6HtlVZXgrNX1PNdEzEHSAoXU85x7s+mo0IhAS9K9qgjyTVuLrM1E/HAMp5qGyoOA==", + "dev": true }, "node_modules/app-builder-lib": { - "version": "24.13.2", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.0.0.tgz", + "integrity": "sha512-GIx0n/QvbeObY8rQTTp08UPn4pS9xSGZLq6cPRy/CyX/mTNN9pO/uU28MWgqjnYXk0bf/595vzDdAijuDyz5Zw==", "dev": true, - "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/notarize": "2.2.1", - "@electron/osx-sign": "1.0.5", - "@electron/universal": "1.5.1", + "@electron/notarize": "2.3.2", + "@electron/osx-sign": "1.3.0", + "@electron/rebuild": "3.6.0", + "@electron/universal": "2.0.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", + "builder-util": "25.0.0", + "builder-util-runtime": "9.2.5", "chromium-pickle-js": "^0.2.0", "debug": "^4.3.4", "ejs": "^3.1.8", - "electron-publish": "24.13.1", + "electron-publish": "25.0.0", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", @@ -19339,8 +19029,9 @@ "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", - "minimatch": "^5.1.1", - "read-config-file": "6.3.2", + "minimatch": "^10.0.0", + "read-config-file": "6.4.0", + "resedit": "^1.7.0", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", "tar": "^6.1.12", @@ -19350,27 +19041,30 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "24.13.2", - "electron-builder-squirrel-windows": "24.13.2" + "dmg-builder": "25.0.0", + "electron-builder-squirrel-windows": "25.0.0" } }, "node_modules/app-builder-lib/node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/app-builder-lib/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, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/app-builder-lib/node_modules/form-data": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, - "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -19382,8 +19076,9 @@ }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -19395,8 +19090,9 @@ }, "node_modules/app-builder-lib/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -19405,14 +19101,18 @@ } }, "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "5.1.6", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/app-root-dir": { @@ -19432,13 +19132,14 @@ }, "node_modules/aproba": { "version": "1.2.0", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/archiver": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "archiver-utils": "^2.1.0", @@ -19455,8 +19156,9 @@ }, "node_modules/archiver-utils": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "glob": "^7.1.4", @@ -19476,8 +19178,9 @@ }, "node_modules/archiver/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "inherits": "^2.0.3", @@ -19749,8 +19452,9 @@ }, "node_modules/assert-plus": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, - "license": "MIT", "optional": true, "engines": { "node": ">=0.8" @@ -19793,8 +19497,9 @@ }, "node_modules/astral-regex": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "license": "MIT", "optional": true, "engines": { "node": ">=8" @@ -19814,8 +19519,9 @@ }, "node_modules/async-exit-hook": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -20375,9 +20081,9 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-c23de8d-20240515", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-c23de8d-20240515.tgz", - "integrity": "sha512-0XN2gmpT55QtAz5n7d5g91y1AuO9tRhWBaLgCRyc4ExHrlr7+LfxW+YTb3mOwxngkkiggwM8HyYsaEK9MqhnlQ==", + "version": "0.0.0-experimental-696af53-20240625", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-0.0.0-experimental-696af53-20240625.tgz", + "integrity": "sha512-OUDKms8qmcm5bX0D+sJWC1YcKcd7AZ2aJ7eY6gkR+Xr7PDfkXLbqAld4Qs9B0ntjVbUMEtW/PjlQrxDtY4raHg==", "dev": true, "dependencies": { "@babel/generator": "7.2.0", @@ -20956,13 +20662,15 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true }, "node_modules/bluebird-lst": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", "dev": true, - "license": "MIT", "dependencies": { "bluebird": "^3.5.5" } @@ -21282,17 +20990,6 @@ "node": "*" } }, - "node_modules/buffer-equal": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/buffer-fill": { "version": "1.0.0", "license": "MIT" @@ -21306,15 +21003,16 @@ "license": "MIT" }, "node_modules/builder-util": { - "version": "24.13.1", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.0.0.tgz", + "integrity": "sha512-cI8zIsipo/gciZ5jGEA1qYL5Em1N6cWoNMpeJWZAfOs3H9s5zQWKnAS7rTdlJpsJ88gEmL5/32yeXUF2Uzxw6w==", "dev": true, - "license": "MIT", "dependencies": { "@types/debug": "^4.1.6", "7zip-bin": "~5.2.0", - "app-builder-bin": "4.0.0", + "app-builder-bin": "v5.0.0-alpha.4", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.2.4", + "builder-util-runtime": "9.2.5", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", @@ -21329,9 +21027,10 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.4", + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz", + "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -21342,8 +21041,9 @@ }, "node_modules/builder-util/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -21356,13 +21056,15 @@ }, "node_modules/builder-util/node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/builder-util/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -21376,8 +21078,9 @@ }, "node_modules/builder-util/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -21387,13 +21090,15 @@ }, "node_modules/builder-util/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/builder-util/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -21405,16 +21110,18 @@ }, "node_modules/builder-util/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/builder-util/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -21424,8 +21131,9 @@ }, "node_modules/builder-util/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -21818,8 +21526,9 @@ }, "node_modules/chromium-pickle-js": { "version": "0.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true }, "node_modules/ci-info": { "version": "3.8.0", @@ -22025,8 +21734,9 @@ }, "node_modules/cli-truncate": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "slice-ansi": "^3.0.0", @@ -22179,8 +21889,8 @@ }, "node_modules/color-support": { "version": "1.1.3", + "devOptional": true, "license": "ISC", - "optional": true, "bin": { "color-support": "bin.js" } @@ -22245,8 +21955,9 @@ }, "node_modules/compare-version": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -22277,8 +21988,9 @@ }, "node_modules/compress-commons": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", @@ -22292,8 +22004,9 @@ }, "node_modules/compress-commons/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "inherits": "^2.0.3", @@ -22458,47 +22171,64 @@ } }, "node_modules/config-file-ts": { - "version": "0.2.6", + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", "dev": true, - "license": "MIT", "dependencies": { - "glob": "^10.3.10", - "typescript": "^5.3.3" + "glob": "^10.3.12", + "typescript": "^5.4.3" } }, "node_modules/config-file-ts/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, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/config-file-ts/node_modules/glob": { - "version": "10.3.10", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" }, "funding": { "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/config-file-ts/node_modules/minimatch": { - "version": "9.0.3", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -22510,9 +22240,10 @@ } }, "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.0.4", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -22601,8 +22332,8 @@ }, "node_modules/console-control-strings": { "version": "1.1.0", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/constants-browserify": { "version": "1.0.0", @@ -22820,8 +22551,9 @@ }, "node_modules/crc": { "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "buffer": "^5.1.0" @@ -22829,8 +22561,9 @@ }, "node_modules/crc-32": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, - "license": "Apache-2.0", "peer": true, "bin": { "crc32": "bin/crc32.njs" @@ -22841,8 +22574,9 @@ }, "node_modules/crc32-stream": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "crc-32": "^1.2.0", @@ -22854,8 +22588,9 @@ }, "node_modules/crc32-stream/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "inherits": "^2.0.3", @@ -23595,8 +23330,8 @@ }, "node_modules/delegates": { "version": "1.0.0", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/denodeify": { "version": "1.2.1", @@ -23657,8 +23392,8 @@ }, "node_modules/detect-libc": { "version": "2.0.1", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -23751,12 +23486,13 @@ "license": "MIT" }, "node_modules/dir-compare": { - "version": "3.3.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, - "license": "MIT", "dependencies": { - "buffer-equal": "^1.0.0", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " } }, "node_modules/dir-glob": { @@ -23770,13 +23506,14 @@ } }, "node_modules/dmg-builder": { - "version": "24.13.2", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.0.0.tgz", + "integrity": "sha512-kXETWCy/JIXS8PHYc8Y0EdSWO02gpf4jleW74hkIp6o9WWTjAdBRw2fAcRBNIEBUJtVHFrgCYsEWh0wKFUB0+A==", "dev": true, - "license": "MIT", "dependencies": { - "app-builder-lib": "24.13.2", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", + "app-builder-lib": "25.0.0", + "builder-util": "25.0.0", + "builder-util-runtime": "9.2.5", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -23787,13 +23524,15 @@ }, "node_modules/dmg-builder/node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/dmg-builder/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -23805,8 +23544,9 @@ }, "node_modules/dmg-builder/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -23816,8 +23556,9 @@ }, "node_modules/dmg-license": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -23841,8 +23582,9 @@ }, "node_modules/dmg-license/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "fast-deep-equal": "^3.1.1", @@ -23857,8 +23599,9 @@ }, "node_modules/dmg-license/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/dns-packet": { @@ -23973,20 +23716,31 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { - "version": "5.1.0", + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", + "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", "dev": true, - "license": "BSD-2-Clause" + "dependencies": { + "dotenv": "^16.4.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } }, "node_modules/duplexer": { "version": "0.1.2", @@ -24054,19 +23808,20 @@ } }, "node_modules/electron-builder": { - "version": "24.13.2", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.0.0.tgz", + "integrity": "sha512-3nEqF6KnoM206mLz1C70VXWCzXmH2boL82wkpgLB1GXgK3dly6ay/cepI+2BmQT4iWkIHeG8qH9bPjPj0hn+1A==", "dev": true, - "license": "MIT", "dependencies": { - "app-builder-lib": "24.13.2", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", + "app-builder-lib": "25.0.0", + "builder-util": "25.0.0", + "builder-util-runtime": "9.2.5", "chalk": "^4.1.2", - "dmg-builder": "24.13.2", + "dmg-builder": "25.0.0", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", - "read-config-file": "6.3.2", + "read-config-file": "6.4.0", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, @@ -24079,21 +23834,23 @@ } }, "node_modules/electron-builder-squirrel-windows": { - "version": "24.13.2", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.0.0.tgz", + "integrity": "sha512-bfARwAdye1UkFQZ7NedHZBcOek2lvDDeg/pCaXT4Nrki7gdwrvVY/Be/QJm7Smc6IR/mviozbL9ykUHQ/FSsbw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { - "app-builder-lib": "24.13.2", + "app-builder-lib": "25.0.0", "archiver": "^5.3.1", - "builder-util": "24.13.1", + "builder-util": "25.0.0", "fs-extra": "^10.1.0" } }, "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -24182,13 +23939,14 @@ } }, "node_modules/electron-publish": { - "version": "24.13.1", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.0.0.tgz", + "integrity": "sha512-8wq3pVLq9bpd/jNKJGIXbeL8B8AovLojtCDkVSuSgrLtxEndqy5JfuadUKPAgbmh1zjholNAHsfHH9FS5yeYAg==", "dev": true, - "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "24.13.1", - "builder-util-runtime": "9.2.4", + "builder-util": "25.0.0", + "builder-util-runtime": "9.2.5", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", @@ -24197,8 +23955,9 @@ }, "node_modules/electron-publish/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -24211,8 +23970,9 @@ }, "node_modules/electron-publish/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -24226,8 +23986,9 @@ }, "node_modules/electron-publish/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -24237,13 +23998,15 @@ }, "node_modules/electron-publish/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/electron-publish/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -24255,16 +24018,18 @@ }, "node_modules/electron-publish/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/electron-publish/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -24327,6 +24092,15 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "license": "MIT", @@ -25341,9 +25115,9 @@ } }, "node_modules/eslint-plugin-react-compiler": { - "version": "0.0.0-experimental-53bb89e-20240515", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-53bb89e-20240515.tgz", - "integrity": "sha512-L3HV9qja1dnClRlR9aaWEJeJoGPH9cgjKq0sYqIOOH9uyWdVMH9CudsFr6yLva7dj05FpLZkiIaRSZJ3P/v6yQ==", + "version": "0.0.0-experimental-0998c1e-20240625", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-0998c1e-20240625.tgz", + "integrity": "sha512-npq2RomExoQI3jETs4OrifaygyJYgOcX/q74Q9OC7GmffLh5zSJaQpzjs2fi61NMNkJyIvTBD0C6sKTGGcetOw==", "dev": true, "dependencies": { "@babel/core": "^7.24.4", @@ -26017,9 +25791,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.19.tgz", - "integrity": "sha512-GdWlYiHOAapy/jxjcvL9NKGOofhoEuKIwvJNGNVHbDXcA+0NxVCNYrHt1yrLnVcE4KtK6PGT6fQ2Lp8NTCoA+g==", + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.49.tgz", + "integrity": "sha512-67QbRuR2XEl2RoNLSbyqGWATIbOXPV42azAfs2sqNT6iyWKcOgHUqRkWPhxA0GmSW35lwq66bvgPVsQUfMGCow==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -26031,9 +25805,9 @@ "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.6.0", + "semver": "^7.6.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "ua-parser-js": "^1.0.37" + "ua-parser-js": "^1.0.38" } }, "node_modules/expensify-common/node_modules/react": { @@ -26070,9 +25844,9 @@ } }, "node_modules/expensify-common/node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", + "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", "funding": [ { "type": "opencollective", @@ -26395,6 +26169,12 @@ "node": ">=8" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, "node_modules/express": { "version": "4.18.1", "license": "MIT", @@ -26558,11 +26338,12 @@ }, "node_modules/extsprintf": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true }, "node_modules/fast-deep-equal": { @@ -27839,8 +27620,8 @@ }, "node_modules/has-unicode": { "version": "2.0.1", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/has-value": { "version": "1.0.0", @@ -28066,8 +27847,9 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -28611,8 +28393,9 @@ }, "node_modules/iconv-corefoundation": { "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -28981,6 +28764,25 @@ "node": ">=0.10.0" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/ip-regex": { "version": "2.1.0", "license": "MIT", @@ -29128,8 +28930,9 @@ }, "node_modules/is-ci": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, - "license": "MIT", "dependencies": { "ci-info": "^3.2.0" }, @@ -29364,6 +29167,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, "node_modules/is-map": { "version": "2.0.2", "dev": true, @@ -29640,8 +29449,9 @@ }, "node_modules/isbinaryfile": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 18.0.0" }, @@ -29938,28 +29748,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-circus/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -30050,28 +29838,6 @@ } } }, - "node_modules/jest-cli/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-cli/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -30173,28 +29939,6 @@ } } }, - "node_modules/jest-config/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-config/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-config/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -30348,28 +30092,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-each/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-each/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -30453,28 +30175,6 @@ } } }, - "node_modules/jest-environment-jsdom/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-environment-jsdom/node_modules/acorn": { "version": "8.11.3", "license": "MIT", @@ -30485,47 +30185,6 @@ "node": ">=0.4.0" } }, - "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-environment-jsdom/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-environment-jsdom/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, "node_modules/jest-environment-jsdom/node_modules/cssstyle": { "version": "2.3.0", "license": "MIT", @@ -30574,13 +30233,6 @@ "node": ">= 6" } }, - "node_modules/jest-environment-jsdom/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { "version": "3.0.0", "license": "MIT", @@ -30644,16 +30296,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/jest-environment-jsdom/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-environment-jsdom/node_modules/tr46": { "version": "3.0.0", "license": "MIT", @@ -30724,86 +30366,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-environment-node/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/@types/yargs": { - "version": "17.0.31", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-environment-node/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-environment-node/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-environment-node/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-environment-node/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/jest-environment-node/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-node/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-expo": { "version": "50.0.1", "license": "MIT", @@ -30871,69 +30433,6 @@ "fsevents": "^2.3.2" } }, - "node_modules/jest-haste-map/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-haste-map/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-haste-map/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-haste-map/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, "node_modules/jest-haste-map/node_modules/has-flag": { "version": "4.0.0", "license": "MIT", @@ -30967,16 +30466,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jest-haste-map/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-leak-detector": { "version": "29.4.1", "license": "MIT", @@ -31077,28 +30566,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-message-util/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-message-util/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -31169,86 +30636,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-mock/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-mock/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-mock/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-mock/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-mock/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/jest-mock/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "license": "MIT", @@ -31388,28 +30775,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-runner/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -31534,28 +30899,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runtime/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-runtime/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -31643,28 +30986,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-snapshot/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -31747,28 +31068,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-util/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -31842,28 +31141,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-validate/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -32157,28 +31434,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watcher/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/jest-watcher/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", @@ -32279,86 +31534,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jest/node_modules/@jest/types": { - "version": "29.6.3", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest/node_modules/@types/yargs": { - "version": "17.0.24", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/jest/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jimp-compact": { "version": "0.16.1", "license": "MIT" @@ -32410,6 +31585,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsc-android": { "version": "250231.0.0", "license": "BSD-2-Clause" @@ -32758,13 +31939,15 @@ }, "node_modules/lazy-val": { "version": "1.0.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true }, "node_modules/lazystream": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "readable-stream": "^2.0.5" @@ -32983,20 +32166,23 @@ }, "node_modules/lodash.defaults": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/lodash.difference": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/lodash.flatten": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/lodash.isequal": { @@ -33005,8 +32191,9 @@ }, "node_modules/lodash.isplainobject": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/lodash.memoize": { @@ -33026,8 +32213,9 @@ }, "node_modules/lodash.union": { "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/log-symbols": { @@ -33395,6 +32583,175 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/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/make-fetch-happen/node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/makeerror": { "version": "1.0.12", "license": "BSD-3-Clause", @@ -34152,8 +33509,9 @@ } }, "node_modules/metro/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -34291,6 +33649,23 @@ "node": ">= 8" } }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, "node_modules/minipass-flush": { "version": "1.0.5", "license": "ISC", @@ -34311,6 +33686,18 @@ "node": ">=8" } }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "2.1.2", "license": "MIT", @@ -34567,16 +33954,38 @@ "node": ">= 10.13" } }, + "node_modules/node-abi": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", + "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "license": "MIT" }, "node_modules/node-addon-api": { "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "dev": true, - "license": "MIT", "optional": true }, + "node_modules/node-api-version": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.0.tgz", + "integrity": "sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + } + }, "node_modules/node-dir": { "version": "0.1.17", "license": "MIT", @@ -34653,6 +34062,95 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/node-int64": { "version": "0.4.0", "license": "MIT" @@ -34720,6 +34218,21 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "license": "BSD-2-Clause", @@ -35974,6 +35487,16 @@ "path2d-polyfill": "^2.0.1" } }, + "node_modules/pe-library": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.0.tgz", + "integrity": "sha512-JAmVv2jGxmczplhHO7UoFGJ+pM/yMBpny3vNjwNFuaeQfzKlekQidZ8Ss8EJ0qee8wEQN4lY2IwtWx2oRfMsag==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/peek-stream": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", @@ -35985,6 +35508,32 @@ "through2": "^2.0.3" } }, + "node_modules/peggy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/peggy/-/peggy-4.0.3.tgz", + "integrity": "sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==", + "dev": true, + "dependencies": { + "@peggyjs/from-mem": "1.3.0", + "commander": "^12.1.0", + "source-map-generator": "0.8.0" + }, + "bin": { + "peggy": "bin/peggy.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/peggy/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/pend": { "version": "1.2.0", "dev": true, @@ -36117,14 +35666,24 @@ } }, "node_modules/plist": { - "version": "3.0.6", - "license": "MIT", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dependencies": { + "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, "engines": { - "node": ">=6" + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" } }, "node_modules/plist/node_modules/xmlbuilder": { @@ -36886,6 +36445,98 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-compiler-healthcheck": { + "version": "0.0.0-experimental-b130d5f-20240625", + "resolved": "https://registry.npmjs.org/react-compiler-healthcheck/-/react-compiler-healthcheck-0.0.0-experimental-b130d5f-20240625.tgz", + "integrity": "sha512-vf3Ipg+f19yOYQeRP938e5jWNEpwR6EX5pwBZdJUF9rt11vJ3ckgUVcF5qGWUU/7DB0N9MH1koBqwqMYabrBiQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "chalk": "4", + "fast-glob": "^3.3.2", + "ora": "5.4.1", + "yargs": "^17.7.2", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.3" + }, + "bin": { + "react-compiler-healthcheck": "dist/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" + } + }, + "node_modules/react-compiler-healthcheck/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/react-compiler-healthcheck/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/react-compiler-healthcheck/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-compiler-healthcheck/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/react-compiler-healthcheck/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-compiler-healthcheck/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/react-compiler-runtime": { "resolved": "lib/react-compiler-runtime", "link": true @@ -37022,9 +36673,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.13.tgz", - "integrity": "sha512-rF7NQZ26rJAI8ysRJaG71dl2c7AIq48ibbn7xCyF3lEZ/yOjA8BeR0utRwDjaHGtswQscgETboilhaaH5UtIYg==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.14.tgz", + "integrity": "sha512-iWomykxvnZtokIKpRK5xpaRfXz9ufrY7AVANtIBYsAZtX5/7VDlpIQwieljfMZwFc96TyceCnneufsgXpykTQw==", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" @@ -37405,9 +37056,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.54.tgz", - "integrity": "sha512-cANbs0KuiwHAIUC0HY7DGNXbFMHH4ZWbTci+qhHhuNNf4aNIP0/ncJ4W8a3VCgFVtfobIFAX5ouT40dEcgBOIQ==", + "version": "2.0.56", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.56.tgz", + "integrity": "sha512-3rn1+J4tli9zPS9w5x6tOAUz01wVHkiTFgtHoIwjD7HdLUO/9nk6H8JX6Oqb9Vzq2XQOSavUFRepIHnGvzNtgg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -37470,8 +37121,9 @@ } }, "node_modules/react-native-permissions": { - "version": "3.9.3", - "license": "MIT", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.10.1.tgz", + "integrity": "sha512-Gc5BxxpjZn4QNUDiVeHOO0vXh3AH7ToolmwTJozqC6DsxV7NAf3ttap+8BSmzDR8WxuAM3Cror+YNiBhHJx7/w==", "peerDependencies": { "react": ">=16.13.1", "react-native": ">=0.63.3", @@ -37496,17 +37148,17 @@ } }, "node_modules/react-native-plaid-link-sdk": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/react-native-plaid-link-sdk/-/react-native-plaid-link-sdk-11.5.0.tgz", - "integrity": "sha512-B3fwujxBS9nZwadXFcseU3nrYG7Ptob6p9eG/gXde65cqwErMaq2k1rVv3R17s/rpKnmU5Cx5pKOMmkxPUn08w==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/react-native-plaid-link-sdk/-/react-native-plaid-link-sdk-11.11.0.tgz", + "integrity": "sha512-Kmimhr6iOwCtIzsW7gygz48TzaZsdjnpgstJ2PM1q+THulOnx+BnkFu8UpLIGGkVe19E4wkxOAYL8kJ8vefNSQ==", "peerDependencies": { "react": "*", "react-native": "*" } }, "node_modules/react-native-qrcode-svg": { - "version": "6.2.0", - "license": "MIT", + "version": "6.3.0", + "resolved": "git+ssh://git@github.com/Expensify/react-native-qrcode-svg.git#295f87d45c0f10d9b50838ad28fa70e47d054c3b", "dependencies": { "prop-types": "^15.8.0", "qrcode": "^1.5.1" @@ -37514,7 +37166,7 @@ "peerDependencies": { "react": "*", "react-native": ">=0.63.4", - "react-native-svg": "^13.2.0" + "react-native-svg": ">=13.2.0" } }, "node_modules/react-native-quick-sqlite": { @@ -37557,14 +37209,15 @@ "license": "MIT" }, "node_modules/react-native-release-profiler": { - "version": "0.1.6", - "license": "MIT", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/react-native-release-profiler/-/react-native-release-profiler-0.2.1.tgz", + "integrity": "sha512-gDOwEXypd4Gu++nlKyaVLHPfwrVkkdBrsjMrQORYTTDqcrD/OfuNZ8YK7p+u5LUNjnPD4WmBF88C5dEW7iM1lg==", "workspaces": [ "example" ], "dependencies": { - "@react-native-community/cli": "^12.2.1", - "commander": "^11.1.0" + "commander": "^11.1.0", + "hermes-profile-transformer": "^0.0.9" }, "bin": { "react-native-release-profiler": "lib/commonjs/cli.js" @@ -37584,6 +37237,25 @@ "node": ">=16" } }, + "node_modules/react-native-release-profiler/node_modules/hermes-profile-transformer": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/hermes-profile-transformer/-/hermes-profile-transformer-0.0.9.tgz", + "integrity": "sha512-JYPUE9zA+W/hpTIGBV+t2ODvntataLLMfntoEcpEpKFDwdR6+Quk9SwLnWX9y2A3ZII6N4T8w3TUBC2ejsEGBw==", + "dependencies": { + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-native-release-profiler/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/react-native-render-html": { "version": "6.3.1", "license": "BSD-2-Clause", @@ -37634,9 +37306,9 @@ } }, "node_modules/react-native-screens": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.30.1.tgz", - "integrity": "sha512-/muEvjocCtFb+j5J3YmLvB25+f4rIU8hnnxgGTkXcAf2omPBY8uhPjJaaFUlvj64VEoEzJcRpugbXWsjfPPIFg==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.32.0.tgz", + "integrity": "sha512-wybqZAHX7v8ipOXhh90CqGLkBHw5JYqKNRBX7R/b0c2WQisTOgu0M0yGwBMM6LyXRBT+4k3NTGHdDbpJVpq0yQ==", "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" @@ -37661,11 +37333,13 @@ } }, "node_modules/react-native-svg": { - "version": "14.1.0", - "license": "MIT", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.4.0.tgz", + "integrity": "sha512-zkBEbme/Dba4yqreg/oI2P6/6LrLywWY7HhaSwpU7Pb5COpTd2fV6/ShsgZz8GRFFdidUPwWmx01FITUsjhkmw==", "dependencies": { "css-select": "^5.1.0", - "css-tree": "^1.1.3" + "css-tree": "^1.1.3", + "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", @@ -37937,8 +37611,9 @@ } }, "node_modules/react-native/node_modules/ws": { - "version": "6.2.2", - "license": "MIT", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "dependencies": { "async-limiter": "~1.0.0" } @@ -38112,10 +37787,6 @@ "react": "^18.2.0" } }, - "node_modules/react-test-renderer/node_modules/react-is": { - "version": "18.2.0", - "license": "MIT" - }, "node_modules/react-test-renderer/node_modules/scheduler": { "version": "0.23.0", "license": "MIT", @@ -38544,6 +38215,18 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-cmd-shim": { "version": "4.0.0", "license": "ISC", @@ -38552,16 +38235,17 @@ } }, "node_modules/read-config-file": { - "version": "6.3.2", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.4.0.tgz", + "integrity": "sha512-uB5QOBeF84PT61GlV11OTV4jUGHAO3iDEOP6v9ygxhG6Bs9PLg7WsjNT6mtIX2G+x8lJTr4ZWNeG6LDTKkNf2Q==", "dev": true, - "license": "MIT", "dependencies": { - "config-file-ts": "^0.2.4", - "dotenv": "^9.0.2", - "dotenv-expand": "^5.1.0", + "config-file-ts": "0.2.8-rc1", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", "js-yaml": "^4.1.0", - "json5": "^2.2.0", - "lazy-val": "^1.0.4" + "json5": "^2.2.3", + "lazy-val": "^1.0.5" }, "engines": { "node": ">=12.0.0" @@ -38569,21 +38253,15 @@ }, "node_modules/read-config-file/node_modules/argparse": { "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/read-config-file/node_modules/dotenv": { - "version": "9.0.2", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/read-config-file/node_modules/js-yaml": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -38754,8 +38432,9 @@ }, "node_modules/readdir-glob": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, - "license": "Apache-2.0", "peer": true, "dependencies": { "minimatch": "^5.1.0" @@ -38763,8 +38442,9 @@ }, "node_modules/readdir-glob/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, - "license": "MIT", "peer": true, "dependencies": { "balanced-match": "^1.0.0" @@ -38772,8 +38452,9 @@ }, "node_modules/readdir-glob/node_modules/minimatch": { "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, - "license": "ISC", "peer": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -39139,6 +38820,19 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/resedit": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.0.tgz", + "integrity": "sha512-dbsZ0gk5opWPFlKMqvxCrLCuMZUVmsW3yTPT0tT4mYwo5fjQM8c4HMN9ZJt6dRDqDV/78m9SU4rv24PN4NiYaA==", + "dev": true, + "dependencies": { + "pe-library": "^0.4.0" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/reselect": { "version": "4.1.7", "dev": true, @@ -39411,8 +39105,9 @@ }, "node_modules/sanitize-filename": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", "dev": true, - "license": "WTFPL OR ISC", "dependencies": { "truncate-utf8-bytes": "^1.0.0" } @@ -39508,11 +39203,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -39954,8 +39647,9 @@ }, "node_modules/slice-ansi": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -39968,8 +39662,9 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "color-convert": "^2.0.1" @@ -39983,8 +39678,9 @@ }, "node_modules/slice-ansi/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "color-name": "~1.1.4" @@ -39995,8 +39691,9 @@ }, "node_modules/slice-ansi/node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/slugify": { @@ -40008,9 +39705,9 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, - "license": "MIT", - "optional": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -40216,6 +39913,34 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sort-asc": { "version": "0.2.0", "license": "MIT", @@ -40263,6 +39988,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-generator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", + "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "license": "BSD-3-Clause", @@ -40493,8 +40227,9 @@ }, "node_modules/stat-mode": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } @@ -41235,8 +40970,9 @@ }, "node_modules/temp-file": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, - "license": "MIT", "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" @@ -41244,8 +40980,9 @@ }, "node_modules/temp-file/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -41587,16 +41324,18 @@ }, "node_modules/tmp": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "license": "MIT", "engines": { "node": ">=14.14" } }, "node_modules/tmp-promise": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, - "license": "MIT", "dependencies": { "tmp": "^0.2.0" } @@ -41737,8 +41476,9 @@ }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, - "license": "WTFPL", "dependencies": { "utf8-byte-length": "^1.0.1" } @@ -41968,9 +41708,10 @@ } }, "node_modules/type-fest": { - "version": "4.10.3", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", + "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -42542,9 +42283,10 @@ "license": "MIT" }, "node_modules/utf8-byte-length": { - "version": "1.0.4", - "dev": true, - "license": "WTFPL" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true }, "node_modules/util": { "version": "0.11.1", @@ -42630,8 +42372,9 @@ }, "node_modules/verror": { "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -43143,9 +42886,10 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -43768,8 +43512,8 @@ }, "node_modules/wide-align": { "version": "1.1.5", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -43907,8 +43651,9 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "license": "MIT", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -44056,8 +43801,9 @@ }, "node_modules/zip-stream": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "archiver-utils": "^3.0.4", @@ -44070,8 +43816,9 @@ }, "node_modules/zip-stream/node_modules/archiver-utils": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "glob": "^7.2.3", @@ -44091,8 +43838,10 @@ }, "node_modules/zip-stream/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "ISC", "peer": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -44111,8 +43860,9 @@ }, "node_modules/zip-stream/node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "inherits": "^2.0.3", diff --git a/package.json b/package.json index 966feed06fb8..5efe9951de9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.3-2", + "version": "9.0.10-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -60,13 +60,15 @@ "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.ts", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", - "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/" + "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/", + "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", + "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy " }, "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.88", + "@expensify/react-native-live-markdown": "0.1.105", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -79,12 +81,12 @@ "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", - "@kie/act-js": "^2.6.0", + "@kie/act-js": "^2.6.2", "@kie/mock-github": "2.0.1", "@onfido/react-native-sdk": "10.6.0", "@react-native-camera-roll/camera-roll": "7.4.0", "@react-native-clipboard/clipboard": "^1.13.2", - "@react-native-community/geolocation": "3.2.1", + "@react-native-community/geolocation": "3.3.0", "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", @@ -108,7 +110,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.19", + "expensify-common": "2.0.49", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -132,7 +134,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.13", + "react-fast-pdf": "1.0.14", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -155,23 +157,23 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.54", + "react-native-onyx": "2.0.56", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", - "react-native-permissions": "^3.9.3", + "react-native-permissions": "^3.10.0", "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-plaid-link-sdk": "11.11.0", + "react-native-qrcode-svg": "git+https://github.com/Expensify/react-native-qrcode-svg", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.8.0", - "react-native-release-profiler": "^0.1.6", + "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "3.30.1", + "react-native-screens": "3.32.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", - "react-native-svg": "14.1.0", + "react-native-svg": "15.4.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", @@ -254,7 +256,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "^0.0.0-experimental-c23de8d-20240515", + "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", @@ -266,14 +268,14 @@ "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "^29.4.1", - "electron-builder": "24.13.2", + "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-expensify": "^2.0.52", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-react-compiler": "^0.0.0-experimental-53bb89e-20240515", + "eslint-plugin-react-compiler": "0.0.0-experimental-0998c1e-20240625", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -288,9 +290,11 @@ "onchange": "^7.1.0", "openai": "^4.47.2", "patch-package": "^8.0.0", + "peggy": "^4.0.3", "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", + "react-compiler-healthcheck": "^0.0.0-experimental-b130d5f-20240625", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.2.0", @@ -303,7 +307,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "type-fest": "^4.10.2", + "type-fest": "4.20.0", "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", diff --git a/patches/@expensify+react-native-live-markdown+0.1.85.patch b/patches/@expensify+react-native-live-markdown+0.1.85.patch deleted file mode 100644 index f745786a088e..000000000000 --- a/patches/@expensify+react-native-live-markdown+0.1.85.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js b/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js -index e975fb2..6a4b510 100644 ---- a/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js -+++ b/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js -@@ -53,7 +53,7 @@ function setCursorPosition(target, start, end = null) { - // 3. Caret at the end of whole input, when pressing enter - // 4. All other placements - if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) { -- if (nextChar !== '\n') { -+ if (nextChar !== '\n' && i !== n - 1 && nextChar) { - range.setStart(textNodes[i + 1], 0); - } else if (i !== textNodes.length - 1) { - range.setStart(textNodes[i], 1); diff --git a/patches/@perf-profiler+android+0.12.1.patch b/patches/@perf-profiler+android+0.12.1.patch deleted file mode 100644 index e6e4a90d6ab4..000000000000 --- a/patches/@perf-profiler+android+0.12.1.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js b/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js -index 59aeed9..ee1d8a6 100644 ---- a/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js -+++ b/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js -@@ -28,7 +28,7 @@ exports.CppProfilerName = `BAMPerfProfiler`; - // into the Flipper plugin directory - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error --const binaryFolder = global.Flipper -+const binaryFolder = (global.Flipper || process.env.AWS) - ? `${__dirname}/bin` - : `${__dirname}/../../..${__dirname.includes("dist") ? "/.." : ""}/cpp-profiler/bin`; - class UnixProfiler { -diff --git a/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts b/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts -index ccacf09..1eea659 100644 ---- a/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts -+++ b/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts -@@ -26,7 +26,7 @@ export const CppProfilerName = `BAMPerfProfiler`; - // into the Flipper plugin directory - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error --const binaryFolder = global.Flipper -+const binaryFolder = (global.Flipper || process.env.AWS) - ? `${__dirname}/bin` - : `${__dirname}/../../..${__dirname.includes("dist") ? "/.." : ""}/cpp-profiler/bin`; - diff --git a/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch b/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch index f8e171008e14..c679bdbf73b9 100644 --- a/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch +++ b/patches/@react-native-community+netinfo+11.2.1+002+turbomodule.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-native-community/netinfo/android/build.gradle b/node_modules/@react-native-community/netinfo/android/build.gradle -index 0d617ed..e93d64a 100644 +index 0d617ed..97439e6 100644 --- a/node_modules/@react-native-community/netinfo/android/build.gradle +++ b/node_modules/@react-native-community/netinfo/android/build.gradle @@ -3,9 +3,10 @@ buildscript { @@ -105,7 +105,6 @@ index 0d617ed..e93d64a 100644 + implementation 'com.facebook.react:react-native:+' + } } -\ No newline at end of file diff --git a/node_modules/@react-native-community/netinfo/android/src/main/java/com/reactnativecommunity/netinfo/NetInfoModuleImpl.java b/node_modules/@react-native-community/netinfo/android/src/main/java/com/reactnativecommunity/netinfo/NetInfoModuleImpl.java index 2c3280b..296bbfd 100644 --- a/node_modules/@react-native-community/netinfo/android/src/main/java/com/reactnativecommunity/netinfo/NetInfoModuleImpl.java @@ -1609,10 +1608,10 @@ index 095dd3b..596ace1 100644 +{"version":3,"names":["NetInfoStateType","exports","NetInfoCellularGeneration"],"sources":["types.ts"],"sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @format\n */\n\nexport enum NetInfoStateType {\n unknown = 'unknown',\n none = 'none',\n cellular = 'cellular',\n wifi = 'wifi',\n bluetooth = 'bluetooth',\n ethernet = 'ethernet',\n wimax = 'wimax',\n vpn = 'vpn',\n other = 'other',\n}\n\nexport type NetInfoMethodType = 'HEAD' | 'GET';\n\nexport enum NetInfoCellularGeneration {\n '2g' = '2g',\n '3g' = '3g',\n '4g' = '4g',\n '5g' = '5g',\n}\n\nexport interface NetInfoConnectedDetails {\n isConnectionExpensive: boolean;\n}\n\ninterface NetInfoConnectedState<\n T extends NetInfoStateType,\n D extends Record = Record,\n> {\n type: T;\n isConnected: true;\n isInternetReachable: boolean | null;\n details: D & NetInfoConnectedDetails;\n isWifiEnabled?: boolean;\n}\n\ninterface NetInfoDisconnectedState {\n type: T;\n isConnected: false;\n isInternetReachable: false;\n details: null;\n isWifiEnabled?: boolean;\n}\n\nexport interface NetInfoUnknownState {\n type: NetInfoStateType.unknown;\n isConnected: boolean | null;\n isInternetReachable: null;\n details: null;\n isWifiEnabled?: boolean;\n}\n\nexport type NetInfoNoConnectionState =\n NetInfoDisconnectedState;\nexport type NetInfoDisconnectedStates =\n | NetInfoUnknownState\n | NetInfoNoConnectionState;\n\nexport type NetInfoCellularState = NetInfoConnectedState<\n NetInfoStateType.cellular,\n {\n cellularGeneration: NetInfoCellularGeneration | null;\n carrier: string | null;\n }\n>;\nexport type NetInfoWifiState = NetInfoConnectedState<\n NetInfoStateType.wifi,\n {\n ssid: string | null;\n bssid: string | null;\n strength: number | null;\n ipAddress: string | null;\n subnet: string | null;\n frequency: number | null;\n linkSpeed: number | null;\n rxLinkSpeed: number | null;\n txLinkSpeed: number | null;\n }\n>;\nexport type NetInfoBluetoothState =\n NetInfoConnectedState;\nexport type NetInfoEthernetState = NetInfoConnectedState<\n NetInfoStateType.ethernet,\n {\n ipAddress: string | null;\n subnet: string | null;\n }\n>;\nexport type NetInfoWimaxState = NetInfoConnectedState;\nexport type NetInfoVpnState = NetInfoConnectedState;\nexport type NetInfoOtherState = NetInfoConnectedState;\nexport type NetInfoConnectedStates =\n | NetInfoCellularState\n | NetInfoWifiState\n | NetInfoBluetoothState\n | NetInfoEthernetState\n | NetInfoWimaxState\n | NetInfoVpnState\n | NetInfoOtherState;\n\nexport type NetInfoState = NetInfoDisconnectedStates | NetInfoConnectedStates;\n\nexport type NetInfoChangeHandler = (state: NetInfoState) => void;\nexport type NetInfoSubscription = () => void;\n\nexport interface NetInfoConfiguration {\n reachabilityUrl: string;\n reachabilityMethod?: NetInfoMethodType;\n reachabilityHeaders?: Record;\n reachabilityTest: (response: Response) => Promise;\n reachabilityLongTimeout: number;\n reachabilityShortTimeout: number;\n reachabilityRequestTimeout: number;\n reachabilityShouldRun: () => boolean;\n shouldFetchWiFiSSID: boolean;\n useNativeReachability: boolean;\n}\n"],"mappings":";;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAPA,IASYA,gBAAgB,GAAAC,OAAA,CAAAD,gBAAA,0BAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAhBA,gBAAgB;EAAA,OAAhBA,gBAAgB;AAAA;AAAA,IAchBE,yBAAyB,GAAAD,OAAA,CAAAC,yBAAA,0BAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAzBA,yBAAyB;EAAA,OAAzBA,yBAAyB;AAAA"} \ No newline at end of file diff --git a/node_modules/@react-native-community/netinfo/lib/module/index.js b/node_modules/@react-native-community/netinfo/lib/module/index.js -index 147c72e..02aa0db 100644 +index 147c72e..5de4e7c 100644 --- a/node_modules/@react-native-community/netinfo/lib/module/index.js +++ b/node_modules/@react-native-community/netinfo/lib/module/index.js -@@ -6,20 +6,23 @@ +@@ -6,20 +6,26 @@ * * @format */ @@ -1635,11 +1634,14 @@ index 147c72e..02aa0db 100644 const createState = () => { return new State(_configuration); }; ++ ++// Track ongoing requests ++let isRequestInProgress = false; + /** * Configures the library with the given configuration. Note that calling this will stop all * previously added listeners from being called again. It is best to call this right when your -@@ -27,23 +30,20 @@ const createState = () => { +@@ -27,23 +33,20 @@ const createState = () => { * * @param configuration The new configuration to set. */ @@ -1666,7 +1668,7 @@ index 147c72e..02aa0db 100644 /** * Returns a `Promise` that resolves to a `NetInfoState` object. * This function operates on the global singleton instance configured using `configure()` -@@ -52,27 +52,25 @@ export function configure(configuration) { +@@ -52,27 +55,33 @@ export function configure(configuration) { * * @returns A Promise which contains the current connection state. */ @@ -1689,14 +1691,22 @@ index 147c72e..02aa0db 100644 if (!_state) { _state = createState(); } -- - return _state._fetchCurrentState(); + +- return _state._fetchCurrentState(); ++ if (isRequestInProgress) { ++ return _state.latest(); // Return the latest state if a request is already in progress ++ } ++ ++ isRequestInProgress = true; ++ return _state._fetchCurrentState().finally(() => { ++ isRequestInProgress = false; ++ }); } + /** * Subscribe to the global singleton's connection information. The callback is called with a parameter of type * [`NetInfoState`](README.md#netinfostate) whenever the connection state changes. Your listener -@@ -84,18 +82,16 @@ export function refresh() { +@@ -84,18 +93,16 @@ export function refresh() { * * @returns A function which can be called to unsubscribe. */ @@ -1716,7 +1726,7 @@ index 147c72e..02aa0db 100644 /** * A React Hook into this library's singleton which updates when the connection state changes. * -@@ -103,12 +99,10 @@ export function addEventListener(listener) { +@@ -103,12 +110,10 @@ export function addEventListener(listener) { * * @returns The connection state. */ @@ -1729,7 +1739,7 @@ index 147c72e..02aa0db 100644 const [netInfo, setNetInfo] = useState({ type: Types.NetInfoStateType.unknown, isConnected: null, -@@ -120,6 +114,7 @@ export function useNetInfo(configuration) { +@@ -120,6 +125,7 @@ export function useNetInfo(configuration) { }, []); return netInfo; } @@ -1737,7 +1747,7 @@ index 147c72e..02aa0db 100644 /** * A React Hook which manages an isolated instance of the network info manager. * This is not a hook into a singleton shared state. NetInfo.configure, NetInfo.addEventListener, -@@ -129,7 +124,6 @@ export function useNetInfo(configuration) { +@@ -129,7 +135,6 @@ export function useNetInfo(configuration) { * * @returns the netInfo state and a refresh function */ @@ -1745,7 +1755,7 @@ index 147c72e..02aa0db 100644 export function useNetInfoInstance(isPaused = false, configuration) { const [networkInfoManager, setNetworkInfoManager] = useState(); const [netInfo, setNetInfo] = useState({ -@@ -142,8 +136,8 @@ export function useNetInfoInstance(isPaused = false, configuration) { +@@ -142,8 +147,8 @@ export function useNetInfoInstance(isPaused = false, configuration) { if (isPaused) { return; } @@ -2610,22 +2620,13 @@ index 6982220..b515270 100644 }; export default _default; diff --git a/node_modules/@react-native-community/netinfo/package.json b/node_modules/@react-native-community/netinfo/package.json -index 3c80db2..61e6564 100644 +index 3c80db2..15d214d 100644 --- a/node_modules/@react-native-community/netinfo/package.json +++ b/node_modules/@react-native-community/netinfo/package.json -@@ -48,6 +48,7 @@ - "network info" - ], - "peerDependencies": { -+ "react": "*", - "react-native": ">=0.59" +@@ -97,6 +97,14 @@ + "webpack-cli": "^3.3.10", + "webpack-dev-server": "^3.11.3" }, - "dependencies": {}, -@@ -121,5 +122,13 @@ - "yarn eslint --fix", - "git add" - ] -+ }, + "codegenConfig": { + "name": "RNCNetInfoSpec", + "type": "modules", @@ -2633,8 +2634,10 @@ index 3c80db2..61e6564 100644 + "android": { + "javaPackageName": "com.reactnativecommunity.netinfo" + } - } - } ++ }, + "repository": { + "type": "git", + "url": "https://github.com/react-native-netinfo/react-native-netinfo.git" diff --git a/node_modules/@react-native-community/netinfo/react-native-netinfo.podspec b/node_modules/@react-native-community/netinfo/react-native-netinfo.podspec index e34e728..9090eb1 100644 --- a/node_modules/@react-native-community/netinfo/react-native-netinfo.podspec @@ -3062,4 +3065,4 @@ index 878f7ba..0000000 - -#Files generated by the VS build -**/Generated Files/** -- +- \ No newline at end of file diff --git a/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch b/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch index 8941bb380a79..f68cd6fe9ca4 100644 --- a/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch +++ b/patches/@react-navigation+core+6.4.11+001+fix-react-strictmode.patch @@ -42,3 +42,48 @@ index 051520b..6fb49e0 100644 }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); +diff --git a/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx b/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx +index b1971ba..7d550e0 100644 +--- a/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx ++++ b/node_modules/@react-navigation/core/src/useNavigationBuilder.tsx +@@ -362,11 +362,6 @@ export default function useNavigationBuilder< + + const stateCleanedUp = React.useRef(false); + +- const cleanUpState = React.useCallback(() => { +- setCurrentState(undefined); +- stateCleanedUp.current = true; +- }, [setCurrentState]); +- + const setState = React.useCallback( + (state: NavigationState | PartialState | undefined) => { + if (stateCleanedUp.current) { +@@ -540,6 +535,9 @@ export default function useNavigationBuilder< + state = nextState; + + React.useEffect(() => { ++ // In strict mode, React will double-invoke effects. ++ // So we need to reset the flag if component was not unmounted ++ stateCleanedUp.current = false; + setKey(navigatorKey); + + if (!getIsInitial()) { +@@ -551,14 +549,10 @@ export default function useNavigationBuilder< + + return () => { + // We need to clean up state for this navigator on unmount +- // We do it in a timeout because we need to detect if another navigator mounted in the meantime +- // For example, if another navigator has started rendering, we should skip cleanup +- // Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it +- setTimeout(() => { +- if (getCurrentState() !== undefined && getKey() === navigatorKey) { +- cleanUpState(); +- } +- }, 0); ++ if (getCurrentState() !== undefined && getKey() === navigatorKey) { ++ setCurrentState(undefined); ++ stateCleanedUp.current = true; ++ } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); diff --git a/patches/eslint-plugin-react-compiler+0.0.0-experimental-53bb89e-20240515.patch b/patches/eslint-plugin-react-compiler+0.0.0-experimental-53bb89e-20240515.patch deleted file mode 100644 index f81f70944dd2..000000000000 --- a/patches/eslint-plugin-react-compiler+0.0.0-experimental-53bb89e-20240515.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/eslint-plugin-react-compiler/dist/index.js b/node_modules/eslint-plugin-react-compiler/dist/index.js -index a0f47a7..f649250 100644 ---- a/node_modules/eslint-plugin-react-compiler/dist/index.js -+++ b/node_modules/eslint-plugin-react-compiler/dist/index.js -@@ -69108,7 +69108,7 @@ const rule = { - return false; - } - let babelAST; -- if (context.filename.endsWith(".tsx") || context.filename.endsWith(".ts")) { -+ if (filename.endsWith(".tsx") || filename.endsWith(".ts")) { - try { - const { parse: babelParse } = require("@babel/parser"); - babelAST = babelParse(sourceCode, { diff --git a/patches/focus-trap+7.5.4.patch b/patches/focus-trap+7.5.4.patch new file mode 100644 index 000000000000..c7b2aef2b51f --- /dev/null +++ b/patches/focus-trap+7.5.4.patch @@ -0,0 +1,106 @@ +diff --git a/node_modules/focus-trap/dist/focus-trap.esm.js b/node_modules/focus-trap/dist/focus-trap.esm.js +index 10d56db..a6d76d8 100644 +--- a/node_modules/focus-trap/dist/focus-trap.esm.js ++++ b/node_modules/focus-trap/dist/focus-trap.esm.js +@@ -100,8 +100,8 @@ var isKeyForward = function isKeyForward(e) { + var isKeyBackward = function isKeyBackward(e) { + return isTabEvent(e) && e.shiftKey; + }; +-var delay = function delay(fn) { +- return setTimeout(fn, 0); ++var delay = function delay(fn, delayTime = 0) { ++ return setTimeout(() => setTimeout(fn, delayTime), 0); + }; + + // Array.find/findIndex() are not supported on IE; this replicates enough +@@ -283,7 +283,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { + return node; + }; + var getInitialFocusNode = function getInitialFocusNode() { +- var node = getNodeForOption('initialFocus'); ++ var node = getNodeForOption('initialFocus', state.containers); + + // false explicitly indicates we want no initialFocus at all + if (node === false) { +@@ -744,7 +744,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { + // that caused the focus trap activation. + state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () { + tryFocus(getInitialFocusNode()); +- }) : tryFocus(getInitialFocusNode()); ++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined) : tryFocus(getInitialFocusNode()); + doc.addEventListener('focusin', checkFocusIn, true); + doc.addEventListener('mousedown', checkPointerDown, { + capture: true, +@@ -880,7 +880,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) { + tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)); + } + onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate(); +- }); ++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined); + }; + if (returnFocus && checkCanReturnFocus) { + checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation); +diff --git a/node_modules/focus-trap/index.d.ts b/node_modules/focus-trap/index.d.ts +index 400db1b..69f4b94 100644 +--- a/node_modules/focus-trap/index.d.ts ++++ b/node_modules/focus-trap/index.d.ts +@@ -16,7 +16,7 @@ declare module 'focus-trap' { + * `document.querySelector()` to find the DOM node), `false` to explicitly indicate + * an opt-out, or a function that returns a DOM node or `false`. + */ +- export type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse); ++ export type FocusTargetOrFalse = FocusTargetValueOrFalse | ((containers?: HTMLElement[]) => FocusTargetValueOrFalse | undefined); + + type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean; + type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean; +@@ -185,7 +185,7 @@ declare module 'focus-trap' { + * This prevents elements within the focusable element from capturing + * the event that triggered the focus trap activation. + */ +- delayInitialFocus?: boolean; ++ delayInitialFocus?: boolean | number; + /** + * Default: `window.document`. Document where the focus trap will be active. + * This allows to use FocusTrap in an iFrame context. +diff --git a/node_modules/focus-trap/index.js b/node_modules/focus-trap/index.js +index de8e46a..bfc8b63 100644 +--- a/node_modules/focus-trap/index.js ++++ b/node_modules/focus-trap/index.js +@@ -63,8 +63,8 @@ const isKeyBackward = function (e) { + return isTabEvent(e) && e.shiftKey; + }; + +-const delay = function (fn) { +- return setTimeout(fn, 0); ++const delay = function (fn, delayTime = 0) { ++ return setTimeout(() => setTimeout(fn, delayTime), 0); + }; + + // Array.find/findIndex() are not supported on IE; this replicates enough +@@ -267,7 +267,7 @@ const createFocusTrap = function (elements, userOptions) { + }; + + const getInitialFocusNode = function () { +- let node = getNodeForOption('initialFocus'); ++ let node = getNodeForOption('initialFocus', state.containers); + + // false explicitly indicates we want no initialFocus at all + if (node === false) { +@@ -817,7 +817,7 @@ const createFocusTrap = function (elements, userOptions) { + state.delayInitialFocusTimer = config.delayInitialFocus + ? delay(function () { + tryFocus(getInitialFocusNode()); +- }) ++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined) + : tryFocus(getInitialFocusNode()); + + doc.addEventListener('focusin', checkFocusIn, true); +@@ -989,7 +989,7 @@ const createFocusTrap = function (elements, userOptions) { + tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)); + } + onPostDeactivate?.(); +- }); ++ }, typeof config.delayInitialFocus === 'number' ? config.delayInitialFocus : undefined); + }; + + if (returnFocus && checkCanReturnFocus) { diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625.patch b/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625.patch new file mode 100644 index 000000000000..d7c02701a636 --- /dev/null +++ b/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625.patch @@ -0,0 +1,90 @@ +diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js +index b427385..4bf23db 100755 +--- a/node_modules/react-compiler-healthcheck/dist/index.js ++++ b/node_modules/react-compiler-healthcheck/dist/index.js +@@ -69154,7 +69154,7 @@ var reactCompilerCheck = { + compile(source, path); + } + }, +- report() { ++ report(verbose) { + const totalComponents = + SucessfulCompilation.length + + countUniqueLocInEvents(OtherFailures) + +@@ -69164,6 +69164,50 @@ var reactCompilerCheck = { + `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.` + ) + ); ++ ++ if (verbose) { ++ for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) { ++ const filename = compilation.fnLoc?.filename; ++ ++ if (compilation.kind === "CompileSuccess") { ++ const name = compilation.fnName; ++ const isHook = name?.startsWith('use'); ++ ++ if (name) { ++ console.log( ++ chalk.green( ++ `Successfully compiled ${isHook ? "hook" : "component" } [${name}](${filename})` ++ ) ++ ); ++ } else { ++ console.log(chalk.green(`Successfully compiled ${compilation.fnLoc?.filename}`)); ++ } ++ } ++ ++ if (compilation.kind === "CompileError") { ++ const { reason, severity, loc } = compilation.detail; ++ ++ const lnNo = loc.start?.line; ++ const colNo = loc.start?.column; ++ ++ const isTodo = severity === ErrorSeverity.Todo; ++ ++ console.log( ++ chalk[isTodo ? 'yellow' : 'red']( ++ `Failed to compile ${ ++ filename ++ }${ ++ lnNo !== undefined ? `:${lnNo}${ ++ colNo !== undefined ? `:${colNo}` : "" ++ }.` : "" ++ }` ++ ), ++ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "") ++ ); ++ console.log("\n"); ++ } ++ } ++ } + }, + }; + const JsFileExtensionRE = /(js|ts|jsx|tsx)$/; +@@ -69200,9 +69244,16 @@ function main() { + type: "string", + default: "**/+(*.{js,mjs,jsx,ts,tsx}|package.json)", + }) ++ .option('verbose', { ++ description: 'run with verbose logging', ++ type: 'boolean', ++ default: false, ++ alias: 'v', ++ }) + .parseSync(); + const spinner = ora("Checking").start(); + let src = argv.src; ++ let verbose = argv.verbose; + const globOptions = { + onlyFiles: true, + ignore: [ +@@ -69222,7 +69273,7 @@ function main() { + libraryCompatCheck.run(source, path); + } + spinner.stop(); +- reactCompilerCheck.report(); ++ reactCompilerCheck.report(verbose); + strictModeCheck.report(); + libraryCompatCheck.report(); + }); diff --git a/patches/react-native-keyboard-controller+1.12.2.patch.patch b/patches/react-native-keyboard-controller+1.12.2.patch similarity index 100% rename from patches/react-native-keyboard-controller+1.12.2.patch.patch rename to patches/react-native-keyboard-controller+1.12.2.patch diff --git a/patches/react-native-plaid-link-sdk+11.5.0+001+initial.patch b/patches/react-native-plaid-link-sdk+11.5.0+001+initial.patch deleted file mode 100644 index 6035477256b7..000000000000 --- a/patches/react-native-plaid-link-sdk+11.5.0+001+initial.patch +++ /dev/null @@ -1,4 +0,0 @@ -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.m b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm -similarity index 100% -rename from node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.m -rename to node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm diff --git a/patches/react-native-plaid-link-sdk+11.5.0+002+turbomodule.patch b/patches/react-native-plaid-link-sdk+11.5.0+002+turbomodule.patch deleted file mode 100644 index 7d5aab6c84cf..000000000000 --- a/patches/react-native-plaid-link-sdk+11.5.0+002+turbomodule.patch +++ /dev/null @@ -1,3287 +0,0 @@ -diff --git a/node_modules/react-native-plaid-link-sdk/README.md b/node_modules/react-native-plaid-link-sdk/README.md -index 93ebca6..7bea608 100644 ---- a/node_modules/react-native-plaid-link-sdk/README.md -+++ b/node_modules/react-native-plaid-link-sdk/README.md -@@ -49,7 +49,6 @@ cd ios && bundle install && bundle exec pod install - - AutoLinking should handle all of the Android setup. - -- - ### React Native Setup - - - To initialize `PlaidLink`, you will need to first create a `link_token` at [/link/token/create](https://plaid.com/docs/#create-link-token). Check out our [QuickStart guide](https://plaid.com/docs/quickstart/#introduction) for additional API information. -@@ -58,7 +57,13 @@ AutoLinking should handle all of the Android setup. - - ```javascript - import { Text } from 'react-native'; --import { PlaidLink, LinkSuccess, LinkExit, LinkLogLevel, LinkIOSPresentationStyle } from 'react-native-plaid-link-sdk'; -+import { -+ PlaidLink, -+ LinkSuccess, -+ LinkExit, -+ LinkLogLevel, -+ LinkIOSPresentationStyle, -+} from 'react-native-plaid-link-sdk'; - - const MyPlaidComponent = () => { - return ( -@@ -77,7 +82,7 @@ const MyPlaidComponent = () => { - // UI is always presented in full screen on Android. - iOSPresentationStyle={LinkIOSPresentationStyle.MODAL} - > -- Add Account -+ Add Account - - ); - }; -@@ -92,6 +97,7 @@ const MyPlaidComponent = () => { - ##### Android OAuth Requirements - - ###### Register your app package name -+ - 1. Log into your [Plaid Dashboard](https://dashboard.plaid.com/developers/api) and navigate to the API page under the Developers tab. - 2. Next to Allowed Android package names click "Configure" then "Add New Android Package Name". - 3. Enter your package name, for example `com.plaid.example`. -@@ -100,17 +106,16 @@ const MyPlaidComponent = () => { - ##### iOS OAuth Requirements - - For iOS OAuth to work, specific requirements must be met. -+ - 1. Redirect URIs must be [registered](https://plaid.com/docs/link/ios/#register-your-redirect-uri), and set up as [universal links](https://developer.apple.com/documentation/xcode/supporting-associated-domains). - 2. Your native iOS application, must be configured with your associated domain. See your iOS [set up universal links](https://plaid.com/docs/link/ios/#set-up-universal-links) for more information. - -- - ##### Link Token OAuth Requirements - - - On iOS you must configure your `link_token` with a [redirect_uri](https://plaid.com/docs/api/tokens/#link-token-create-request-redirect-uri) to support OAuth. When creating a `link_token` for initializing Link on Android, `android_package_name` must be specified and `redirect_uri` must be left blank. - - - On Android you must configure your `link_token` with an [android_package_name](https://plaid.com/docs/api/tokens/#link-token-create-request-android-package-name) to support OAuth. When creating a `link_token` for initializing Link on iOS, `android_package_name` must be left blank and `redirect_uri` should be used instead. - -- - #### To receive onEvent callbacks: - - The React Native Plaid module emits `onEvent` events throughout the account linking process — see [details here](https://plaid.com/docs/link/react-native/#onevent). To receive these events in your React Native app, wrap the `PlaidLink` react component with the following in order to listen for those events: -@@ -139,9 +144,9 @@ class PlaidEventContainer extends React.Component { - You can also use the `usePlaidEmitter` hook in react functional components: - - ```javascript -- usePlaidEmitter((event: LinkEvent) => { -- console.log(event) -- }) -+usePlaidEmitter((event: LinkEvent) => { -+ console.log(event); -+}); - ``` - - ## Upgrading -@@ -165,6 +170,8 @@ While these older versions are expected to continue to work without disruption, - | 11.0.2 | * | [4.0.0+] | 21 | 33 | >=5.0.0 | 14.0 | Active, supports Xcode 15.0.1 | - | 11.0.1 | * | [4.0.0+] | 21 | 33 | >=5.0.0 | 14.0 | Active, supports Xcode 15.0.1 | - | 11.0.0 | * | [4.0.0+] | 21 | 33 | >=5.0.0 | 14.0 | Active, supports Xcode 15.0.1 | -+| 10.13.0 | >= 0.66.0 | [3.14.3+] | 21 | 33 | >=4.7.2 | 11.0 | Active, supports Xcode 14 | -+| 10.12.0 | >= 0.66.0 | [3.14.3+] | 21 | 33 | >=4.7.1 | 11.0 | Active, supports Xcode 14 | - | 10.11.0 | >= 0.66.0 | [3.14.1+] | 21 | 33 | >=4.7.1 | 11.0 | Active, supports Xcode 14 | - | ~10.10.0~ | >= 0.66.0 | [3.14.2+] | 21 | 33 | >=4.7.1 | 11.0 | **Deprecated** | - | 10.9.1 | >= 0.66.0 | [3.14.1+] | 21 | 33 | >=4.7.0 | 11.0 | Active, supports Xcode 14 | -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/checksums.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/checksums.lock -deleted file mode 100644 -index b5da584..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/checksums.lock and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/md5-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/md5-checksums.bin -deleted file mode 100644 -index ef608b4..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/md5-checksums.bin and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/sha1-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/sha1-checksums.bin -deleted file mode 100644 -index 0856ae4..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/checksums/sha1-checksums.bin and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock -deleted file mode 100644 -index 12aea68..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/dependencies-accessors.lock and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/gc.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/dependencies-accessors/gc.properties -deleted file mode 100644 -index e69de29..0000000 -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileChanges/last-build.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileChanges/last-build.bin -deleted file mode 100644 -index f76dd23..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileChanges/last-build.bin and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileHashes/fileHashes.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileHashes/fileHashes.lock -deleted file mode 100644 -index 752a252..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/fileHashes/fileHashes.lock and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/gc.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/7.4.2/gc.properties -deleted file mode 100644 -index e69de29..0000000 -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock -deleted file mode 100644 -index 470ca89..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/cache.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/cache.properties -deleted file mode 100644 -index 1439672..0000000 ---- a/node_modules/react-native-plaid-link-sdk/android/.gradle/buildOutputCleanup/cache.properties -+++ /dev/null -@@ -1,2 +0,0 @@ --#Thu Nov 09 09:41:17 PST 2023 --gradle.version=7.4.2 -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/checksums.lock b/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/checksums.lock -deleted file mode 100644 -index 34602e1..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/checksums.lock and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/md5-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/md5-checksums.bin -deleted file mode 100644 -index 2420123..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/md5-checksums.bin and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/sha1-checksums.bin b/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/sha1-checksums.bin -deleted file mode 100644 -index 1081852..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/android/.gradle/checksums/sha1-checksums.bin and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/android/.gradle/vcs-1/gc.properties b/node_modules/react-native-plaid-link-sdk/android/.gradle/vcs-1/gc.properties -deleted file mode 100644 -index e69de29..0000000 -diff --git a/node_modules/react-native-plaid-link-sdk/android/.idea/gradle.xml b/node_modules/react-native-plaid-link-sdk/android/.idea/gradle.xml -deleted file mode 100644 -index 0364d75..0000000 ---- a/node_modules/react-native-plaid-link-sdk/android/.idea/gradle.xml -+++ /dev/null -@@ -1,14 +0,0 @@ -- -- -- -- -- -- -- -\ No newline at end of file -diff --git a/node_modules/react-native-plaid-link-sdk/android/.idea/misc.xml b/node_modules/react-native-plaid-link-sdk/android/.idea/misc.xml -deleted file mode 100644 -index a318cae..0000000 ---- a/node_modules/react-native-plaid-link-sdk/android/.idea/misc.xml -+++ /dev/null -@@ -1,9 +0,0 @@ -- -- -- -- -- -- -- -- -\ No newline at end of file -diff --git a/node_modules/react-native-plaid-link-sdk/android/.idea/vcs.xml b/node_modules/react-native-plaid-link-sdk/android/.idea/vcs.xml -deleted file mode 100644 -index 6c0b863..0000000 ---- a/node_modules/react-native-plaid-link-sdk/android/.idea/vcs.xml -+++ /dev/null -@@ -1,6 +0,0 @@ -- -- -- -- -- -- -\ No newline at end of file -diff --git a/node_modules/react-native-plaid-link-sdk/android/build.gradle b/node_modules/react-native-plaid-link-sdk/android/build.gradle -index 2d9e2ce..e88208b 100644 ---- a/node_modules/react-native-plaid-link-sdk/android/build.gradle -+++ b/node_modules/react-native-plaid-link-sdk/android/build.gradle -@@ -12,7 +12,12 @@ allprojects { - - - buildscript { -- ext.kotlin_version = '1.8.22' -+ ext { -+ kotlin_version = '1.8.22' -+ } -+ ext.safeExtGet = {prop, fallback -> -+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback -+ } - repositories { - google() - mavenCentral() -@@ -25,10 +30,32 @@ buildscript { - } - } - -+def isNewArchitectureEnabled() { -+ // To opt-in for the New Architecture, you can either: -+ // - Set `newArchEnabled` to true inside the `gradle.properties` file -+ // - Invoke gradle with `-newArchEnabled=true` -+ // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` -+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" -+} -+ -+if (isNewArchitectureEnabled()) { -+ apply plugin: "com.facebook.react" -+} -+ - apply plugin: 'com.android.library' - apply plugin: "kotlin-android" - - android { -+ -+ // Used to override the NDK path/version on internal CI or by allowing -+ // users to customize the NDK path/version from their root project (e.g. for M1 support) -+ if (rootProject.hasProperty("ndkPath")) { -+ ndkPath rootProject.ext.ndkPath -+ } -+ if (rootProject.hasProperty("ndkVersion")) { -+ ndkVersion rootProject.ext.ndkVersion -+ } -+ - def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger() - if (agpVersion >= 7) { - namespace 'com.plaid' -@@ -52,6 +79,14 @@ android { - } - } - -+ sourceSets.main { -+ java { -+ if (!isNewArchitectureEnabled()) { -+ srcDirs += 'src/paper/java' -+ } -+ } -+ } -+ - buildTypes { - release { - debuggable = false -diff --git a/node_modules/react-native-plaid-link-sdk/android/local.properties b/node_modules/react-native-plaid-link-sdk/android/local.properties -deleted file mode 100644 -index 0b4e321..0000000 ---- a/node_modules/react-native-plaid-link-sdk/android/local.properties -+++ /dev/null -@@ -1,8 +0,0 @@ --## This file must *NOT* be checked into Version Control Systems, --# as it contains information specific to your local configuration. --# --# Location of the SDK. This is only used by Gradle. --# For customization when using a Version Control System, please read the --# header note. --#Fri Aug 11 13:58:32 PDT 2023 --sdk.dir=/Users/dtroupe/Library/Android/sdk -diff --git a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt -index c73011f..66fd266 100644 ---- a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt -+++ b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PLKEmbeddedViewManager.kt -@@ -19,9 +19,9 @@ class PLKEmbeddedViewManager : SimpleViewManager() { - } - - override fun getExportedCustomBubblingEventTypeConstants(): Map { -- return mapOf( -- EVENT_NAME to mapOf( -- "phasedRegistrationNames" to mapOf( -+ return mutableMapOf( -+ EVENT_NAME to mutableMapOf( -+ "phasedRegistrationNames" to mutableMapOf( - "bubbled" to EVENT_NAME - ) - )) -diff --git a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt -index 293374a..b79352e 100644 ---- a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt -+++ b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidModule.kt -@@ -24,9 +24,9 @@ import org.json.JSONException - import org.json.JSONObject - import java.util.ArrayList - --@ReactModule(name = PlaidModule.TAG) -+@ReactModule(name = PlaidModule.NAME) - class PlaidModule internal constructor(reactContext: ReactApplicationContext) : -- ReactContextBaseJavaModule(reactContext), ActivityEventListener { -+ NativePlaidLinkModuleAndroidSpec(reactContext), ActivityEventListener { - - val mActivityResultManager by lazy { ActivityResultManager() } - -@@ -38,11 +38,11 @@ class PlaidModule internal constructor(reactContext: ReactApplicationContext) : - companion object { - private const val LINK_TOKEN_PREFIX = "link" - -- const val TAG = "PlaidAndroid" -+ const val NAME = "PlaidAndroid" - } - - override fun getName(): String { -- return PlaidModule.TAG -+ return NAME - } - - override fun initialize() { -@@ -78,7 +78,7 @@ class PlaidModule internal constructor(reactContext: ReactApplicationContext) : - - @ReactMethod - @Suppress("unused") -- fun startLinkActivityForResult( -+ override fun startLinkActivityForResult( - token: String, - noLoadingState: Boolean, - logLevel: String, -@@ -113,6 +113,10 @@ class PlaidModule internal constructor(reactContext: ReactApplicationContext) : - } - } - -+ override fun addListener(eventName: String?) = Unit -+ -+ override fun removeListeners(count: Double) = Unit -+ - private fun maybeGetStringField(obj: JSONObject, fieldName: String): String? { - if (obj.has(fieldName) && !TextUtils.isEmpty(obj.getString(fieldName))) { - return obj.getString(fieldName) -diff --git a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java -index c59299e..d6b310e 100644 ---- a/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java -+++ b/node_modules/react-native-plaid-link-sdk/android/src/main/java/com/plaid/PlaidPackage.java -@@ -6,19 +6,54 @@ import java.util.List; - import java.util.Map; - - import com.facebook.react.TurboReactPackage; -+import com.facebook.react.ViewManagerOnDemandReactPackage; -+import com.facebook.react.bridge.ModuleSpec; - import com.facebook.react.bridge.NativeModule; - import com.facebook.react.bridge.ReactApplicationContext; -+import com.facebook.react.module.annotations.ReactModule; -+import com.facebook.react.module.annotations.ReactModuleList; - import com.facebook.react.module.model.ReactModuleInfo; - import com.facebook.react.module.model.ReactModuleInfoProvider; -+import com.facebook.react.turbomodule.core.interfaces.TurboModule; - import com.facebook.react.uimanager.ViewManager; - --@SuppressWarnings("unused") --public class PlaidPackage extends TurboReactPackage { -+import javax.annotation.Nonnull; -+import javax.annotation.Nullable; - -+@ReactModuleList(nativeModules = {PlaidModule.class}) -+public class PlaidPackage extends TurboReactPackage implements ViewManagerOnDemandReactPackage { -+ -+ /** -+ * {@inheritDoc} -+ */ -+ @Override -+ public List getViewManagerNames(ReactApplicationContext reactContext) { -+ return null; -+ } -+ -+ @Override -+ protected List getViewManagers(ReactApplicationContext reactContext) { -+ return null; -+ } -+ -+ /** -+ * {@inheritDoc} -+ */ - @Override -- public NativeModule getModule( -- String name, ReactApplicationContext reactContext) { -- return new PlaidModule(reactContext); -+ public @Nullable -+ ViewManager createViewManager( -+ ReactApplicationContext reactContext, String viewManagerName) { -+ return null; -+ } -+ -+ @Override -+ public NativeModule getModule(String name, @Nonnull ReactApplicationContext reactContext) { -+ switch (name) { -+ case PlaidModule.NAME: -+ return new PlaidModule(reactContext); -+ default: -+ return null; -+ } - } - - @Override -@@ -28,19 +63,44 @@ public class PlaidPackage extends TurboReactPackage { - - @Override - public ReactModuleInfoProvider getReactModuleInfoProvider() { -- return () -> { -- Map map = new HashMap<>(); -- map.put( -- "PlaidAndroid", -- new ReactModuleInfo( -- "PlaidAndroid", -- "com.reactlibrary.PlaidModule", -- false, -- false, -- true, -- false, -- false)); -- return map; -- }; -+ try { -+ Class reactModuleInfoProviderClass = -+ Class.forName("com.plaid.PlaidPackage$$ReactModuleInfoProvider"); -+ return (ReactModuleInfoProvider) reactModuleInfoProviderClass.newInstance(); -+ } catch (ClassNotFoundException e) { -+ // ReactModuleSpecProcessor does not run at build-time. Create this ReactModuleInfoProvider by -+ // hand. -+ return new ReactModuleInfoProvider() { -+ @Override -+ public Map getReactModuleInfos() { -+ final Map reactModuleInfoMap = new HashMap<>(); -+ -+ Class[] moduleList = -+ new Class[]{ -+ PlaidModule.class, -+ }; -+ -+ for (Class moduleClass : moduleList) { -+ ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class); -+ -+ reactModuleInfoMap.put( -+ reactModule.name(), -+ new ReactModuleInfo( -+ reactModule.name(), -+ moduleClass.getName(), -+ reactModule.canOverrideExistingModule(), -+ reactModule.needsEagerInit(), -+ reactModule.hasConstants(), -+ reactModule.isCxxModule(), -+ TurboModule.class.isAssignableFrom(moduleClass))); -+ } -+ -+ return reactModuleInfoMap; -+ } -+ }; -+ } catch (InstantiationException | IllegalAccessException e) { -+ throw new RuntimeException( -+ "No ReactModuleInfoProvider for com.plaid.PlaidPackage$$ReactModuleInfoProvider", e); -+ } - } - } -diff --git a/node_modules/react-native-plaid-link-sdk/android/src/paper/java/com/plaid/NativePlaidLinkModuleAndroidSpec.java b/node_modules/react-native-plaid-link-sdk/android/src/paper/java/com/plaid/NativePlaidLinkModuleAndroidSpec.java -new file mode 100644 -index 0000000..fee5a11 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/android/src/paper/java/com/plaid/NativePlaidLinkModuleAndroidSpec.java -@@ -0,0 +1,46 @@ -+ -+/** -+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). -+ * -+ * Do not edit this file as changes may cause incorrect behavior and will be lost -+ * once the code is regenerated. -+ * -+ * @generated by codegen project: GenerateModuleJavaSpec.js -+ * -+ * @nolint -+ */ -+ -+package com.plaid; -+ -+import com.facebook.proguard.annotations.DoNotStrip; -+import com.facebook.react.bridge.Callback; -+import com.facebook.react.bridge.ReactApplicationContext; -+import com.facebook.react.bridge.ReactContextBaseJavaModule; -+import com.facebook.react.bridge.ReactMethod; -+import com.facebook.react.turbomodule.core.interfaces.TurboModule; -+import javax.annotation.Nonnull; -+ -+public abstract class NativePlaidLinkModuleAndroidSpec extends ReactContextBaseJavaModule implements TurboModule { -+ public static final String NAME = "PlaidAndroid"; -+ -+ public NativePlaidLinkModuleAndroidSpec(ReactApplicationContext reactContext) { -+ super(reactContext); -+ } -+ -+ @Override -+ public @Nonnull String getName() { -+ return NAME; -+ } -+ -+ @ReactMethod -+ @DoNotStrip -+ public abstract void startLinkActivityForResult(String token, boolean noLoadingState, String logLevel, Callback onSuccessCallback, Callback onExitCallback); -+ -+ @ReactMethod -+ @DoNotStrip -+ public abstract void addListener(String eventName); -+ -+ @ReactMethod -+ @DoNotStrip -+ public abstract void removeListeners(double count); -+} -diff --git a/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js b/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js -index c7b1e96..c429da7 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js -+++ b/node_modules/react-native-plaid-link-sdk/dist/EmbeddedLink/EmbeddedLinkView.js -@@ -1,55 +1,69 @@ - import React from 'react'; - import NativeEmbeddedLinkView from './NativeEmbeddedLinkView'; - class EmbeddedEvent { -- constructor(event) { -- this.eventName = event.eventName; -- this.metadata = event.metadata; -- } -+ constructor(event) { -+ this.eventName = event.eventName; -+ this.metadata = event.metadata; -+ } - } - class EmbeddedExit { -- constructor(event) { -- this.error = event.error; -- this.metadata = event.metadata; -- } -+ constructor(event) { -+ this.error = event.error; -+ this.metadata = event.metadata; -+ } - } - class EmbeddedSuccess { -- constructor(event) { -- this.publicToken = event.publicToken; -- this.metadata = event.metadata; -- } -+ constructor(event) { -+ this.publicToken = event.publicToken; -+ this.metadata = event.metadata; -+ } - } --export const EmbeddedLinkView = (props) => { -- const { token, iOSPresentationStyle, onEvent, onSuccess, onExit, style } = props; -- const onEmbeddedEvent = (event) => { -- switch (event.nativeEvent.embeddedEventName) { -- case 'onSuccess': { -- if (!onSuccess) { -- return; -- } -- const embeddedSuccess = new EmbeddedSuccess(event.nativeEvent); -- onSuccess(embeddedSuccess); -- break; -- } -- case 'onExit': { -- if (!onExit) { -- return; -- } -- const embeddedExit = new EmbeddedExit(event.nativeEvent); -- onExit(embeddedExit); -- break; -- } -- case 'onEvent': { -- if (!onEvent) { -- return; -- } -- const embeddedEvent = new EmbeddedEvent(event.nativeEvent); -- onEvent(embeddedEvent); -- break; -- } -- default: { -- return; -- } -+export const EmbeddedLinkView = props => { -+ const { -+ token, -+ iOSPresentationStyle, -+ onEvent, -+ onSuccess, -+ onExit, -+ style, -+ } = props; -+ const onEmbeddedEvent = event => { -+ switch (event.nativeEvent.embeddedEventName) { -+ case 'onSuccess': { -+ if (!onSuccess) { -+ return; - } -- }; -- return ; -+ const embeddedSuccess = new EmbeddedSuccess(event.nativeEvent); -+ onSuccess(embeddedSuccess); -+ break; -+ } -+ case 'onExit': { -+ if (!onExit) { -+ return; -+ } -+ const embeddedExit = new EmbeddedExit(event.nativeEvent); -+ onExit(embeddedExit); -+ break; -+ } -+ case 'onEvent': { -+ if (!onEvent) { -+ return; -+ } -+ const embeddedEvent = new EmbeddedEvent(event.nativeEvent); -+ onEvent(embeddedEvent); -+ break; -+ } -+ default: { -+ return; -+ } -+ } -+ }; -+ return ( -+ -+ ); - }; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts -index a48b319..43205dd 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts -+++ b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.d.ts -@@ -4,9 +4,9 @@ import { LinkEventListener, PlaidLinkComponentProps, PlaidLinkProps } from './Ty - * A hook that registers a listener on the Plaid emitter for the 'onEvent' type. - * The listener is cleaned up when this view is unmounted - * -- * @param LinkEventListener the listener to call -+ * @param linkEventListener the listener to call - */ --export declare const usePlaidEmitter: (LinkEventListener: LinkEventListener) => void; -+export declare const usePlaidEmitter: (linkEventListener: LinkEventListener) => void; - export declare const openLink: (props: PlaidLinkProps) => Promise; - export declare const dismissLink: () => void; - export declare const PlaidLink: (props: PlaidLinkComponentProps) => React.JSX.Element; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js -index 21da2bc..6c43633 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js -+++ b/node_modules/react-native-plaid-link-sdk/dist/PlaidLink.js -@@ -1,83 +1,146 @@ --var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { -- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } -- return new (P || (P = Promise))(function (resolve, reject) { -- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } -- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } -- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } -- step((generator = generator.apply(thisArg, _arguments || [])).next()); -+var __awaiter = -+ (this && this.__awaiter) || -+ function(thisArg, _arguments, P, generator) { -+ function adopt(value) { -+ return value instanceof P -+ ? value -+ : new P(function(resolve) { -+ resolve(value); -+ }); -+ } -+ return new (P || (P = Promise))(function(resolve, reject) { -+ function fulfilled(value) { -+ try { -+ step(generator.next(value)); -+ } catch (e) { -+ reject(e); -+ } -+ } -+ function rejected(value) { -+ try { -+ step(generator['throw'](value)); -+ } catch (e) { -+ reject(e); -+ } -+ } -+ function step(result) { -+ result.done -+ ? resolve(result.value) -+ : adopt(result.value).then(fulfilled, rejected); -+ } -+ step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); --}; -+ }; -+var _a; - import React, { useEffect } from 'react'; --import { NativeEventEmitter, NativeModules, Platform, TouchableOpacity, } from 'react-native'; --import { LinkIOSPresentationStyle, LinkLogLevel, } from './Types'; -+import { NativeEventEmitter, Platform, TouchableOpacity } from 'react-native'; -+import { LinkIOSPresentationStyle, LinkLogLevel } from './Types'; -+import RNLinksdkAndroid from './fabric/NativePlaidLinkModuleAndroid'; -+import RNLinksdkiOS from './fabric/NativePlaidLinkModuleiOS'; -+const RNLinksdk = -+ (_a = Platform.OS === 'android' ? RNLinksdkAndroid : RNLinksdkiOS) !== null && -+ _a !== void 0 -+ ? _a -+ : undefined; - /** - * A hook that registers a listener on the Plaid emitter for the 'onEvent' type. - * The listener is cleaned up when this view is unmounted - * -- * @param LinkEventListener the listener to call -+ * @param linkEventListener the listener to call - */ --export const usePlaidEmitter = (LinkEventListener) => { -- useEffect(() => { -- const emitter = new NativeEventEmitter(Platform.OS === 'ios' -- ? NativeModules.RNLinksdk -- : NativeModules.PlaidAndroid); -- const listener = emitter.addListener('onEvent', LinkEventListener); -- // Clean up after this effect: -- return function cleanup() { -- listener.remove(); -- }; -- }, []); -+export const usePlaidEmitter = linkEventListener => { -+ useEffect(() => { -+ const emitter = new NativeEventEmitter(RNLinksdk); -+ const listener = emitter.addListener('onEvent', linkEventListener); -+ // Clean up after this effect: -+ return function cleanup() { -+ listener.remove(); -+ }; -+ }, []); - }; --export const openLink = (props) => __awaiter(void 0, void 0, void 0, function* () { -- var _a, _b; -+export const openLink = props => -+ __awaiter(void 0, void 0, void 0, function*() { -+ var _b, _c; - let config = props.tokenConfig; -- let noLoadingState = (_a = config.noLoadingState) !== null && _a !== void 0 ? _a : false; -+ let noLoadingState = -+ (_b = config.noLoadingState) !== null && _b !== void 0 ? _b : false; - if (Platform.OS === 'android') { -- NativeModules.PlaidAndroid.startLinkActivityForResult(config.token, noLoadingState, (_b = config.logLevel) !== null && _b !== void 0 ? _b : LinkLogLevel.ERROR, (result) => { -- if (props.onSuccess != null) { -- props.onSuccess(result); -- } -- }, (result) => { -- if (props.onExit != null) { -- if (result.error != null && result.error.displayMessage != null) { -- //TODO(RNSDK-118): Remove errorDisplayMessage field in next major update. -- result.error.errorDisplayMessage = result.error.displayMessage; -- } -- props.onExit(result); -+ if (RNLinksdkAndroid === null) { -+ throw new Error( -+ '[react-native-plaid-link-sdk] RNLinksdkAndroid is not defined', -+ ); -+ } -+ RNLinksdkAndroid.startLinkActivityForResult( -+ config.token, -+ noLoadingState, -+ (_c = config.logLevel) !== null && _c !== void 0 -+ ? _c -+ : LinkLogLevel.ERROR, -+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap -+ result => { -+ if (props.onSuccess != null) { -+ props.onSuccess(result); -+ } -+ }, -+ result => { -+ if (props.onExit != null) { -+ if (result.error != null && result.error.displayMessage != null) { -+ //TODO(RNSDK-118): Remove errorDisplayMessage field in next major update. -+ result.error.errorDisplayMessage = result.error.displayMessage; - } -- }); -- } -- else { -- NativeModules.RNLinksdk.create(config.token, noLoadingState); -- let presentFullScreen = props.iOSPresentationStyle == LinkIOSPresentationStyle.FULL_SCREEN; -- NativeModules.RNLinksdk.open(presentFullScreen, (result) => { -- if (props.onSuccess != null) { -- props.onSuccess(result); -- } -- }, (error, result) => { -- if (props.onExit != null) { -- if (error) { -- var data = result || {}; -- data.error = error; -- props.onExit(data); -- } -- else { -- props.onExit(result); -- } -+ props.onExit(result); -+ } -+ }, -+ ); -+ } else { -+ if (RNLinksdkiOS === null) { -+ throw new Error( -+ '[react-native-plaid-link-sdk] RNLinksdkiOS is not defined', -+ ); -+ } -+ RNLinksdkiOS.create(config.token, noLoadingState); -+ let presentFullScreen = -+ props.iOSPresentationStyle == LinkIOSPresentationStyle.FULL_SCREEN; -+ RNLinksdkiOS.open( -+ presentFullScreen, -+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap -+ result => { -+ if (props.onSuccess != null) { -+ props.onSuccess(result); -+ } -+ }, -+ (error, result) => { -+ if (props.onExit != null) { -+ if (error) { -+ var data = result || {}; -+ data.error = error; -+ props.onExit(data); -+ } else { -+ props.onExit(result); - } -- }); -+ } -+ }, -+ ); - } --}); -+ }); - export const dismissLink = () => { -- if (Platform.OS === 'ios') { -- NativeModules.RNLinksdk.dismiss(); -+ if (Platform.OS === 'ios') { -+ if (RNLinksdkiOS === null) { -+ throw new Error( -+ '[react-native-plaid-link-sdk] RNLinksdkiOS is not defined', -+ ); - } -+ RNLinksdkiOS.dismiss(); -+ } - }; --export const PlaidLink = (props) => { -- function onPress() { -- var _a; -- (_a = props.onPress) === null || _a === void 0 ? void 0 : _a.call(props); -- openLink(props); -- } -- return {props.children}; -+export const PlaidLink = props => { -+ function onPress() { -+ var _a; -+ (_a = props.onPress) === null || _a === void 0 ? void 0 : _a.call(props); -+ openLink(props); -+ } -+ return ( -+ // @ts-ignore some types directories misconfiguration -+ {props.children} -+ ); - }; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/Types.js b/node_modules/react-native-plaid-link-sdk/dist/Types.js -index 184adad..11b34e3 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/Types.js -+++ b/node_modules/react-native-plaid-link-sdk/dist/Types.js -@@ -1,430 +1,681 @@ - export var LinkLogLevel; --(function (LinkLogLevel) { -- LinkLogLevel["DEBUG"] = "debug"; -- LinkLogLevel["INFO"] = "info"; -- LinkLogLevel["WARN"] = "warn"; -- LinkLogLevel["ERROR"] = "error"; -+(function(LinkLogLevel) { -+ LinkLogLevel['DEBUG'] = 'debug'; -+ LinkLogLevel['INFO'] = 'info'; -+ LinkLogLevel['WARN'] = 'warn'; -+ LinkLogLevel['ERROR'] = 'error'; - })(LinkLogLevel || (LinkLogLevel = {})); - export var PlaidEnvironment; --(function (PlaidEnvironment) { -- PlaidEnvironment["PRODUCTION"] = "production"; -- PlaidEnvironment["DEVELOPMENT"] = "development"; -- PlaidEnvironment["SANDBOX"] = "sandbox"; -+(function(PlaidEnvironment) { -+ PlaidEnvironment['PRODUCTION'] = 'production'; -+ PlaidEnvironment['DEVELOPMENT'] = 'development'; -+ PlaidEnvironment['SANDBOX'] = 'sandbox'; - })(PlaidEnvironment || (PlaidEnvironment = {})); - export var PlaidProduct; --(function (PlaidProduct) { -- PlaidProduct["ASSETS"] = "assets"; -- PlaidProduct["AUTH"] = "auth"; -- PlaidProduct["DEPOSIT_SWITCH"] = "deposit_switch"; -- PlaidProduct["IDENTITY"] = "identity"; -- PlaidProduct["INCOME"] = "income"; -- PlaidProduct["INVESTMENTS"] = "investments"; -- PlaidProduct["LIABILITIES"] = "liabilities"; -- PlaidProduct["LIABILITIES_REPORT"] = "liabilities_report"; -- PlaidProduct["PAYMENT_INITIATION"] = "payment_initiation"; -- PlaidProduct["TRANSACTIONS"] = "transactions"; -+(function(PlaidProduct) { -+ PlaidProduct['ASSETS'] = 'assets'; -+ PlaidProduct['AUTH'] = 'auth'; -+ PlaidProduct['DEPOSIT_SWITCH'] = 'deposit_switch'; -+ PlaidProduct['IDENTITY'] = 'identity'; -+ PlaidProduct['INCOME'] = 'income'; -+ PlaidProduct['INVESTMENTS'] = 'investments'; -+ PlaidProduct['LIABILITIES'] = 'liabilities'; -+ PlaidProduct['LIABILITIES_REPORT'] = 'liabilities_report'; -+ PlaidProduct['PAYMENT_INITIATION'] = 'payment_initiation'; -+ PlaidProduct['TRANSACTIONS'] = 'transactions'; - })(PlaidProduct || (PlaidProduct = {})); - export var LinkAccountType; --(function (LinkAccountType) { -- LinkAccountType["CREDIT"] = "credit"; -- LinkAccountType["DEPOSITORY"] = "depository"; -- LinkAccountType["INVESTMENT"] = "investment"; -- LinkAccountType["LOAN"] = "loan"; -- LinkAccountType["OTHER"] = "other"; -+(function(LinkAccountType) { -+ LinkAccountType['CREDIT'] = 'credit'; -+ LinkAccountType['DEPOSITORY'] = 'depository'; -+ LinkAccountType['INVESTMENT'] = 'investment'; -+ LinkAccountType['LOAN'] = 'loan'; -+ LinkAccountType['OTHER'] = 'other'; - })(LinkAccountType || (LinkAccountType = {})); - export var LinkAccountSubtypes; --(function (LinkAccountSubtypes) { -- LinkAccountSubtypes["ALL"] = "all"; -- LinkAccountSubtypes["CREDIT_CARD"] = "credit card"; -- LinkAccountSubtypes["PAYPAL"] = "paypal"; -- LinkAccountSubtypes["AUTO"] = "auto"; -- LinkAccountSubtypes["BUSINESS"] = "business"; -- LinkAccountSubtypes["COMMERCIAL"] = "commercial"; -- LinkAccountSubtypes["CONSTRUCTION"] = "construction"; -- LinkAccountSubtypes["CONSUMER"] = "consumer"; -- LinkAccountSubtypes["HOME_EQUITY"] = "home equity"; -- LinkAccountSubtypes["LINE_OF_CREDIT"] = "line of credit"; -- LinkAccountSubtypes["LOAN"] = "loan"; -- LinkAccountSubtypes["MORTGAGE"] = "mortgage"; -- LinkAccountSubtypes["OVERDRAFT"] = "overdraft"; -- LinkAccountSubtypes["STUDENT"] = "student"; -- LinkAccountSubtypes["CASH_MANAGEMENT"] = "cash management"; -- LinkAccountSubtypes["CD"] = "cd"; -- LinkAccountSubtypes["CHECKING"] = "checking"; -- LinkAccountSubtypes["EBT"] = "ebt"; -- LinkAccountSubtypes["HSA"] = "hsa"; -- LinkAccountSubtypes["MONEY_MARKET"] = "money market"; -- LinkAccountSubtypes["PREPAID"] = "prepaid"; -- LinkAccountSubtypes["SAVINGS"] = "savings"; -- LinkAccountSubtypes["FOUR_0_1_A"] = "401a"; -- LinkAccountSubtypes["FOUR_0_1_K"] = "401k"; -- LinkAccountSubtypes["FOUR_0_3_B"] = "403B"; -- LinkAccountSubtypes["FOUR_5_7_B"] = "457b"; -- LinkAccountSubtypes["FIVE_2_9"] = "529"; -- LinkAccountSubtypes["BROKERAGE"] = "brokerage"; -- LinkAccountSubtypes["CASH_ISA"] = "cash isa"; -- LinkAccountSubtypes["EDUCATION_SAVINGS_ACCOUNT"] = "education savings account"; -- LinkAccountSubtypes["FIXED_ANNUNITY"] = "fixed annuity"; -- LinkAccountSubtypes["GIC"] = "gic"; -- LinkAccountSubtypes["HEALTH_REIMBURSEMENT_ARRANGEMENT"] = "health reimbursement arrangement"; -- LinkAccountSubtypes["IRA"] = "ira"; -- LinkAccountSubtypes["ISA"] = "isa"; -- LinkAccountSubtypes["KEOGH"] = "keogh"; -- LinkAccountSubtypes["LIF"] = "lif"; -- LinkAccountSubtypes["LIRA"] = "lira"; -- LinkAccountSubtypes["LRIF"] = "lrif"; -- LinkAccountSubtypes["LRSP"] = "lrsp"; -- LinkAccountSubtypes["MUTUAL_FUND"] = "mutual fund"; -- LinkAccountSubtypes["NON_TAXABLE_BROKERAGE_ACCOUNT"] = "non-taxable brokerage account"; -- LinkAccountSubtypes["PENSION"] = "pension"; -- LinkAccountSubtypes["PLAN"] = "plan"; -- LinkAccountSubtypes["PRIF"] = "prif"; -- LinkAccountSubtypes["PROFIT_SHARING_PLAN"] = "profit sharing plan"; -- LinkAccountSubtypes["RDSP"] = "rdsp"; -- LinkAccountSubtypes["RESP"] = "resp"; -- LinkAccountSubtypes["RETIREMENT"] = "retirement"; -- LinkAccountSubtypes["RLIF"] = "rlif"; -- LinkAccountSubtypes["ROTH_401K"] = "roth 401k"; -- LinkAccountSubtypes["ROTH"] = "roth"; -- LinkAccountSubtypes["RRIF"] = "rrif"; -- LinkAccountSubtypes["RRSP"] = "rrsp"; -- LinkAccountSubtypes["SARSEP"] = "sarsep"; -- LinkAccountSubtypes["SEP_IRA"] = "sep ira"; -- LinkAccountSubtypes["SIMPLE_IRA"] = "simple ira"; -- LinkAccountSubtypes["SIPP"] = "sipp"; -- LinkAccountSubtypes["STOCK_PLAN"] = "stock plan"; -- LinkAccountSubtypes["TFSA"] = "tfsa"; -- LinkAccountSubtypes["THRIFT_SAVINGS_PLAN"] = "thrift savings plan"; -- LinkAccountSubtypes["TRUST"] = "trust"; -- LinkAccountSubtypes["UGMA"] = "ugma"; -- LinkAccountSubtypes["UTMA"] = "utma"; -- LinkAccountSubtypes["VARIABLE_ANNUITY"] = "variable annuity"; -+(function(LinkAccountSubtypes) { -+ LinkAccountSubtypes['ALL'] = 'all'; -+ LinkAccountSubtypes['CREDIT_CARD'] = 'credit card'; -+ LinkAccountSubtypes['PAYPAL'] = 'paypal'; -+ LinkAccountSubtypes['AUTO'] = 'auto'; -+ LinkAccountSubtypes['BUSINESS'] = 'business'; -+ LinkAccountSubtypes['COMMERCIAL'] = 'commercial'; -+ LinkAccountSubtypes['CONSTRUCTION'] = 'construction'; -+ LinkAccountSubtypes['CONSUMER'] = 'consumer'; -+ LinkAccountSubtypes['HOME_EQUITY'] = 'home equity'; -+ LinkAccountSubtypes['LINE_OF_CREDIT'] = 'line of credit'; -+ LinkAccountSubtypes['LOAN'] = 'loan'; -+ LinkAccountSubtypes['MORTGAGE'] = 'mortgage'; -+ LinkAccountSubtypes['OVERDRAFT'] = 'overdraft'; -+ LinkAccountSubtypes['STUDENT'] = 'student'; -+ LinkAccountSubtypes['CASH_MANAGEMENT'] = 'cash management'; -+ LinkAccountSubtypes['CD'] = 'cd'; -+ LinkAccountSubtypes['CHECKING'] = 'checking'; -+ LinkAccountSubtypes['EBT'] = 'ebt'; -+ LinkAccountSubtypes['HSA'] = 'hsa'; -+ LinkAccountSubtypes['MONEY_MARKET'] = 'money market'; -+ LinkAccountSubtypes['PREPAID'] = 'prepaid'; -+ LinkAccountSubtypes['SAVINGS'] = 'savings'; -+ LinkAccountSubtypes['FOUR_0_1_A'] = '401a'; -+ LinkAccountSubtypes['FOUR_0_1_K'] = '401k'; -+ LinkAccountSubtypes['FOUR_0_3_B'] = '403B'; -+ LinkAccountSubtypes['FOUR_5_7_B'] = '457b'; -+ LinkAccountSubtypes['FIVE_2_9'] = '529'; -+ LinkAccountSubtypes['BROKERAGE'] = 'brokerage'; -+ LinkAccountSubtypes['CASH_ISA'] = 'cash isa'; -+ LinkAccountSubtypes['EDUCATION_SAVINGS_ACCOUNT'] = -+ 'education savings account'; -+ LinkAccountSubtypes['FIXED_ANNUNITY'] = 'fixed annuity'; -+ LinkAccountSubtypes['GIC'] = 'gic'; -+ LinkAccountSubtypes['HEALTH_REIMBURSEMENT_ARRANGEMENT'] = -+ 'health reimbursement arrangement'; -+ LinkAccountSubtypes['IRA'] = 'ira'; -+ LinkAccountSubtypes['ISA'] = 'isa'; -+ LinkAccountSubtypes['KEOGH'] = 'keogh'; -+ LinkAccountSubtypes['LIF'] = 'lif'; -+ LinkAccountSubtypes['LIRA'] = 'lira'; -+ LinkAccountSubtypes['LRIF'] = 'lrif'; -+ LinkAccountSubtypes['LRSP'] = 'lrsp'; -+ LinkAccountSubtypes['MUTUAL_FUND'] = 'mutual fund'; -+ LinkAccountSubtypes['NON_TAXABLE_BROKERAGE_ACCOUNT'] = -+ 'non-taxable brokerage account'; -+ LinkAccountSubtypes['PENSION'] = 'pension'; -+ LinkAccountSubtypes['PLAN'] = 'plan'; -+ LinkAccountSubtypes['PRIF'] = 'prif'; -+ LinkAccountSubtypes['PROFIT_SHARING_PLAN'] = 'profit sharing plan'; -+ LinkAccountSubtypes['RDSP'] = 'rdsp'; -+ LinkAccountSubtypes['RESP'] = 'resp'; -+ LinkAccountSubtypes['RETIREMENT'] = 'retirement'; -+ LinkAccountSubtypes['RLIF'] = 'rlif'; -+ LinkAccountSubtypes['ROTH_401K'] = 'roth 401k'; -+ LinkAccountSubtypes['ROTH'] = 'roth'; -+ LinkAccountSubtypes['RRIF'] = 'rrif'; -+ LinkAccountSubtypes['RRSP'] = 'rrsp'; -+ LinkAccountSubtypes['SARSEP'] = 'sarsep'; -+ LinkAccountSubtypes['SEP_IRA'] = 'sep ira'; -+ LinkAccountSubtypes['SIMPLE_IRA'] = 'simple ira'; -+ LinkAccountSubtypes['SIPP'] = 'sipp'; -+ LinkAccountSubtypes['STOCK_PLAN'] = 'stock plan'; -+ LinkAccountSubtypes['TFSA'] = 'tfsa'; -+ LinkAccountSubtypes['THRIFT_SAVINGS_PLAN'] = 'thrift savings plan'; -+ LinkAccountSubtypes['TRUST'] = 'trust'; -+ LinkAccountSubtypes['UGMA'] = 'ugma'; -+ LinkAccountSubtypes['UTMA'] = 'utma'; -+ LinkAccountSubtypes['VARIABLE_ANNUITY'] = 'variable annuity'; - })(LinkAccountSubtypes || (LinkAccountSubtypes = {})); - export class LinkAccountSubtypeCredit { -- constructor(type, subtype) { -- this.type = type; -- this.subtype = subtype; -- } -+ constructor(type, subtype) { -+ this.type = type; -+ this.subtype = subtype; -+ } - } --LinkAccountSubtypeCredit.ALL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL); --LinkAccountSubtypeCredit.CREDIT_CARD = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.CREDIT_CARD); --LinkAccountSubtypeCredit.PAYPAL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.PAYPAL); -+LinkAccountSubtypeCredit.ALL = new LinkAccountSubtypeCredit( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.ALL, -+); -+LinkAccountSubtypeCredit.CREDIT_CARD = new LinkAccountSubtypeCredit( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.CREDIT_CARD, -+); -+LinkAccountSubtypeCredit.PAYPAL = new LinkAccountSubtypeCredit( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.PAYPAL, -+); - export class LinkAccountSubtypeDepository { -- constructor(type, subtype) { -- this.type = type; -- this.subtype = subtype; -- } -+ constructor(type, subtype) { -+ this.type = type; -+ this.subtype = subtype; -+ } - } --LinkAccountSubtypeDepository.ALL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.ALL); --LinkAccountSubtypeDepository.CASH_MANAGEMENT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CASH_MANAGEMENT); --LinkAccountSubtypeDepository.CD = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CD); --LinkAccountSubtypeDepository.CHECKING = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CHECKING); --LinkAccountSubtypeDepository.EBT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.EBT); --LinkAccountSubtypeDepository.HSA = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.HSA); --LinkAccountSubtypeDepository.MONEY_MARKET = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.MONEY_MARKET); --LinkAccountSubtypeDepository.PAYPAL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PAYPAL); --LinkAccountSubtypeDepository.PREPAID = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PREPAID); --LinkAccountSubtypeDepository.SAVINGS = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.SAVINGS); -+LinkAccountSubtypeDepository.ALL = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.ALL, -+); -+LinkAccountSubtypeDepository.CASH_MANAGEMENT = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.CASH_MANAGEMENT, -+); -+LinkAccountSubtypeDepository.CD = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.CD, -+); -+LinkAccountSubtypeDepository.CHECKING = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.CHECKING, -+); -+LinkAccountSubtypeDepository.EBT = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.EBT, -+); -+LinkAccountSubtypeDepository.HSA = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.HSA, -+); -+LinkAccountSubtypeDepository.MONEY_MARKET = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.MONEY_MARKET, -+); -+LinkAccountSubtypeDepository.PAYPAL = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.PAYPAL, -+); -+LinkAccountSubtypeDepository.PREPAID = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.PREPAID, -+); -+LinkAccountSubtypeDepository.SAVINGS = new LinkAccountSubtypeDepository( -+ LinkAccountType.DEPOSITORY, -+ LinkAccountSubtypes.SAVINGS, -+); - export class LinkAccountSubtypeInvestment { -- constructor(type, subtype) { -- this.type = type; -- this.subtype = subtype; -- } -+ constructor(type, subtype) { -+ this.type = type; -+ this.subtype = subtype; -+ } - } --LinkAccountSubtypeInvestment.ALL = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ALL); --LinkAccountSubtypeInvestment.BROKERAGE = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.BROKERAGE); --LinkAccountSubtypeInvestment.CASH_ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.CASH_ISA); --LinkAccountSubtypeInvestment.EDUCATION_SAVINGS_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.EDUCATION_SAVINGS_ACCOUNT); --LinkAccountSubtypeInvestment.FIXED_ANNUNITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIXED_ANNUNITY); --LinkAccountSubtypeInvestment.GIC = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.GIC); --LinkAccountSubtypeInvestment.HEALTH_REIMBURSEMENT_ARRANGEMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HEALTH_REIMBURSEMENT_ARRANGEMENT); --LinkAccountSubtypeInvestment.HSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HSA); --LinkAccountSubtypeInvestment.INVESTMENT_401A = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_A); --LinkAccountSubtypeInvestment.INVESTMENT_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_K); --LinkAccountSubtypeInvestment.INVESTMENT_403B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_3_B); --LinkAccountSubtypeInvestment.INVESTMENT_457B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_5_7_B); --LinkAccountSubtypeInvestment.INVESTMENT_529 = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIVE_2_9); --LinkAccountSubtypeInvestment.IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.IRA); --LinkAccountSubtypeInvestment.ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ISA); --LinkAccountSubtypeInvestment.KEOGH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.KEOGH); --LinkAccountSubtypeInvestment.LIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIF); --LinkAccountSubtypeInvestment.LIRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIRA); --LinkAccountSubtypeInvestment.LRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRIF); --LinkAccountSubtypeInvestment.LRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRSP); --LinkAccountSubtypeInvestment.MUTUAL_FUND = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.MUTUAL_FUND); --LinkAccountSubtypeInvestment.NON_TAXABLE_BROKERAGE_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.NON_TAXABLE_BROKERAGE_ACCOUNT); --LinkAccountSubtypeInvestment.PENSION = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PENSION); --LinkAccountSubtypeInvestment.PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PLAN); --LinkAccountSubtypeInvestment.PRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PRIF); --LinkAccountSubtypeInvestment.PROFIT_SHARING_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PROFIT_SHARING_PLAN); --LinkAccountSubtypeInvestment.RDSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RDSP); --LinkAccountSubtypeInvestment.RESP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RESP); --LinkAccountSubtypeInvestment.RETIREMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RETIREMENT); --LinkAccountSubtypeInvestment.RLIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RLIF); --LinkAccountSubtypeInvestment.ROTH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH); --LinkAccountSubtypeInvestment.ROTH_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH_401K); --LinkAccountSubtypeInvestment.RRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRIF); --LinkAccountSubtypeInvestment.RRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRSP); --LinkAccountSubtypeInvestment.SARSEP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SARSEP); --LinkAccountSubtypeInvestment.SEP_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SEP_IRA); --LinkAccountSubtypeInvestment.SIMPLE_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIMPLE_IRA); --LinkAccountSubtypeInvestment.SIIP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIPP); --LinkAccountSubtypeInvestment.STOCK_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.STOCK_PLAN); --LinkAccountSubtypeInvestment.TFSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TFSA); --LinkAccountSubtypeInvestment.THRIFT_SAVINGS_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.THRIFT_SAVINGS_PLAN); --LinkAccountSubtypeInvestment.TRUST = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TRUST); --LinkAccountSubtypeInvestment.UGMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UGMA); --LinkAccountSubtypeInvestment.UTMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UTMA); --LinkAccountSubtypeInvestment.VARIABLE_ANNUITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.VARIABLE_ANNUITY); -+LinkAccountSubtypeInvestment.ALL = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.ALL, -+); -+LinkAccountSubtypeInvestment.BROKERAGE = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.BROKERAGE, -+); -+LinkAccountSubtypeInvestment.CASH_ISA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.CASH_ISA, -+); -+LinkAccountSubtypeInvestment.EDUCATION_SAVINGS_ACCOUNT = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.EDUCATION_SAVINGS_ACCOUNT, -+); -+LinkAccountSubtypeInvestment.FIXED_ANNUNITY = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.FIXED_ANNUNITY, -+); -+LinkAccountSubtypeInvestment.GIC = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.GIC, -+); -+LinkAccountSubtypeInvestment.HEALTH_REIMBURSEMENT_ARRANGEMENT = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.HEALTH_REIMBURSEMENT_ARRANGEMENT, -+); -+LinkAccountSubtypeInvestment.HSA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.HSA, -+); -+LinkAccountSubtypeInvestment.INVESTMENT_401A = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.FOUR_0_1_A, -+); -+LinkAccountSubtypeInvestment.INVESTMENT_401K = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.FOUR_0_1_K, -+); -+LinkAccountSubtypeInvestment.INVESTMENT_403B = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.FOUR_0_3_B, -+); -+LinkAccountSubtypeInvestment.INVESTMENT_457B = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.FOUR_5_7_B, -+); -+LinkAccountSubtypeInvestment.INVESTMENT_529 = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.FIVE_2_9, -+); -+LinkAccountSubtypeInvestment.IRA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.IRA, -+); -+LinkAccountSubtypeInvestment.ISA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.ISA, -+); -+LinkAccountSubtypeInvestment.KEOGH = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.KEOGH, -+); -+LinkAccountSubtypeInvestment.LIF = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.LIF, -+); -+LinkAccountSubtypeInvestment.LIRA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.LIRA, -+); -+LinkAccountSubtypeInvestment.LRIF = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.LRIF, -+); -+LinkAccountSubtypeInvestment.LRSP = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.LRSP, -+); -+LinkAccountSubtypeInvestment.MUTUAL_FUND = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.MUTUAL_FUND, -+); -+LinkAccountSubtypeInvestment.NON_TAXABLE_BROKERAGE_ACCOUNT = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.NON_TAXABLE_BROKERAGE_ACCOUNT, -+); -+LinkAccountSubtypeInvestment.PENSION = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.PENSION, -+); -+LinkAccountSubtypeInvestment.PLAN = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.PLAN, -+); -+LinkAccountSubtypeInvestment.PRIF = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.PRIF, -+); -+LinkAccountSubtypeInvestment.PROFIT_SHARING_PLAN = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.PROFIT_SHARING_PLAN, -+); -+LinkAccountSubtypeInvestment.RDSP = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.RDSP, -+); -+LinkAccountSubtypeInvestment.RESP = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.RESP, -+); -+LinkAccountSubtypeInvestment.RETIREMENT = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.RETIREMENT, -+); -+LinkAccountSubtypeInvestment.RLIF = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.RLIF, -+); -+LinkAccountSubtypeInvestment.ROTH = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.ROTH, -+); -+LinkAccountSubtypeInvestment.ROTH_401K = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.ROTH_401K, -+); -+LinkAccountSubtypeInvestment.RRIF = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.RRIF, -+); -+LinkAccountSubtypeInvestment.RRSP = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.RRSP, -+); -+LinkAccountSubtypeInvestment.SARSEP = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.SARSEP, -+); -+LinkAccountSubtypeInvestment.SEP_IRA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.SEP_IRA, -+); -+LinkAccountSubtypeInvestment.SIMPLE_IRA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.SIMPLE_IRA, -+); -+LinkAccountSubtypeInvestment.SIIP = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.SIPP, -+); -+LinkAccountSubtypeInvestment.STOCK_PLAN = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.STOCK_PLAN, -+); -+LinkAccountSubtypeInvestment.TFSA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.TFSA, -+); -+LinkAccountSubtypeInvestment.THRIFT_SAVINGS_PLAN = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.THRIFT_SAVINGS_PLAN, -+); -+LinkAccountSubtypeInvestment.TRUST = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.TRUST, -+); -+LinkAccountSubtypeInvestment.UGMA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.UGMA, -+); -+LinkAccountSubtypeInvestment.UTMA = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.UTMA, -+); -+LinkAccountSubtypeInvestment.VARIABLE_ANNUITY = new LinkAccountSubtypeInvestment( -+ LinkAccountType.INVESTMENT, -+ LinkAccountSubtypes.VARIABLE_ANNUITY, -+); - export class LinkAccountSubtypeLoan { -- constructor(type, subtype) { -- this.type = type; -- this.subtype = subtype; -- } -+ constructor(type, subtype) { -+ this.type = type; -+ this.subtype = subtype; -+ } - } --LinkAccountSubtypeLoan.ALL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL); --LinkAccountSubtypeLoan.AUTO = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.AUTO); --LinkAccountSubtypeLoan.BUSINESS = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.BUSINESS); --LinkAccountSubtypeLoan.COMMERCIAL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.COMMERCIAL); --LinkAccountSubtypeLoan.CONSTRUCTION = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSTRUCTION); --LinkAccountSubtypeLoan.CONSUMER = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSUMER); --LinkAccountSubtypeLoan.HOME_EQUITY = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.HOME_EQUITY); --LinkAccountSubtypeLoan.LINE_OF_CREDIT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LINE_OF_CREDIT); --LinkAccountSubtypeLoan.LOAN = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LOAN); --LinkAccountSubtypeLoan.MORTGAGE = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.MORTGAGE); --LinkAccountSubtypeLoan.OVERDRAFT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.OVERDRAFT); --LinkAccountSubtypeLoan.STUDENT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.STUDENT); -+LinkAccountSubtypeLoan.ALL = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.ALL, -+); -+LinkAccountSubtypeLoan.AUTO = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.AUTO, -+); -+LinkAccountSubtypeLoan.BUSINESS = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.BUSINESS, -+); -+LinkAccountSubtypeLoan.COMMERCIAL = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.COMMERCIAL, -+); -+LinkAccountSubtypeLoan.CONSTRUCTION = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.CONSTRUCTION, -+); -+LinkAccountSubtypeLoan.CONSUMER = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.CONSUMER, -+); -+LinkAccountSubtypeLoan.HOME_EQUITY = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.HOME_EQUITY, -+); -+LinkAccountSubtypeLoan.LINE_OF_CREDIT = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.LINE_OF_CREDIT, -+); -+LinkAccountSubtypeLoan.LOAN = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.LOAN, -+); -+LinkAccountSubtypeLoan.MORTGAGE = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.MORTGAGE, -+); -+LinkAccountSubtypeLoan.OVERDRAFT = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.OVERDRAFT, -+); -+LinkAccountSubtypeLoan.STUDENT = new LinkAccountSubtypeLoan( -+ LinkAccountType.CREDIT, -+ LinkAccountSubtypes.STUDENT, -+); - export class LinkAccountSubtypeUnknown { -- constructor(type, subtype) { -- this.type = type; -- this.subtype = subtype; -- } -+ constructor(type, subtype) { -+ this.type = type; -+ this.subtype = subtype; -+ } - } - export var LinkAccountVerificationStatus; --(function (LinkAccountVerificationStatus) { -- LinkAccountVerificationStatus["PENDING_AUTOMATIC_VERIFICATION"] = "pending_automatic_verification"; -- LinkAccountVerificationStatus["PENDING_MANUAL_VERIFICATION"] = "pending_manual_verification"; -- LinkAccountVerificationStatus["MANUALLY_VERIFIED"] = "manually_verified"; -+(function(LinkAccountVerificationStatus) { -+ LinkAccountVerificationStatus['PENDING_AUTOMATIC_VERIFICATION'] = -+ 'pending_automatic_verification'; -+ LinkAccountVerificationStatus['PENDING_MANUAL_VERIFICATION'] = -+ 'pending_manual_verification'; -+ LinkAccountVerificationStatus['MANUALLY_VERIFIED'] = 'manually_verified'; - })(LinkAccountVerificationStatus || (LinkAccountVerificationStatus = {})); - export var LinkExitMetadataStatus; --(function (LinkExitMetadataStatus) { -- LinkExitMetadataStatus["CONNECTED"] = "connected"; -- LinkExitMetadataStatus["CHOOSE_DEVICE"] = "choose_device"; -- LinkExitMetadataStatus["REQUIRES_ACCOUNT_SELECTION"] = "requires_account_selection"; -- LinkExitMetadataStatus["REQUIRES_CODE"] = "requires_code"; -- LinkExitMetadataStatus["REQUIRES_CREDENTIALS"] = "requires_credentials"; -- LinkExitMetadataStatus["REQUIRES_EXTERNAL_ACTION"] = "requires_external_action"; -- LinkExitMetadataStatus["REQUIRES_OAUTH"] = "requires_oauth"; -- LinkExitMetadataStatus["REQUIRES_QUESTIONS"] = "requires_questions"; -- LinkExitMetadataStatus["REQUIRES_RECAPTCHA"] = "requires_recaptcha"; -- LinkExitMetadataStatus["REQUIRES_SELECTIONS"] = "requires_selections"; -- LinkExitMetadataStatus["REQUIRES_DEPOSIT_SWITCH_ALLOCATION_CONFIGURATION"] = "requires_deposit_switch_allocation_configuration"; -- LinkExitMetadataStatus["REQUIRES_DEPOSIT_SWITCH_ALLOCATION_SELECTION"] = "requires_deposit_switch_allocation_selection"; -+(function(LinkExitMetadataStatus) { -+ LinkExitMetadataStatus['CONNECTED'] = 'connected'; -+ LinkExitMetadataStatus['CHOOSE_DEVICE'] = 'choose_device'; -+ LinkExitMetadataStatus['REQUIRES_ACCOUNT_SELECTION'] = -+ 'requires_account_selection'; -+ LinkExitMetadataStatus['REQUIRES_CODE'] = 'requires_code'; -+ LinkExitMetadataStatus['REQUIRES_CREDENTIALS'] = 'requires_credentials'; -+ LinkExitMetadataStatus['REQUIRES_EXTERNAL_ACTION'] = -+ 'requires_external_action'; -+ LinkExitMetadataStatus['REQUIRES_OAUTH'] = 'requires_oauth'; -+ LinkExitMetadataStatus['REQUIRES_QUESTIONS'] = 'requires_questions'; -+ LinkExitMetadataStatus['REQUIRES_RECAPTCHA'] = 'requires_recaptcha'; -+ LinkExitMetadataStatus['REQUIRES_SELECTIONS'] = 'requires_selections'; -+ LinkExitMetadataStatus['REQUIRES_DEPOSIT_SWITCH_ALLOCATION_CONFIGURATION'] = -+ 'requires_deposit_switch_allocation_configuration'; -+ LinkExitMetadataStatus['REQUIRES_DEPOSIT_SWITCH_ALLOCATION_SELECTION'] = -+ 'requires_deposit_switch_allocation_selection'; - })(LinkExitMetadataStatus || (LinkExitMetadataStatus = {})); - export var LinkErrorCode; --(function (LinkErrorCode) { -- // ITEM_ERROR -- LinkErrorCode["INVALID_CREDENTIALS"] = "INVALID_CREDENTIALS"; -- LinkErrorCode["INVALID_MFA"] = "INVALID_MFA"; -- LinkErrorCode["ITEM_LOGIN_REQUIRED"] = "ITEM_LOGIN_REQUIRED"; -- LinkErrorCode["INSUFFICIENT_CREDENTIALS"] = "INSUFFICIENT_CREDENTIALS"; -- LinkErrorCode["ITEM_LOCKED"] = "ITEM_LOCKED"; -- LinkErrorCode["USER_SETUP_REQUIRED"] = "USER_SETUP_REQUIRED"; -- LinkErrorCode["MFA_NOT_SUPPORTED"] = "MFA_NOT_SUPPORTED"; -- LinkErrorCode["INVALID_SEND_METHOD"] = "INVALID_SEND_METHOD"; -- LinkErrorCode["NO_ACCOUNTS"] = "NO_ACCOUNTS"; -- LinkErrorCode["ITEM_NOT_SUPPORTED"] = "ITEM_NOT_SUPPORTED"; -- LinkErrorCode["TOO_MANY_VERIFICATION_ATTEMPTS"] = "TOO_MANY_VERIFICATION_ATTEMPTS"; -- LinkErrorCode["INVALD_UPDATED_USERNAME"] = "INVALD_UPDATED_USERNAME"; -- LinkErrorCode["INVALID_UPDATED_USERNAME"] = "INVALID_UPDATED_USERNAME"; -- LinkErrorCode["ITEM_NO_ERROR"] = "ITEM_NO_ERROR"; -- LinkErrorCode["item_no_error"] = "item-no-error"; -- LinkErrorCode["NO_AUTH_ACCOUNTS"] = "NO_AUTH_ACCOUNTS"; -- LinkErrorCode["NO_INVESTMENT_ACCOUNTS"] = "NO_INVESTMENT_ACCOUNTS"; -- LinkErrorCode["NO_INVESTMENT_AUTH_ACCOUNTS"] = "NO_INVESTMENT_AUTH_ACCOUNTS"; -- LinkErrorCode["NO_LIABILITY_ACCOUNTS"] = "NO_LIABILITY_ACCOUNTS"; -- LinkErrorCode["PRODUCTS_NOT_SUPPORTED"] = "PRODUCTS_NOT_SUPPORTED"; -- LinkErrorCode["ITEM_NOT_FOUND"] = "ITEM_NOT_FOUND"; -- LinkErrorCode["ITEM_PRODUCT_NOT_READY"] = "ITEM_PRODUCT_NOT_READY"; -- // INSTITUTION_ERROR -- LinkErrorCode["INSTITUTION_DOWN"] = "INSTITUTION_DOWN"; -- LinkErrorCode["INSTITUTION_NOT_RESPONDING"] = "INSTITUTION_NOT_RESPONDING"; -- LinkErrorCode["INSTITUTION_NOT_AVAILABLE"] = "INSTITUTION_NOT_AVAILABLE"; -- LinkErrorCode["INSTITUTION_NO_LONGER_SUPPORTED"] = "INSTITUTION_NO_LONGER_SUPPORTED"; -- // API_ERROR -- LinkErrorCode["INTERNAL_SERVER_ERROR"] = "INTERNAL_SERVER_ERROR"; -- LinkErrorCode["PLANNED_MAINTENANCE"] = "PLANNED_MAINTENANCE"; -- // ASSET_REPORT_ERROR -- LinkErrorCode["PRODUCT_NOT_ENABLED"] = "PRODUCT_NOT_ENABLED"; -- LinkErrorCode["DATA_UNAVAILABLE"] = "DATA_UNAVAILABLE"; -- LinkErrorCode["ASSET_PRODUCT_NOT_READY"] = "ASSET_PRODUCT_NOT_READY"; -- LinkErrorCode["ASSET_REPORT_GENERATION_FAILED"] = "ASSET_REPORT_GENERATION_FAILED"; -- LinkErrorCode["INVALID_PARENT"] = "INVALID_PARENT"; -- LinkErrorCode["INSIGHTS_NOT_ENABLED"] = "INSIGHTS_NOT_ENABLED"; -- LinkErrorCode["INSIGHTS_PREVIOUSLY_NOT_ENABLED"] = "INSIGHTS_PREVIOUSLY_NOT_ENABLED"; -- // BANK_TRANSFER_ERROR -- LinkErrorCode["BANK_TRANSFER_LIMIT_EXCEEDED"] = "BANK_TRANSFER_LIMIT_EXCEEDED"; -- LinkErrorCode["BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT"] = "BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT"; -- LinkErrorCode["BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT"] = "BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT"; -- LinkErrorCode["BANK_TRANSFER_ACCOUNT_BLOCKED"] = "BANK_TRANSFER_ACCOUNT_BLOCKED"; -- LinkErrorCode["BANK_TRANSFER_INSUFFICIENT_FUNDS"] = "BANK_TRANSFER_INSUFFICIENT_FUNDS"; -- LinkErrorCode["BANK_TRANSFER_NOT_CANCELLABLE"] = "BANK_TRANSFER_NOT_CANCELLABLE"; -- LinkErrorCode["BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE"] = "BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE"; -- LinkErrorCode["BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT"] = "BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT"; -- // SANDBOX_ERROR -- LinkErrorCode["SANDBOX_PRODUCT_NOT_ENABLED"] = "SANDBOX_PRODUCT_NOT_ENABLED"; -- LinkErrorCode["SANDBOX_WEBHOOK_INVALID"] = "SANDBOX_WEBHOOK_INVALID"; -- LinkErrorCode["SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID"] = "SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID"; -- // INVALID_REQUEST -- LinkErrorCode["MISSING_FIELDS"] = "MISSING_FIELDS"; -- LinkErrorCode["UNKNOWN_FIELDS"] = "UNKNOWN_FIELDS"; -- LinkErrorCode["INVALID_FIELD"] = "INVALID_FIELD"; -- LinkErrorCode["INCOMPATIBLE_API_VERSION"] = "INCOMPATIBLE_API_VERSION"; -- LinkErrorCode["INVALID_BODY"] = "INVALID_BODY"; -- LinkErrorCode["INVALID_HEADERS"] = "INVALID_HEADERS"; -- LinkErrorCode["NOT_FOUND"] = "NOT_FOUND"; -- LinkErrorCode["NO_LONGER_AVAILABLE"] = "NO_LONGER_AVAILABLE"; -- LinkErrorCode["SANDBOX_ONLY"] = "SANDBOX_ONLY"; -- LinkErrorCode["INVALID_ACCOUNT_NUMBER"] = "INVALID_ACCOUNT_NUMBER"; -- // INVALID_INPUT -- // From above ITEM_LOGIN_REQUIRED = "INVALID_CREDENTIALS", -- LinkErrorCode["INCORRECT_DEPOSIT_AMOUNTS"] = "INCORRECT_DEPOSIT_AMOUNTS"; -- LinkErrorCode["UNAUTHORIZED_ENVIRONMENT"] = "UNAUTHORIZED_ENVIRONMENT"; -- LinkErrorCode["INVALID_PRODUCT"] = "INVALID_PRODUCT"; -- LinkErrorCode["UNAUTHORIZED_ROUTE_ACCESS"] = "UNAUTHORIZED_ROUTE_ACCESS"; -- LinkErrorCode["DIRECT_INTEGRATION_NOT_ENABLED"] = "DIRECT_INTEGRATION_NOT_ENABLED"; -- LinkErrorCode["INVALID_API_KEYS"] = "INVALID_API_KEYS"; -- LinkErrorCode["INVALID_ACCESS_TOKEN"] = "INVALID_ACCESS_TOKEN"; -- LinkErrorCode["INVALID_PUBLIC_TOKEN"] = "INVALID_PUBLIC_TOKEN"; -- LinkErrorCode["INVALID_LINK_TOKEN"] = "INVALID_LINK_TOKEN"; -- LinkErrorCode["INVALID_PROCESSOR_TOKEN"] = "INVALID_PROCESSOR_TOKEN"; -- LinkErrorCode["INVALID_AUDIT_COPY_TOKEN"] = "INVALID_AUDIT_COPY_TOKEN"; -- LinkErrorCode["INVALID_ACCOUNT_ID"] = "INVALID_ACCOUNT_ID"; -- LinkErrorCode["MICRODEPOSITS_ALREADY_VERIFIED"] = "MICRODEPOSITS_ALREADY_VERIFIED"; -- // INVALID_RESULT -- LinkErrorCode["PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA"] = "PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA"; -- // RATE_LIMIT_EXCEEDED -- LinkErrorCode["ACCOUNTS_LIMIT"] = "ACCOUNTS_LIMIT"; -- LinkErrorCode["ADDITION_LIMIT"] = "ADDITION_LIMIT"; -- LinkErrorCode["AUTH_LIMIT"] = "AUTH_LIMIT"; -- LinkErrorCode["BALANCE_LIMIT"] = "BALANCE_LIMIT"; -- LinkErrorCode["IDENTITY_LIMIT"] = "IDENTITY_LIMIT"; -- LinkErrorCode["ITEM_GET_LIMIT"] = "ITEM_GET_LIMIT"; -- LinkErrorCode["RATE_LIMIT"] = "RATE_LIMIT"; -- LinkErrorCode["TRANSACTIONS_LIMIT"] = "TRANSACTIONS_LIMIT"; -- // RECAPTCHA_ERROR -- LinkErrorCode["RECAPTCHA_REQUIRED"] = "RECAPTCHA_REQUIRED"; -- LinkErrorCode["RECAPTCHA_BAD"] = "RECAPTCHA_BAD"; -- // OAUTH_ERROR -- LinkErrorCode["INCORRECT_OAUTH_NONCE"] = "INCORRECT_OAUTH_NONCE"; -- LinkErrorCode["OAUTH_STATE_ID_ALREADY_PROCESSED"] = "OAUTH_STATE_ID_ALREADY_PROCESSED"; -+(function(LinkErrorCode) { -+ // ITEM_ERROR -+ LinkErrorCode['INVALID_CREDENTIALS'] = 'INVALID_CREDENTIALS'; -+ LinkErrorCode['INVALID_MFA'] = 'INVALID_MFA'; -+ LinkErrorCode['ITEM_LOGIN_REQUIRED'] = 'ITEM_LOGIN_REQUIRED'; -+ LinkErrorCode['INSUFFICIENT_CREDENTIALS'] = 'INSUFFICIENT_CREDENTIALS'; -+ LinkErrorCode['ITEM_LOCKED'] = 'ITEM_LOCKED'; -+ LinkErrorCode['USER_SETUP_REQUIRED'] = 'USER_SETUP_REQUIRED'; -+ LinkErrorCode['MFA_NOT_SUPPORTED'] = 'MFA_NOT_SUPPORTED'; -+ LinkErrorCode['INVALID_SEND_METHOD'] = 'INVALID_SEND_METHOD'; -+ LinkErrorCode['NO_ACCOUNTS'] = 'NO_ACCOUNTS'; -+ LinkErrorCode['ITEM_NOT_SUPPORTED'] = 'ITEM_NOT_SUPPORTED'; -+ LinkErrorCode['TOO_MANY_VERIFICATION_ATTEMPTS'] = -+ 'TOO_MANY_VERIFICATION_ATTEMPTS'; -+ LinkErrorCode['INVALD_UPDATED_USERNAME'] = 'INVALD_UPDATED_USERNAME'; -+ LinkErrorCode['INVALID_UPDATED_USERNAME'] = 'INVALID_UPDATED_USERNAME'; -+ LinkErrorCode['ITEM_NO_ERROR'] = 'ITEM_NO_ERROR'; -+ LinkErrorCode['item_no_error'] = 'item-no-error'; -+ LinkErrorCode['NO_AUTH_ACCOUNTS'] = 'NO_AUTH_ACCOUNTS'; -+ LinkErrorCode['NO_INVESTMENT_ACCOUNTS'] = 'NO_INVESTMENT_ACCOUNTS'; -+ LinkErrorCode['NO_INVESTMENT_AUTH_ACCOUNTS'] = 'NO_INVESTMENT_AUTH_ACCOUNTS'; -+ LinkErrorCode['NO_LIABILITY_ACCOUNTS'] = 'NO_LIABILITY_ACCOUNTS'; -+ LinkErrorCode['PRODUCTS_NOT_SUPPORTED'] = 'PRODUCTS_NOT_SUPPORTED'; -+ LinkErrorCode['ITEM_NOT_FOUND'] = 'ITEM_NOT_FOUND'; -+ LinkErrorCode['ITEM_PRODUCT_NOT_READY'] = 'ITEM_PRODUCT_NOT_READY'; -+ // INSTITUTION_ERROR -+ LinkErrorCode['INSTITUTION_DOWN'] = 'INSTITUTION_DOWN'; -+ LinkErrorCode['INSTITUTION_NOT_RESPONDING'] = 'INSTITUTION_NOT_RESPONDING'; -+ LinkErrorCode['INSTITUTION_NOT_AVAILABLE'] = 'INSTITUTION_NOT_AVAILABLE'; -+ LinkErrorCode['INSTITUTION_NO_LONGER_SUPPORTED'] = -+ 'INSTITUTION_NO_LONGER_SUPPORTED'; -+ // API_ERROR -+ LinkErrorCode['INTERNAL_SERVER_ERROR'] = 'INTERNAL_SERVER_ERROR'; -+ LinkErrorCode['PLANNED_MAINTENANCE'] = 'PLANNED_MAINTENANCE'; -+ // ASSET_REPORT_ERROR -+ LinkErrorCode['PRODUCT_NOT_ENABLED'] = 'PRODUCT_NOT_ENABLED'; -+ LinkErrorCode['DATA_UNAVAILABLE'] = 'DATA_UNAVAILABLE'; -+ LinkErrorCode['ASSET_PRODUCT_NOT_READY'] = 'ASSET_PRODUCT_NOT_READY'; -+ LinkErrorCode['ASSET_REPORT_GENERATION_FAILED'] = -+ 'ASSET_REPORT_GENERATION_FAILED'; -+ LinkErrorCode['INVALID_PARENT'] = 'INVALID_PARENT'; -+ LinkErrorCode['INSIGHTS_NOT_ENABLED'] = 'INSIGHTS_NOT_ENABLED'; -+ LinkErrorCode['INSIGHTS_PREVIOUSLY_NOT_ENABLED'] = -+ 'INSIGHTS_PREVIOUSLY_NOT_ENABLED'; -+ // BANK_TRANSFER_ERROR -+ LinkErrorCode['BANK_TRANSFER_LIMIT_EXCEEDED'] = -+ 'BANK_TRANSFER_LIMIT_EXCEEDED'; -+ LinkErrorCode['BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT'] = -+ 'BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT'; -+ LinkErrorCode['BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT'] = -+ 'BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT'; -+ LinkErrorCode['BANK_TRANSFER_ACCOUNT_BLOCKED'] = -+ 'BANK_TRANSFER_ACCOUNT_BLOCKED'; -+ LinkErrorCode['BANK_TRANSFER_INSUFFICIENT_FUNDS'] = -+ 'BANK_TRANSFER_INSUFFICIENT_FUNDS'; -+ LinkErrorCode['BANK_TRANSFER_NOT_CANCELLABLE'] = -+ 'BANK_TRANSFER_NOT_CANCELLABLE'; -+ LinkErrorCode['BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE'] = -+ 'BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE'; -+ LinkErrorCode['BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT'] = -+ 'BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT'; -+ // SANDBOX_ERROR -+ LinkErrorCode['SANDBOX_PRODUCT_NOT_ENABLED'] = 'SANDBOX_PRODUCT_NOT_ENABLED'; -+ LinkErrorCode['SANDBOX_WEBHOOK_INVALID'] = 'SANDBOX_WEBHOOK_INVALID'; -+ LinkErrorCode['SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID'] = -+ 'SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID'; -+ // INVALID_REQUEST -+ LinkErrorCode['MISSING_FIELDS'] = 'MISSING_FIELDS'; -+ LinkErrorCode['UNKNOWN_FIELDS'] = 'UNKNOWN_FIELDS'; -+ LinkErrorCode['INVALID_FIELD'] = 'INVALID_FIELD'; -+ LinkErrorCode['INCOMPATIBLE_API_VERSION'] = 'INCOMPATIBLE_API_VERSION'; -+ LinkErrorCode['INVALID_BODY'] = 'INVALID_BODY'; -+ LinkErrorCode['INVALID_HEADERS'] = 'INVALID_HEADERS'; -+ LinkErrorCode['NOT_FOUND'] = 'NOT_FOUND'; -+ LinkErrorCode['NO_LONGER_AVAILABLE'] = 'NO_LONGER_AVAILABLE'; -+ LinkErrorCode['SANDBOX_ONLY'] = 'SANDBOX_ONLY'; -+ LinkErrorCode['INVALID_ACCOUNT_NUMBER'] = 'INVALID_ACCOUNT_NUMBER'; -+ // INVALID_INPUT -+ // From above ITEM_LOGIN_REQUIRED = "INVALID_CREDENTIALS", -+ LinkErrorCode['INCORRECT_DEPOSIT_AMOUNTS'] = 'INCORRECT_DEPOSIT_AMOUNTS'; -+ LinkErrorCode['UNAUTHORIZED_ENVIRONMENT'] = 'UNAUTHORIZED_ENVIRONMENT'; -+ LinkErrorCode['INVALID_PRODUCT'] = 'INVALID_PRODUCT'; -+ LinkErrorCode['UNAUTHORIZED_ROUTE_ACCESS'] = 'UNAUTHORIZED_ROUTE_ACCESS'; -+ LinkErrorCode['DIRECT_INTEGRATION_NOT_ENABLED'] = -+ 'DIRECT_INTEGRATION_NOT_ENABLED'; -+ LinkErrorCode['INVALID_API_KEYS'] = 'INVALID_API_KEYS'; -+ LinkErrorCode['INVALID_ACCESS_TOKEN'] = 'INVALID_ACCESS_TOKEN'; -+ LinkErrorCode['INVALID_PUBLIC_TOKEN'] = 'INVALID_PUBLIC_TOKEN'; -+ LinkErrorCode['INVALID_LINK_TOKEN'] = 'INVALID_LINK_TOKEN'; -+ LinkErrorCode['INVALID_PROCESSOR_TOKEN'] = 'INVALID_PROCESSOR_TOKEN'; -+ LinkErrorCode['INVALID_AUDIT_COPY_TOKEN'] = 'INVALID_AUDIT_COPY_TOKEN'; -+ LinkErrorCode['INVALID_ACCOUNT_ID'] = 'INVALID_ACCOUNT_ID'; -+ LinkErrorCode['MICRODEPOSITS_ALREADY_VERIFIED'] = -+ 'MICRODEPOSITS_ALREADY_VERIFIED'; -+ // INVALID_RESULT -+ LinkErrorCode['PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA'] = -+ 'PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA'; -+ // RATE_LIMIT_EXCEEDED -+ LinkErrorCode['ACCOUNTS_LIMIT'] = 'ACCOUNTS_LIMIT'; -+ LinkErrorCode['ADDITION_LIMIT'] = 'ADDITION_LIMIT'; -+ LinkErrorCode['AUTH_LIMIT'] = 'AUTH_LIMIT'; -+ LinkErrorCode['BALANCE_LIMIT'] = 'BALANCE_LIMIT'; -+ LinkErrorCode['IDENTITY_LIMIT'] = 'IDENTITY_LIMIT'; -+ LinkErrorCode['ITEM_GET_LIMIT'] = 'ITEM_GET_LIMIT'; -+ LinkErrorCode['RATE_LIMIT'] = 'RATE_LIMIT'; -+ LinkErrorCode['TRANSACTIONS_LIMIT'] = 'TRANSACTIONS_LIMIT'; -+ // RECAPTCHA_ERROR -+ LinkErrorCode['RECAPTCHA_REQUIRED'] = 'RECAPTCHA_REQUIRED'; -+ LinkErrorCode['RECAPTCHA_BAD'] = 'RECAPTCHA_BAD'; -+ // OAUTH_ERROR -+ LinkErrorCode['INCORRECT_OAUTH_NONCE'] = 'INCORRECT_OAUTH_NONCE'; -+ LinkErrorCode['OAUTH_STATE_ID_ALREADY_PROCESSED'] = -+ 'OAUTH_STATE_ID_ALREADY_PROCESSED'; - })(LinkErrorCode || (LinkErrorCode = {})); - export var LinkErrorType; --(function (LinkErrorType) { -- LinkErrorType["BANK_TRANSFER"] = "BANK_TRANSFER_ERROR"; -- LinkErrorType["INVALID_REQUEST"] = "INVALID_REQUEST"; -- LinkErrorType["INVALID_RESULT"] = "INVALID_RESULT"; -- LinkErrorType["INVALID_INPUT"] = "INVALID_INPUT"; -- LinkErrorType["INSTITUTION_ERROR"] = "INSTITUTION_ERROR"; -- LinkErrorType["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED"; -- LinkErrorType["API_ERROR"] = "API_ERROR"; -- LinkErrorType["ITEM_ERROR"] = "ITEM_ERROR"; -- LinkErrorType["AUTH_ERROR"] = "AUTH_ERROR"; -- LinkErrorType["ASSET_REPORT_ERROR"] = "ASSET_REPORT_ERROR"; -- LinkErrorType["SANDBOX_ERROR"] = "SANDBOX_ERROR"; -- LinkErrorType["RECAPTCHA_ERROR"] = "RECAPTCHA_ERROR"; -- LinkErrorType["OAUTH_ERROR"] = "OAUTH_ERROR"; -+(function(LinkErrorType) { -+ LinkErrorType['BANK_TRANSFER'] = 'BANK_TRANSFER_ERROR'; -+ LinkErrorType['INVALID_REQUEST'] = 'INVALID_REQUEST'; -+ LinkErrorType['INVALID_RESULT'] = 'INVALID_RESULT'; -+ LinkErrorType['INVALID_INPUT'] = 'INVALID_INPUT'; -+ LinkErrorType['INSTITUTION_ERROR'] = 'INSTITUTION_ERROR'; -+ LinkErrorType['RATE_LIMIT_EXCEEDED'] = 'RATE_LIMIT_EXCEEDED'; -+ LinkErrorType['API_ERROR'] = 'API_ERROR'; -+ LinkErrorType['ITEM_ERROR'] = 'ITEM_ERROR'; -+ LinkErrorType['AUTH_ERROR'] = 'AUTH_ERROR'; -+ LinkErrorType['ASSET_REPORT_ERROR'] = 'ASSET_REPORT_ERROR'; -+ LinkErrorType['SANDBOX_ERROR'] = 'SANDBOX_ERROR'; -+ LinkErrorType['RECAPTCHA_ERROR'] = 'RECAPTCHA_ERROR'; -+ LinkErrorType['OAUTH_ERROR'] = 'OAUTH_ERROR'; - })(LinkErrorType || (LinkErrorType = {})); - export var LinkEventName; --(function (LinkEventName) { -- LinkEventName["BANK_INCOME_INSIGHTS_COMPLETED"] = "BANK_INCOME_INSIGHTS_COMPLETED"; -- LinkEventName["CLOSE_OAUTH"] = "CLOSE_OAUTH"; -- LinkEventName["ERROR"] = "ERROR"; -- LinkEventName["EXIT"] = "EXIT"; -- LinkEventName["FAIL_OAUTH"] = "FAIL_OAUTH"; -- LinkEventName["HANDOFF"] = "HANDOFF"; -- LinkEventName["IDENTITY_VERIFICATION_START_STEP"] = "IDENTITY_VERIFICATION_START_STEP"; -- LinkEventName["IDENTITY_VERIFICATION_PASS_STEP"] = "IDENTITY_VERIFICATION_PASS_STEP"; -- LinkEventName["IDENTITY_VERIFICATION_FAIL_STEP"] = "IDENTITY_VERIFICATION_FAIL_STEP"; -- LinkEventName["IDENTITY_VERIFICATION_PENDING_REVIEW_STEP"] = "IDENTITY_VERIFICATION_PENDING_REVIEW_STEP"; -- LinkEventName["IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION"] = "IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION"; -- LinkEventName["IDENTITY_VERIFICATION_CREATE_SESSION"] = "IDENTITY_VERIFICATION_CREATE_SESSION"; -- LinkEventName["IDENTITY_VERIFICATION_RESUME_SESSION"] = "IDENTITY_VERIFICATION_RESUME_SESSION"; -- LinkEventName["IDENTITY_VERIFICATION_PASS_SESSION"] = "IDENTITY_VERIFICATION_PASS_SESSION"; -- LinkEventName["IDENTITY_VERIFICATION_FAIL_SESSION"] = "IDENTITY_VERIFICATION_FAIL_SESSION"; -- LinkEventName["IDENTITY_VERIFICATION_OPEN_UI"] = "IDENTITY_VERIFICATION_OPEN_UI"; -- LinkEventName["IDENTITY_VERIFICATION_RESUME_UI"] = "IDENTITY_VERIFICATION_RESUME_UI"; -- LinkEventName["IDENTITY_VERIFICATION_CLOSE_UI"] = "IDENTITY_VERIFICATION_CLOSE_UI"; -- LinkEventName["MATCHED_CONSENT"] = "MATCHED_CONSENT"; -- LinkEventName["MATCHED_SELECT_INSTITUTION"] = "MATCHED_SELECT_INSTITUTION"; -- LinkEventName["MATCHED_SELECT_VERIFY_METHOD"] = "MATCHED_SELECT_VERIFY_METHOD"; -- LinkEventName["OPEN"] = "OPEN"; -- LinkEventName["OPEN_MY_PLAID"] = "OPEN_MY_PLAID"; -- LinkEventName["OPEN_OAUTH"] = "OPEN_OAUTH"; -- LinkEventName["SEARCH_INSTITUTION"] = "SEARCH_INSTITUTION"; -- LinkEventName["SELECT_DEGRADED_INSTITUTION"] = "SELECT_DEGRADED_INSTITUTION"; -- LinkEventName["SELECT_DOWN_INSTITUTION"] = "SELECT_DOWN_INSTITUTION"; -- LinkEventName["SELECT_FILTERED_INSTITUTION"] = "SELECT_FILTERED_INSTITUTION"; -- LinkEventName["SELECT_INSTITUTION"] = "SELECT_INSTITUTION"; -- LinkEventName["SELECT_BRAND"] = "SELECT_BRAND"; -- LinkEventName["SELECT_AUTH_TYPE"] = "SELECT_AUTH_TYPE"; -- LinkEventName["SUBMIT_ACCOUNT_NUMBER"] = "SUBMIT_ACCOUNT_NUMBER"; -- LinkEventName["SUBMIT_DOCUMENTS"] = "SUBMIT_DOCUMENTS"; -- LinkEventName["SUBMIT_DOCUMENTS_SUCCESS"] = "SUBMIT_DOCUMENTS_SUCCESS"; -- LinkEventName["SUBMIT_DOCUMENTS_ERROR"] = "SUBMIT_DOCUMENTS_ERROR"; -- LinkEventName["SUBMIT_ROUTING_NUMBER"] = "SUBMIT_ROUTING_NUMBER"; -- LinkEventName["VIEW_DATA_TYPES"] = "VIEW_DATA_TYPES"; -- LinkEventName["SUBMIT_PHONE"] = "SUBMIT_PHONE"; -- LinkEventName["SKIP_SUBMIT_PHONE"] = "SKIP_SUBMIT_PHONE"; -- LinkEventName["VERIFY_PHONE"] = "VERIFY_PHONE"; -- LinkEventName["SUBMIT_CREDENTIALS"] = "SUBMIT_CREDENTIALS"; -- LinkEventName["SUBMIT_MFA"] = "SUBMIT_MFA"; -- LinkEventName["TRANSITION_VIEW"] = "TRANSITION_VIEW"; -- LinkEventName["CONNECT_NEW_INSTITUTION"] = "CONNECT_NEW_INSTITUTION"; -+(function(LinkEventName) { -+ LinkEventName['BANK_INCOME_INSIGHTS_COMPLETED'] = -+ 'BANK_INCOME_INSIGHTS_COMPLETED'; -+ LinkEventName['CLOSE_OAUTH'] = 'CLOSE_OAUTH'; -+ LinkEventName['ERROR'] = 'ERROR'; -+ LinkEventName['EXIT'] = 'EXIT'; -+ LinkEventName['FAIL_OAUTH'] = 'FAIL_OAUTH'; -+ LinkEventName['HANDOFF'] = 'HANDOFF'; -+ LinkEventName['IDENTITY_VERIFICATION_START_STEP'] = -+ 'IDENTITY_VERIFICATION_START_STEP'; -+ LinkEventName['IDENTITY_VERIFICATION_PASS_STEP'] = -+ 'IDENTITY_VERIFICATION_PASS_STEP'; -+ LinkEventName['IDENTITY_VERIFICATION_FAIL_STEP'] = -+ 'IDENTITY_VERIFICATION_FAIL_STEP'; -+ LinkEventName['IDENTITY_VERIFICATION_PENDING_REVIEW_STEP'] = -+ 'IDENTITY_VERIFICATION_PENDING_REVIEW_STEP'; -+ LinkEventName['IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION'] = -+ 'IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION'; -+ LinkEventName['IDENTITY_VERIFICATION_CREATE_SESSION'] = -+ 'IDENTITY_VERIFICATION_CREATE_SESSION'; -+ LinkEventName['IDENTITY_VERIFICATION_RESUME_SESSION'] = -+ 'IDENTITY_VERIFICATION_RESUME_SESSION'; -+ LinkEventName['IDENTITY_VERIFICATION_PASS_SESSION'] = -+ 'IDENTITY_VERIFICATION_PASS_SESSION'; -+ LinkEventName['IDENTITY_VERIFICATION_FAIL_SESSION'] = -+ 'IDENTITY_VERIFICATION_FAIL_SESSION'; -+ LinkEventName['IDENTITY_VERIFICATION_OPEN_UI'] = -+ 'IDENTITY_VERIFICATION_OPEN_UI'; -+ LinkEventName['IDENTITY_VERIFICATION_RESUME_UI'] = -+ 'IDENTITY_VERIFICATION_RESUME_UI'; -+ LinkEventName['IDENTITY_VERIFICATION_CLOSE_UI'] = -+ 'IDENTITY_VERIFICATION_CLOSE_UI'; -+ LinkEventName['MATCHED_CONSENT'] = 'MATCHED_CONSENT'; -+ LinkEventName['MATCHED_SELECT_INSTITUTION'] = 'MATCHED_SELECT_INSTITUTION'; -+ LinkEventName['MATCHED_SELECT_VERIFY_METHOD'] = -+ 'MATCHED_SELECT_VERIFY_METHOD'; -+ LinkEventName['OPEN'] = 'OPEN'; -+ LinkEventName['OPEN_MY_PLAID'] = 'OPEN_MY_PLAID'; -+ LinkEventName['OPEN_OAUTH'] = 'OPEN_OAUTH'; -+ LinkEventName['SEARCH_INSTITUTION'] = 'SEARCH_INSTITUTION'; -+ LinkEventName['SELECT_DEGRADED_INSTITUTION'] = 'SELECT_DEGRADED_INSTITUTION'; -+ LinkEventName['SELECT_DOWN_INSTITUTION'] = 'SELECT_DOWN_INSTITUTION'; -+ LinkEventName['SELECT_FILTERED_INSTITUTION'] = 'SELECT_FILTERED_INSTITUTION'; -+ LinkEventName['SELECT_INSTITUTION'] = 'SELECT_INSTITUTION'; -+ LinkEventName['SELECT_BRAND'] = 'SELECT_BRAND'; -+ LinkEventName['SELECT_AUTH_TYPE'] = 'SELECT_AUTH_TYPE'; -+ LinkEventName['SUBMIT_ACCOUNT_NUMBER'] = 'SUBMIT_ACCOUNT_NUMBER'; -+ LinkEventName['SUBMIT_DOCUMENTS'] = 'SUBMIT_DOCUMENTS'; -+ LinkEventName['SUBMIT_DOCUMENTS_SUCCESS'] = 'SUBMIT_DOCUMENTS_SUCCESS'; -+ LinkEventName['SUBMIT_DOCUMENTS_ERROR'] = 'SUBMIT_DOCUMENTS_ERROR'; -+ LinkEventName['SUBMIT_ROUTING_NUMBER'] = 'SUBMIT_ROUTING_NUMBER'; -+ LinkEventName['VIEW_DATA_TYPES'] = 'VIEW_DATA_TYPES'; -+ LinkEventName['SUBMIT_PHONE'] = 'SUBMIT_PHONE'; -+ LinkEventName['SKIP_SUBMIT_PHONE'] = 'SKIP_SUBMIT_PHONE'; -+ LinkEventName['VERIFY_PHONE'] = 'VERIFY_PHONE'; -+ LinkEventName['SUBMIT_CREDENTIALS'] = 'SUBMIT_CREDENTIALS'; -+ LinkEventName['SUBMIT_MFA'] = 'SUBMIT_MFA'; -+ LinkEventName['TRANSITION_VIEW'] = 'TRANSITION_VIEW'; -+ LinkEventName['CONNECT_NEW_INSTITUTION'] = 'CONNECT_NEW_INSTITUTION'; - })(LinkEventName || (LinkEventName = {})); - export var LinkEventViewName; --(function (LinkEventViewName) { -- LinkEventViewName["ACCEPT_TOS"] = "ACCEPT_TOS"; -- LinkEventViewName["CONNECTED"] = "CONNECTED"; -- LinkEventViewName["CONSENT"] = "CONSENT"; -- LinkEventViewName["CREDENTIAL"] = "CREDENTIAL"; -- LinkEventViewName["DATA_TRANSPARENCY"] = "DATA_TRANSPARENCY"; -- LinkEventViewName["DATA_TRANSPARENCY_CONSENT"] = "DATA_TRANSPARENCY_CONSENT"; -- LinkEventViewName["DOCUMENTARY_VERIFICATION"] = "DOCUMENTARY_VERIFICATION"; -- LinkEventViewName["ERROR"] = "ERROR"; -- LinkEventViewName["EXIT"] = "EXIT"; -- LinkEventViewName["KYC_CHECK"] = "KYC_CHECK"; -- LinkEventViewName["SELFIE_CHECK"] = "SELFIE_CHECK"; -- LinkEventViewName["LOADING"] = "LOADING"; -- LinkEventViewName["MATCHED_CONSENT"] = "MATCHED_CONSENT"; -- LinkEventViewName["MATCHED_CREDENTIAL"] = "MATCHED_CREDENTIAL"; -- LinkEventViewName["MATCHED_MFA"] = "MATCHED_MFA"; -- LinkEventViewName["MFA"] = "MFA"; -- LinkEventViewName["NUMBERS"] = "NUMBERS"; -- LinkEventViewName["NUMBERS_SELECT_INSTITUTION"] = "NUMBERS_SELECT_INSTITUTION"; -- LinkEventViewName["OAUTH"] = "OAUTH"; -- LinkEventViewName["RECAPTCHA"] = "RECAPTCHA"; -- LinkEventViewName["RISK_CHECK"] = "RISK_CHECK"; -- LinkEventViewName["SCREENING"] = "SCREENING"; -- LinkEventViewName["SELECT_ACCOUNT"] = "SELECT_ACCOUNT"; -- LinkEventViewName["SELECT_AUTH_TYPE"] = "SELECT_AUTH_TYPE"; -- LinkEventViewName["SUBMIT_PHONE"] = "SUBMIT_PHONE"; -- LinkEventViewName["VERIFY_PHONE"] = "VERIFY_PHONE"; -- LinkEventViewName["SELECT_SAVED_INSTITUTION"] = "SELECT_SAVED_INSTITUTION"; -- LinkEventViewName["SELECT_SAVED_ACCOUNT"] = "SELECT_SAVED_ACCOUNT"; -- LinkEventViewName["SELECT_BRAND"] = "SELECT_BRAND"; -- LinkEventViewName["SELECT_INSTITUTION"] = "SELECT_INSTITUTION"; -- LinkEventViewName["SUBMIT_DOCUMENTS"] = "SUBMIT_DOCUMENTS"; -- LinkEventViewName["SUBMIT_DOCUMENTS_SUCCESS"] = "SUBMIT_DOCUMENTS_SUCCESS"; -- LinkEventViewName["SUBMIT_DOCUMENTS_ERROR"] = "SUBMIT_DOCUMENTS_ERROR"; -- LinkEventViewName["UPLOAD_DOCUMENTS"] = "UPLOAD_DOCUMENTS"; -- LinkEventViewName["VERIFY_SMS"] = "VERIFY_SMS"; -+(function(LinkEventViewName) { -+ LinkEventViewName['ACCEPT_TOS'] = 'ACCEPT_TOS'; -+ LinkEventViewName['CONNECTED'] = 'CONNECTED'; -+ LinkEventViewName['CONSENT'] = 'CONSENT'; -+ LinkEventViewName['CREDENTIAL'] = 'CREDENTIAL'; -+ LinkEventViewName['DATA_TRANSPARENCY'] = 'DATA_TRANSPARENCY'; -+ LinkEventViewName['DATA_TRANSPARENCY_CONSENT'] = 'DATA_TRANSPARENCY_CONSENT'; -+ LinkEventViewName['DOCUMENTARY_VERIFICATION'] = 'DOCUMENTARY_VERIFICATION'; -+ LinkEventViewName['ERROR'] = 'ERROR'; -+ LinkEventViewName['EXIT'] = 'EXIT'; -+ LinkEventViewName['KYC_CHECK'] = 'KYC_CHECK'; -+ LinkEventViewName['SELFIE_CHECK'] = 'SELFIE_CHECK'; -+ LinkEventViewName['LOADING'] = 'LOADING'; -+ LinkEventViewName['MATCHED_CONSENT'] = 'MATCHED_CONSENT'; -+ LinkEventViewName['MATCHED_CREDENTIAL'] = 'MATCHED_CREDENTIAL'; -+ LinkEventViewName['MATCHED_MFA'] = 'MATCHED_MFA'; -+ LinkEventViewName['MFA'] = 'MFA'; -+ LinkEventViewName['NUMBERS'] = 'NUMBERS'; -+ LinkEventViewName['NUMBERS_SELECT_INSTITUTION'] = -+ 'NUMBERS_SELECT_INSTITUTION'; -+ LinkEventViewName['OAUTH'] = 'OAUTH'; -+ LinkEventViewName['RECAPTCHA'] = 'RECAPTCHA'; -+ LinkEventViewName['RISK_CHECK'] = 'RISK_CHECK'; -+ LinkEventViewName['SCREENING'] = 'SCREENING'; -+ LinkEventViewName['SELECT_ACCOUNT'] = 'SELECT_ACCOUNT'; -+ LinkEventViewName['SELECT_AUTH_TYPE'] = 'SELECT_AUTH_TYPE'; -+ LinkEventViewName['SUBMIT_PHONE'] = 'SUBMIT_PHONE'; -+ LinkEventViewName['VERIFY_PHONE'] = 'VERIFY_PHONE'; -+ LinkEventViewName['SELECT_SAVED_INSTITUTION'] = 'SELECT_SAVED_INSTITUTION'; -+ LinkEventViewName['SELECT_SAVED_ACCOUNT'] = 'SELECT_SAVED_ACCOUNT'; -+ LinkEventViewName['SELECT_BRAND'] = 'SELECT_BRAND'; -+ LinkEventViewName['SELECT_INSTITUTION'] = 'SELECT_INSTITUTION'; -+ LinkEventViewName['SUBMIT_DOCUMENTS'] = 'SUBMIT_DOCUMENTS'; -+ LinkEventViewName['SUBMIT_DOCUMENTS_SUCCESS'] = 'SUBMIT_DOCUMENTS_SUCCESS'; -+ LinkEventViewName['SUBMIT_DOCUMENTS_ERROR'] = 'SUBMIT_DOCUMENTS_ERROR'; -+ LinkEventViewName['UPLOAD_DOCUMENTS'] = 'UPLOAD_DOCUMENTS'; -+ LinkEventViewName['VERIFY_SMS'] = 'VERIFY_SMS'; - })(LinkEventViewName || (LinkEventViewName = {})); - /// Methods to present Link on iOS. - /// FULL_SCREEN is the converts to UIModalPresentationOverFullScreen on the native side. - /// MODAL will use the default presentation style for iOS which is UIModalPresentationAutomatic. - export var LinkIOSPresentationStyle; --(function (LinkIOSPresentationStyle) { -- LinkIOSPresentationStyle["FULL_SCREEN"] = "FULL_SCREEN"; -- LinkIOSPresentationStyle["MODAL"] = "MODAL"; -+(function(LinkIOSPresentationStyle) { -+ LinkIOSPresentationStyle['FULL_SCREEN'] = 'FULL_SCREEN'; -+ LinkIOSPresentationStyle['MODAL'] = 'MODAL'; - })(LinkIOSPresentationStyle || (LinkIOSPresentationStyle = {})); -diff --git a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts -index cd4ccde..cb0ff5c 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts -+++ b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.d.ts -@@ -1 +1 @@ --declare const Types: any; -+export {}; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js -index 831866b..f38aff8 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js -+++ b/node_modules/react-native-plaid-link-sdk/dist/__tests__/Types.tests.js -@@ -1,14 +1,14 @@ --"use strict"; - const Types = require('./../Types'); - test('test token configuration', () => { -- const linkTokenConfiguration = { -- token: "test-token", -- noLoadingState: false, -- logLevel: Types.LinkLogLevel.DEBUG, -- extras: null, -- }; -- expect(linkTokenConfiguration.noLoadingState).toBe(false); -- expect(linkTokenConfiguration.token).toBe("test-token"); -- expect(linkTokenConfiguration.logLevel).toBe(Types.LinkLogLevel.DEBUG); -- expect(linkTokenConfiguration.extras).toBe(null); -+ const linkTokenConfiguration = { -+ token: 'test-token', -+ noLoadingState: false, -+ logLevel: Types.LinkLogLevel.DEBUG, -+ extras: null, -+ }; -+ expect(linkTokenConfiguration.noLoadingState).toBe(false); -+ expect(linkTokenConfiguration.token).toBe('test-token'); -+ expect(linkTokenConfiguration.logLevel).toBe(Types.LinkLogLevel.DEBUG); -+ expect(linkTokenConfiguration.extras).toBe(null); - }); -+export {}; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.d.ts b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.d.ts -new file mode 100644 -index 0000000..cb0ff5c ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.d.ts -@@ -0,0 +1 @@ -+export {}; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.js b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.js -new file mode 100644 -index 0000000..34a47d4 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/check_version_hook.js -@@ -0,0 +1,15 @@ -+'use strict'; -+const fs = require('fs'); -+const rn_version = JSON.parse(fs.readFileSync('package.json', 'utf-8'))[ -+ 'version' -+]; -+const android_version = fs -+ .readFileSync('android/src/main/AndroidManifest.xml', 'utf-8') -+ .match(/(?<=value=").*(?=")/)[0]; -+if (rn_version != android_version) { -+ console.error('Commit failed SDK version mismatch'); -+ console.error( -+ 'Please ensure package.json and android/src/main/AndroidManifest.xml have the same version', -+ ); -+ process.exit(-1); -+} -diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.d.ts b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.d.ts -new file mode 100644 -index 0000000..1de3019 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.d.ts -@@ -0,0 +1,14 @@ -+import { TurboModule } from 'react-native'; -+import { Int32 } from 'react-native/Libraries/Types/CodegenTypes'; -+import { LinkSuccess as UnsafeObject, LinkExit as Double, LinkError as Float } from '../Types'; -+export interface Spec extends TurboModule { -+ continueFromRedirectUriString(redirectUriString: string): void; -+ create(configuration: Object): void; -+ open(onSuccess: (success: UnsafeObject) => void, onExit: (error: Float, result: Double) => void): void; -+ dismiss(): void; -+ startLinkActivityForResult(data: string, onSuccessCallback: (result: UnsafeObject) => void, onExitCallback: (result: Double) => void): void; -+ addListener(eventName: string): void; -+ removeListeners(count: Int32): void; -+} -+declare const _default: Spec; -+export default _default; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.js b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.js -new file mode 100644 -index 0000000..310a9c5 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModule.js -@@ -0,0 +1,4 @@ -+// we use Object type because methods on the native side use NSDictionary and ReadableMap -+// and we want to stay compatible with those -+import { TurboModuleRegistry } from 'react-native'; -+export default TurboModuleRegistry.getEnforcing('RNLinksdk'); -diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.d.ts b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.d.ts -new file mode 100644 -index 0000000..82f29a1 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.d.ts -@@ -0,0 +1,9 @@ -+import { TurboModule } from 'react-native'; -+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; -+export interface Spec extends TurboModule { -+ startLinkActivityForResult(token: string, noLoadingState: boolean, logLevel: string, onSuccessCallback: (result: UnsafeObject) => void, onExitCallback: (result: UnsafeObject) => void): void; -+ addListener(eventName: string): void; -+ removeListeners(count: Int32): void; -+} -+declare const _default: Spec | null; -+export default _default; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.js b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.js -new file mode 100644 -index 0000000..d0ea456 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleAndroid.js -@@ -0,0 +1,4 @@ -+// we use Object type because methods on the native side use NSDictionary and ReadableMap -+// and we want to stay compatible with those -+import { TurboModuleRegistry } from 'react-native'; -+export default TurboModuleRegistry.get('PlaidAndroid'); -diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.d.ts b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.d.ts -new file mode 100644 -index 0000000..aefee8c ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.d.ts -@@ -0,0 +1,11 @@ -+import { TurboModule } from 'react-native'; -+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; -+export interface Spec extends TurboModule { -+ create(token: string, noLoadingState: boolean): void; -+ open(fullScreen: boolean, onSuccess: (success: UnsafeObject) => void, onExit: (error: UnsafeObject, result: UnsafeObject) => void): void; -+ dismiss(): void; -+ addListener(eventName: string): void; -+ removeListeners(count: Int32): void; -+} -+declare const _default: Spec | null; -+export default _default; -diff --git a/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.js b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.js -new file mode 100644 -index 0000000..99845a1 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/dist/fabric/NativePlaidLinkModuleiOS.js -@@ -0,0 +1,4 @@ -+// we use Object type because methods on the native side use NSDictionary and ReadableMap -+// and we want to stay compatible with those -+import { TurboModuleRegistry } from 'react-native'; -+export default TurboModuleRegistry.get('RNLinksdk'); -diff --git a/node_modules/react-native-plaid-link-sdk/dist/index.js b/node_modules/react-native-plaid-link-sdk/dist/index.js -index 68c2d4b..acad079 100644 ---- a/node_modules/react-native-plaid-link-sdk/dist/index.js -+++ b/node_modules/react-native-plaid-link-sdk/dist/index.js -@@ -1,6 +1,6 @@ --import { openLink, dismissLink, usePlaidEmitter, PlaidLink, } from './PlaidLink'; -+import { openLink, dismissLink, usePlaidEmitter, PlaidLink } from './PlaidLink'; - export * from './Types'; - export default PlaidLink; --export { PlaidLink, openLink, dismissLink, usePlaidEmitter, }; -+export { PlaidLink, openLink, dismissLink, usePlaidEmitter }; - // Components - export { EmbeddedLinkView } from './EmbeddedLink/EmbeddedLinkView'; -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h -index 8a1c350..035b91c 100644 ---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h -+++ b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.h -@@ -1,15 +1,17 @@ -- --#if __has_include() --#import --#import --#else --#import "RCTBridgeModule.h" --#import "RCTEventEmitter.h" -+#ifdef RCT_NEW_ARCH_ENABLED -+#import - #endif -+#import -+#import "RCTEventEmitter.h" - - #import - --@interface RNLinksdk : RCTEventEmitter -+@interface RNLinksdk : RCTEventEmitter -+#ifdef RCT_NEW_ARCH_ENABLED -+ -+#else -+ -+#endif - - + (NSDictionary *)dictionaryFromSuccess:(PLKLinkSuccess *)success; - + (NSDictionary *)dictionaryFromEvent:(PLKLinkEvent *)event; -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm -index ef3fe85..b3b92d6 100644 ---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm -+++ b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.mm -@@ -1,4 +1,5 @@ - #import "RNLinksdk.h" -+#import "RNPlaidHelper.h" - - #import - #import -@@ -66,11 +67,11 @@ - (void)stopObserving { - self.hasObservers = NO; - } - --RCT_EXPORT_METHOD(create:(NSString*)token :(BOOL)noLoadingState) { -- __weak typeof(self) weakSelf = self; -+RCT_EXPORT_METHOD(create:(NSString*)token noLoadingState:(BOOL)noLoadingState) { -+ __weak RNLinksdk *weakSelf = self; - - void (^onSuccess)(PLKLinkSuccess *) = ^(PLKLinkSuccess *success) { -- __typeof(weakSelf) strongSelf = weakSelf; -+ RNLinksdk *strongSelf = weakSelf; - - if (strongSelf.successCallback) { - NSDictionary *jsMetadata = [RNLinksdk dictionaryFromSuccess:success]; -@@ -80,7 +81,7 @@ - (void)stopObserving { - }; - - void (^onExit)(PLKLinkExit *) = ^(PLKLinkExit *exit) { -- __typeof(weakSelf) strongSelf = weakSelf; -+ RNLinksdk *strongSelf = weakSelf; - - if (strongSelf.exitCallback) { - NSDictionary *exitMetadata = [RNLinksdk dictionaryFromExit:exit]; -@@ -94,7 +95,7 @@ - (void)stopObserving { - }; - - void (^onEvent)(PLKLinkEvent *) = ^(PLKLinkEvent *event) { -- __typeof(weakSelf) strongSelf = weakSelf; -+ RNLinksdk *strongSelf = weakSelf; - if (strongSelf.hasObservers) { - NSDictionary *eventDictionary = [RNLinksdk dictionaryFromEvent:event]; - [strongSelf sendEventWithName:kRNLinkKitOnEventEvent -@@ -108,11 +109,11 @@ - (void)stopObserving { - config.noLoadingState = noLoadingState; - - NSError *creationError = nil; -- self.linkHandler = [PLKPlaid createWithLinkTokenConfiguration:config error:&creationError]; -+ self.linkHandler = [RNPlaidHelper createWithLinkTokenConfiguration:config error:&creationError]; - self.creationError = creationError; - } - --RCT_EXPORT_METHOD(open: (BOOL)fullScreen :(RCTResponseSenderBlock)onSuccess :(RCTResponseSenderBlock)onExit) { -+RCT_EXPORT_METHOD(open:(BOOL)fullScreen onSuccess:(RCTResponseSenderBlock)onSuccess onExit:(RCTResponseSenderBlock)onExit) { - if (self.linkHandler) { - self.successCallback = onSuccess; - self.exitCallback = onExit; -@@ -122,7 +123,7 @@ - (void)stopObserving { - // unnecessarily invoked. - __block bool didPresent = NO; - -- __weak typeof(self) weakSelf = self; -+ __weak RNLinksdk *weakSelf = self; - void(^presentationHandler)(UIViewController *) = ^(UIViewController *linkViewController) { - - if (fullScreen) { -@@ -619,4 +620,12 @@ + (NSDictionary *)dictionaryFromExit:(PLKLinkExit *)exit { - }; - } - -+#if RCT_NEW_ARCH_ENABLED -+- (std::shared_ptr)getTurboModule: -+ (const facebook::react::ObjCTurboModule::InitParams &)params -+{ -+ return std::make_shared(params); -+} -+#endif -+ - @end -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata -deleted file mode 100644 -index 919434a..0000000 ---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata -+++ /dev/null -@@ -1,7 +0,0 @@ -- -- -- -- -- -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist -deleted file mode 100644 -index 18d9810..0000000 ---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist -+++ /dev/null -@@ -1,8 +0,0 @@ -- -- -- -- -- IDEDidComputeMac32BitWarning -- -- -- -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate -deleted file mode 100644 -index 47e9cc2..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/project.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/xcuserdata/dtroupe.xcuserdatad/xcschemes/xcschememanagement.plist b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/xcuserdata/dtroupe.xcuserdatad/xcschemes/xcschememanagement.plist -deleted file mode 100644 -index 5b4fa70..0000000 ---- a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcodeproj/xcuserdata/dtroupe.xcuserdatad/xcschemes/xcschememanagement.plist -+++ /dev/null -@@ -1,19 +0,0 @@ -- -- -- -- -- SchemeUserState -- -- RNLinksdk.xcscheme_^#shared#^_ -- -- orderHint -- 0 -- -- RNLinksdkUnitTests.xcscheme_^#shared#^_ -- -- orderHint -- 1 -- -- -- -- -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate b/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate -deleted file mode 100644 -index 824773d..0000000 -Binary files a/node_modules/react-native-plaid-link-sdk/ios/RNLinksdk.xcworkspace/xcuserdata/dtroupe.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.h b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.h -new file mode 100644 -index 0000000..535d333 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.h -@@ -0,0 +1,7 @@ -+#import -+ -+@interface RNPlaidHelper : NSObject -+ -++ (id _Nullable)createWithLinkTokenConfiguration:(PLKLinkTokenConfiguration * _Nonnull)linkTokenConfiguration error:(NSError * _Nullable * _Nullable)error; -+ -+@end -diff --git a/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.m b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.m -new file mode 100644 -index 0000000..2288299 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/ios/RNPlaidHelper.m -@@ -0,0 +1,10 @@ -+#import "RNPlaidHelper.h" -+ -+@implementation RNPlaidHelper -+ -++ (id _Nullable)createWithLinkTokenConfiguration:(PLKLinkTokenConfiguration * _Nonnull)linkTokenConfiguration error:(NSError * _Nullable * _Nullable)error -+{ -+ return [PLKPlaid createWithLinkTokenConfiguration:linkTokenConfiguration error:error]; -+} -+ -+@end -diff --git a/node_modules/react-native-plaid-link-sdk/package.json b/node_modules/react-native-plaid-link-sdk/package.json -index 22c7d2c..3d1b85c 100644 ---- a/node_modules/react-native-plaid-link-sdk/package.json -+++ b/node_modules/react-native-plaid-link-sdk/package.json -@@ -4,11 +4,13 @@ - "description": "React Native Plaid Link SDK", - "main": "dist/index.js", - "types": "dist/index.d.ts", -+ "react-native": "src/index.ts", - "files": [ - "dist/**/*", - "android/**/*", -- "ios", -- "react-native-plaid-link-sdk.podspec" -+ "ios/**/*", -+ "react-native-plaid-link-sdk.podspec", -+ "src/**/*" - ], - "scripts": { - "lint": "eslint \"./**/*.{js,jsx}\" --fix", -@@ -47,7 +49,7 @@ - "@react-native-community/eslint-plugin": "^1.1.0", - "@types/jest": "^26.0.14", - "@types/react": "^16.14.20", -- "@types/react-native": "^0.66.0", -+ "@types/react-native": "^0.71.3", - "@types/react-test-renderer": "^16.9.3", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", -@@ -62,5 +64,16 @@ - "react": "18.0.0", - "react-native": "0.69.9", - "typescript": "^4.9.5" -+ }, -+ "dependencies": { -+ "react-native-plaid-link-sdk": "^10.4.0" -+ }, -+ "codegenConfig": { -+ "name": "rnplaidlink", -+ "type": "modules", -+ "jsSrcsDir": "./src/fabric", -+ "android": { -+ "javaPackageName": "com.plaid" -+ } - } - } -diff --git a/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec b/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec -index ee59a19..40ac7df 100644 ---- a/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec -+++ b/node_modules/react-native-plaid-link-sdk/react-native-plaid-link-sdk.podspec -@@ -2,6 +2,8 @@ require 'json' - - package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) - -+fabric_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' -+ - Pod::Spec.new do |s| - s.name = package['name'] - s.version = package['version'] -@@ -13,8 +15,13 @@ Pod::Spec.new do |s| - s.platform = :ios, "14.0" - - s.source = { :git => "https://github.com/plaid/react-native-plaid-link-sdk.git", :tag => "v#{s.version}" } -- s.source_files = "ios/*.{h,m,swift}" -+ s.source_files = "ios/**/*.{h,m,mm,swift}" -+ -+ if fabric_enabled -+ install_modules_dependencies(s) -+ else -+ s.dependency "React-Core" -+ end - -- s.dependency 'React-Core' - s.dependency 'Plaid', '~> 5.2.0' - end -diff --git a/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.tsx b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.tsx -new file mode 100644 -index 0000000..0243de2 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.tsx -@@ -0,0 +1,95 @@ -+import React from 'react'; -+import { StyleProp, ViewStyle } from 'react-native'; -+import NativeEmbeddedLinkView from './NativeEmbeddedLinkView'; -+import { -+ LinkSuccessListener, -+ LinkSuccess, -+ LinkExitListener, -+ LinkExit, -+ LinkIOSPresentationStyle, -+ LinkOnEventListener, -+ LinkEvent, -+ LinkEventName, -+ LinkEventMetadata, -+ LinkError, -+ LinkExitMetadata, -+ LinkSuccessMetadata, -+} from '../Types'; -+ -+type EmbeddedLinkProps = { -+ token: String, -+ iOSPresentationStyle: LinkIOSPresentationStyle, -+ onEvent: LinkOnEventListener | undefined, -+ onSuccess: LinkSuccessListener, -+ onExit: LinkExitListener | undefined, -+ style: StyleProp | undefined, -+} -+ -+class EmbeddedEvent implements LinkEvent { -+ eventName: LinkEventName; -+ metadata: LinkEventMetadata; -+ -+ constructor(event: any) { -+ this.eventName = event.eventName -+ this.metadata = event.metadata -+ } -+} -+ -+class EmbeddedExit implements LinkExit { -+ error: LinkError | undefined; -+ metadata: LinkExitMetadata; -+ -+ constructor(event: any) { -+ this.error = event.error; -+ this.metadata = event.metadata; -+ } -+} -+ -+class EmbeddedSuccess implements LinkSuccess { -+ publicToken: string; -+ metadata: LinkSuccessMetadata; -+ -+ constructor(event: any) { -+ this.publicToken = event.publicToken; -+ this.metadata = event.metadata; -+ } -+} -+ -+export const EmbeddedLinkView: React.FC = (props) => { -+ -+ const {token, iOSPresentationStyle, onEvent, onSuccess, onExit, style} = props; -+ -+ const onEmbeddedEvent = (event: any) => { -+ -+ switch (event.nativeEvent.embeddedEventName) { -+ case 'onSuccess': { -+ if (!onSuccess) { return; } -+ const embeddedSuccess = new EmbeddedSuccess(event.nativeEvent); -+ onSuccess(embeddedSuccess); -+ break; -+ } -+ case 'onExit': { -+ if (!onExit) {return; } -+ const embeddedExit = new EmbeddedExit(event.nativeEvent); -+ onExit(embeddedExit); -+ break; -+ } -+ case 'onEvent': { -+ if (!onEvent) { return; } -+ const embeddedEvent = new EmbeddedEvent(event.nativeEvent); -+ onEvent(embeddedEvent); -+ break; -+ } -+ default: { -+ return; -+ } -+ } -+ } -+ -+ return -+}; -\ No newline at end of file -diff --git a/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.web.tsx b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.web.tsx -new file mode 100644 -index 0000000..7a71609 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/EmbeddedLinkView.web.tsx -@@ -0,0 +1,5 @@ -+// EmbeddedLinkView.web.tsx is a shim file which causes web bundlers to ignore the EmbeddedLinkView.tsx file -+// which imports requireNativeComponent (causing a runtime error with react-native-web). -+// Ref - https://github.com/plaid/react-native-plaid-link-sdk/issues/564 -+import React from 'react'; -+export const EmbeddedLinkView = () => null; -diff --git a/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/NativeEmbeddedLinkView.tsx b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/NativeEmbeddedLinkView.tsx -new file mode 100644 -index 0000000..da05fb1 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/EmbeddedLink/NativeEmbeddedLinkView.tsx -@@ -0,0 +1,9 @@ -+import { requireNativeComponent } from 'react-native'; -+ -+// Error "Tried to register two views with the same name PLKEmbeddedView" -+// will be thrown during hot reload when any change is made to the -+// file that is calling this requireNativeComponent('PLKEmbeddedView') call. -+// Leaving this in its own file resolves this issue. -+const NativeEmbeddedLinkView = requireNativeComponent('PLKEmbeddedView'); -+ -+export default NativeEmbeddedLinkView; -\ No newline at end of file -diff --git a/node_modules/react-native-plaid-link-sdk/src/PlaidLink.tsx b/node_modules/react-native-plaid-link-sdk/src/PlaidLink.tsx -new file mode 100644 -index 0000000..b35c06f ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/PlaidLink.tsx -@@ -0,0 +1,117 @@ -+import React, { useEffect } from 'react'; -+import { NativeEventEmitter, Platform, TouchableOpacity } from 'react-native'; -+import { -+ LinkError, -+ LinkEventListener, -+ LinkExit, -+ LinkIOSPresentationStyle, -+ LinkLogLevel, -+ LinkSuccess, -+ PlaidLinkComponentProps, -+ PlaidLinkProps, -+} from './Types'; -+import RNLinksdkAndroid from './fabric/NativePlaidLinkModuleAndroid'; -+import RNLinksdkiOS from './fabric/NativePlaidLinkModuleiOS'; -+ -+const RNLinksdk = (Platform.OS === 'android' ? RNLinksdkAndroid : RNLinksdkiOS) ?? undefined; -+ -+/** -+ * A hook that registers a listener on the Plaid emitter for the 'onEvent' type. -+ * The listener is cleaned up when this view is unmounted -+ * -+ * @param linkEventListener the listener to call -+ */ -+export const usePlaidEmitter = (linkEventListener: LinkEventListener) => { -+ useEffect(() => { -+ const emitter = new NativeEventEmitter(RNLinksdk); -+ const listener = emitter.addListener('onEvent', linkEventListener); -+ // Clean up after this effect: -+ return function cleanup() { -+ listener.remove(); -+ }; -+ }, []); -+}; -+ -+export const openLink = async (props: PlaidLinkProps) => { -+ let config = props.tokenConfig; -+ let noLoadingState = config.noLoadingState ?? false; -+ -+ if (Platform.OS === 'android') { -+ if (RNLinksdkAndroid === null) { -+ throw new Error('[react-native-plaid-link-sdk] RNLinksdkAndroid is not defined'); -+ } -+ -+ RNLinksdkAndroid.startLinkActivityForResult( -+ config.token, -+ noLoadingState, -+ config.logLevel ?? LinkLogLevel.ERROR, -+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap -+ (result: LinkSuccess) => { -+ if (props.onSuccess != null) { -+ props.onSuccess(result); -+ } -+ }, -+ (result: LinkExit) => { -+ if (props.onExit != null) { -+ if (result.error != null && result.error.displayMessage != null) { -+ //TODO(RNSDK-118): Remove errorDisplayMessage field in next major update. -+ result.error.errorDisplayMessage = result.error.displayMessage; -+ } -+ props.onExit(result); -+ } -+ }, -+ ); -+ } else { -+ if (RNLinksdkiOS === null) { -+ throw new Error('[react-native-plaid-link-sdk] RNLinksdkiOS is not defined'); -+ } -+ -+ RNLinksdkiOS.create(config.token, noLoadingState); -+ -+ let presentFullScreen = -+ props.iOSPresentationStyle == LinkIOSPresentationStyle.FULL_SCREEN; -+ -+ RNLinksdkiOS.open( -+ presentFullScreen, -+ // @ts-ignore we use Object type in the spec file as it maps to NSDictionary and ReadableMap -+ (result: LinkSuccess) => { -+ if (props.onSuccess != null) { -+ props.onSuccess(result); -+ } -+ }, -+ (error: LinkError, result: LinkExit) => { -+ if (props.onExit != null) { -+ if (error) { -+ var data = result || {}; -+ data.error = error; -+ props.onExit(data); -+ } else { -+ props.onExit(result); -+ } -+ } -+ }, -+ ); -+ } -+}; -+ -+export const dismissLink = () => { -+ if (Platform.OS === 'ios') { -+ if (RNLinksdkiOS === null) { -+ throw new Error('[react-native-plaid-link-sdk] RNLinksdkiOS is not defined'); -+ } -+ -+ RNLinksdkiOS.dismiss(); -+ } -+}; -+ -+export const PlaidLink = (props: PlaidLinkComponentProps) => { -+ function onPress() { -+ props.onPress?.(); -+ openLink(props); -+ } -+ -+ return ( -+ // @ts-ignore some types directories misconfiguration -+ {props.children} -+ ); -+}; -diff --git a/node_modules/react-native-plaid-link-sdk/src/Types.ts b/node_modules/react-native-plaid-link-sdk/src/Types.ts -new file mode 100644 -index 0000000..a7d30c6 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/Types.ts -@@ -0,0 +1,550 @@ -+interface CommonPlaidLinkOptions { -+ logLevel?: LinkLogLevel; -+ extras?: Record; -+} -+ -+export type LinkTokenConfiguration = (CommonPlaidLinkOptions & { -+ token: string; -+ // A `Bool` indicating that Link should skip displaying a loading animation until the Link UI is fully loaded. -+ // This can be used to display custom loading UI while Link content is loading (and will skip any initial loading UI in Link). -+ // Note: Dismiss custom loading UI on the OPEN & EXIT events. -+ // -+ // Note: This should be set to `true` when setting the `eu_config.headless` field in /link/token/create requests to `true`. -+ // For reference, see https://plaid.com/docs/api/tokens/#link-token-create-request-eu-config-headless -+ noLoadingState?: boolean; -+}); -+ -+export enum LinkLogLevel { -+ DEBUG="debug", -+ INFO="info", -+ WARN="warn", -+ ERROR="error", -+} -+ -+export enum PlaidEnvironment { -+ PRODUCTION = 'production', -+ DEVELOPMENT = 'development', -+ SANDBOX = 'sandbox', -+} -+ -+export enum PlaidProduct { -+ ASSETS="assets", -+ AUTH="auth", -+ DEPOSIT_SWITCH="deposit_switch", -+ IDENTITY="identity", -+ INCOME="income", -+ INVESTMENTS="investments", -+ LIABILITIES="liabilities", -+ LIABILITIES_REPORT="liabilities_report", -+ PAYMENT_INITIATION="payment_initiation", -+ TRANSACTIONS="transactions", -+} -+ -+export enum LinkAccountType { -+ CREDIT = 'credit', -+ DEPOSITORY = 'depository', -+ INVESTMENT = 'investment', -+ LOAN = 'loan', -+ OTHER = 'other', -+} -+ -+export enum LinkAccountSubtypes { -+ ALL = 'all', -+ CREDIT_CARD = 'credit card', -+ PAYPAL = 'paypal', -+ AUTO = 'auto', -+ BUSINESS = 'business', -+ COMMERCIAL = 'commercial', -+ CONSTRUCTION = 'construction', -+ CONSUMER = 'consumer', -+ HOME_EQUITY = 'home equity', -+ LINE_OF_CREDIT = 'line of credit', -+ LOAN = 'loan', -+ MORTGAGE = 'mortgage', -+ OVERDRAFT = 'overdraft', -+ STUDENT = 'student', -+ CASH_MANAGEMENT = 'cash management', -+ CD = 'cd', -+ CHECKING = 'checking', -+ EBT = 'ebt', -+ HSA = 'hsa', -+ MONEY_MARKET = 'money market', -+ PREPAID = 'prepaid', -+ SAVINGS = 'savings', -+ FOUR_0_1_A = '401a', -+ FOUR_0_1_K = '401k', -+ FOUR_0_3_B = '403B', -+ FOUR_5_7_B = '457b', -+ FIVE_2_9 = '529', -+ BROKERAGE = 'brokerage', -+ CASH_ISA = 'cash isa', -+ EDUCATION_SAVINGS_ACCOUNT = 'education savings account', -+ FIXED_ANNUNITY = 'fixed annuity', -+ GIC = 'gic', -+ HEALTH_REIMBURSEMENT_ARRANGEMENT = 'health reimbursement arrangement', -+ IRA = 'ira', -+ ISA = 'isa', -+ KEOGH = 'keogh', -+ LIF = 'lif', -+ LIRA = 'lira', -+ LRIF = 'lrif', -+ LRSP = 'lrsp', -+ MUTUAL_FUND = 'mutual fund', -+ NON_TAXABLE_BROKERAGE_ACCOUNT = 'non-taxable brokerage account', -+ PENSION = 'pension', -+ PLAN = 'plan', -+ PRIF = 'prif', -+ PROFIT_SHARING_PLAN = 'profit sharing plan', -+ RDSP = 'rdsp', -+ RESP = 'resp', -+ RETIREMENT = 'retirement', -+ RLIF = 'rlif', -+ ROTH_401K = 'roth 401k', -+ ROTH = 'roth', -+ RRIF = 'rrif', -+ RRSP = 'rrsp', -+ SARSEP = 'sarsep', -+ SEP_IRA = 'sep ira', -+ SIMPLE_IRA = 'simple ira', -+ SIPP = 'sipp', -+ STOCK_PLAN = 'stock plan', -+ TFSA = 'tfsa', -+ THRIFT_SAVINGS_PLAN = 'thrift savings plan', -+ TRUST = 'trust', -+ UGMA = 'ugma', -+ UTMA = 'utma', -+ VARIABLE_ANNUITY = 'variable annuity' -+} -+ -+export interface LinkAccountSubtype { -+} -+ -+export class LinkAccountSubtypeCredit implements LinkAccountSubtype { -+ public static readonly ALL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL); -+ public static readonly CREDIT_CARD = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.CREDIT_CARD); -+ public static readonly PAYPAL = new LinkAccountSubtypeCredit(LinkAccountType.CREDIT, LinkAccountSubtypes.PAYPAL); -+ -+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { } -+} -+ -+export class LinkAccountSubtypeDepository implements LinkAccountSubtype { -+ public static readonly ALL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.ALL); -+ public static readonly CASH_MANAGEMENT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CASH_MANAGEMENT); -+ public static readonly CD = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CD); -+ public static readonly CHECKING = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.CHECKING); -+ public static readonly EBT = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.EBT); -+ public static readonly HSA = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.HSA); -+ public static readonly MONEY_MARKET = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.MONEY_MARKET); -+ public static readonly PAYPAL = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PAYPAL); -+ public static readonly PREPAID = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.PREPAID); -+ public static readonly SAVINGS = new LinkAccountSubtypeDepository(LinkAccountType.DEPOSITORY, LinkAccountSubtypes.SAVINGS); -+ -+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { } -+} -+ -+export class LinkAccountSubtypeInvestment implements LinkAccountSubtype { -+ public static readonly ALL = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ALL); -+ public static readonly BROKERAGE = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.BROKERAGE); -+ public static readonly CASH_ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.CASH_ISA); -+ public static readonly EDUCATION_SAVINGS_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.EDUCATION_SAVINGS_ACCOUNT); -+ public static readonly FIXED_ANNUNITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIXED_ANNUNITY); -+ public static readonly GIC = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.GIC); -+ public static readonly HEALTH_REIMBURSEMENT_ARRANGEMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HEALTH_REIMBURSEMENT_ARRANGEMENT); -+ public static readonly HSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.HSA); -+ public static readonly INVESTMENT_401A = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_A); -+ public static readonly INVESTMENT_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_1_K); -+ public static readonly INVESTMENT_403B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_0_3_B); -+ public static readonly INVESTMENT_457B = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FOUR_5_7_B); -+ public static readonly INVESTMENT_529 = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.FIVE_2_9); -+ public static readonly IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.IRA); -+ public static readonly ISA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ISA); -+ public static readonly KEOGH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.KEOGH); -+ public static readonly LIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIF); -+ public static readonly LIRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LIRA); -+ public static readonly LRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRIF); -+ public static readonly LRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.LRSP); -+ public static readonly MUTUAL_FUND = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.MUTUAL_FUND); -+ public static readonly NON_TAXABLE_BROKERAGE_ACCOUNT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.NON_TAXABLE_BROKERAGE_ACCOUNT); -+ public static readonly PENSION = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PENSION); -+ public static readonly PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PLAN); -+ public static readonly PRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PRIF); -+ public static readonly PROFIT_SHARING_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.PROFIT_SHARING_PLAN); -+ public static readonly RDSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RDSP); -+ public static readonly RESP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RESP); -+ public static readonly RETIREMENT = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RETIREMENT); -+ public static readonly RLIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RLIF); -+ public static readonly ROTH = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH); -+ public static readonly ROTH_401K = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.ROTH_401K); -+ public static readonly RRIF = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRIF); -+ public static readonly RRSP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.RRSP); -+ public static readonly SARSEP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SARSEP); -+ public static readonly SEP_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SEP_IRA); -+ public static readonly SIMPLE_IRA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIMPLE_IRA); -+ public static readonly SIIP = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.SIPP); -+ public static readonly STOCK_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.STOCK_PLAN); -+ public static readonly TFSA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TFSA); -+ public static readonly THRIFT_SAVINGS_PLAN = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.THRIFT_SAVINGS_PLAN); -+ public static readonly TRUST = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.TRUST); -+ public static readonly UGMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UGMA); -+ public static readonly UTMA = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.UTMA); -+ public static readonly VARIABLE_ANNUITY = new LinkAccountSubtypeInvestment(LinkAccountType.INVESTMENT, LinkAccountSubtypes.VARIABLE_ANNUITY); -+ -+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { } -+} -+ -+export class LinkAccountSubtypeLoan implements LinkAccountSubtype { -+ public static readonly ALL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.ALL); -+ public static readonly AUTO = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.AUTO); -+ public static readonly BUSINESS = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.BUSINESS); -+ public static readonly COMMERCIAL = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.COMMERCIAL); -+ public static readonly CONSTRUCTION = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSTRUCTION); -+ public static readonly CONSUMER = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.CONSUMER); -+ public static readonly HOME_EQUITY = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.HOME_EQUITY); -+ public static readonly LINE_OF_CREDIT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LINE_OF_CREDIT); -+ public static readonly LOAN = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.LOAN); -+ public static readonly MORTGAGE = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.MORTGAGE); -+ public static readonly OVERDRAFT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.OVERDRAFT); -+ public static readonly STUDENT = new LinkAccountSubtypeLoan(LinkAccountType.CREDIT, LinkAccountSubtypes.STUDENT); -+ -+ private constructor(public readonly type: LinkAccountType, public readonly subtype: LinkAccountSubtype) { } -+} -+ -+export class LinkAccountSubtypeUnknown implements LinkAccountSubtype { -+ constructor(public readonly type: string, public readonly subtype: string) { } -+} -+ -+export interface LinkSuccess { -+ publicToken: string; -+ metadata: LinkSuccessMetadata; -+} -+ -+export interface LinkSuccessMetadata { -+ institution?: LinkInstitution; -+ accounts: LinkAccount[]; -+ linkSessionId: string; -+ metadataJson?: string; -+} -+ -+export interface LinkAccount { -+ id: string; -+ name?: string; -+ mask?: string; -+ type: LinkAccountType; -+ subtype: LinkAccountSubtype; -+ verificationStatus?: LinkAccountVerificationStatus; -+} -+ -+export enum LinkAccountVerificationStatus { -+ PENDING_AUTOMATIC_VERIFICATION = 'pending_automatic_verification', -+ PENDING_MANUAL_VERIFICATION = 'pending_manual_verification', -+ MANUALLY_VERIFIED = 'manually_verified', -+} -+ -+export interface LinkInstitution { -+ id: string; -+ name: string; -+} -+ -+export interface LinkExit { -+ error?: LinkError; -+ metadata: LinkExitMetadata; -+} -+ -+export interface LinkExitMetadata { -+ status?: LinkExitMetadataStatus; -+ institution?: LinkInstitution; -+ linkSessionId: string; -+ requestId: string; -+ metadataJson?: string; -+} -+ -+export enum LinkExitMetadataStatus { -+ CONNECTED = 'connected', -+ CHOOSE_DEVICE = 'choose_device', -+ REQUIRES_ACCOUNT_SELECTION = 'requires_account_selection', -+ REQUIRES_CODE = 'requires_code', -+ REQUIRES_CREDENTIALS = 'requires_credentials', -+ REQUIRES_EXTERNAL_ACTION = 'requires_external_action', -+ REQUIRES_OAUTH = 'requires_oauth', -+ REQUIRES_QUESTIONS = 'requires_questions', -+ REQUIRES_RECAPTCHA = 'requires_recaptcha', -+ REQUIRES_SELECTIONS = 'requires_selections', -+ REQUIRES_DEPOSIT_SWITCH_ALLOCATION_CONFIGURATION = 'requires_deposit_switch_allocation_configuration', -+ REQUIRES_DEPOSIT_SWITCH_ALLOCATION_SELECTION = 'requires_deposit_switch_allocation_selection', -+} -+ -+export interface LinkError { -+ errorCode: LinkErrorCode; -+ errorType: LinkErrorType; -+ errorMessage: string; -+ /** @deprecated DO NOT USE, data not guaranteed. Use `displayMessage` instead */ -+ errorDisplayMessage?: string; -+ displayMessage?: string; -+ errorJson?: string; -+} -+ -+export enum LinkErrorCode { -+ // ITEM_ERROR -+ INVALID_CREDENTIALS = "INVALID_CREDENTIALS", -+ INVALID_MFA = "INVALID_MFA", -+ ITEM_LOGIN_REQUIRED = "ITEM_LOGIN_REQUIRED", -+ INSUFFICIENT_CREDENTIALS = "INSUFFICIENT_CREDENTIALS", -+ ITEM_LOCKED = "ITEM_LOCKED", -+ USER_SETUP_REQUIRED = "USER_SETUP_REQUIRED", -+ MFA_NOT_SUPPORTED = "MFA_NOT_SUPPORTED", -+ INVALID_SEND_METHOD = "INVALID_SEND_METHOD", -+ NO_ACCOUNTS = "NO_ACCOUNTS", -+ ITEM_NOT_SUPPORTED = "ITEM_NOT_SUPPORTED", -+ TOO_MANY_VERIFICATION_ATTEMPTS = "TOO_MANY_VERIFICATION_ATTEMPTS", -+ -+ INVALD_UPDATED_USERNAME = "INVALD_UPDATED_USERNAME", -+ INVALID_UPDATED_USERNAME = "INVALID_UPDATED_USERNAME", -+ -+ ITEM_NO_ERROR = "ITEM_NO_ERROR", -+ item_no_error = "item-no-error", -+ NO_AUTH_ACCOUNTS = "NO_AUTH_ACCOUNTS", -+ NO_INVESTMENT_ACCOUNTS = "NO_INVESTMENT_ACCOUNTS", -+ NO_INVESTMENT_AUTH_ACCOUNTS = "NO_INVESTMENT_AUTH_ACCOUNTS", -+ NO_LIABILITY_ACCOUNTS = "NO_LIABILITY_ACCOUNTS", -+ PRODUCTS_NOT_SUPPORTED = "PRODUCTS_NOT_SUPPORTED", -+ ITEM_NOT_FOUND = "ITEM_NOT_FOUND", -+ ITEM_PRODUCT_NOT_READY = "ITEM_PRODUCT_NOT_READY", -+ -+ // INSTITUTION_ERROR -+ INSTITUTION_DOWN = "INSTITUTION_DOWN", -+ INSTITUTION_NOT_RESPONDING = "INSTITUTION_NOT_RESPONDING", -+ INSTITUTION_NOT_AVAILABLE = "INSTITUTION_NOT_AVAILABLE", -+ INSTITUTION_NO_LONGER_SUPPORTED = "INSTITUTION_NO_LONGER_SUPPORTED", -+ -+ // API_ERROR -+ INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR", -+ PLANNED_MAINTENANCE = "PLANNED_MAINTENANCE", -+ -+ // ASSET_REPORT_ERROR -+ PRODUCT_NOT_ENABLED = "PRODUCT_NOT_ENABLED", -+ DATA_UNAVAILABLE = "DATA_UNAVAILABLE", -+ ASSET_PRODUCT_NOT_READY = "ASSET_PRODUCT_NOT_READY", -+ ASSET_REPORT_GENERATION_FAILED = "ASSET_REPORT_GENERATION_FAILED", -+ INVALID_PARENT = "INVALID_PARENT", -+ INSIGHTS_NOT_ENABLED = "INSIGHTS_NOT_ENABLED", -+ INSIGHTS_PREVIOUSLY_NOT_ENABLED = "INSIGHTS_PREVIOUSLY_NOT_ENABLED", -+ -+ // BANK_TRANSFER_ERROR -+ BANK_TRANSFER_LIMIT_EXCEEDED = "BANK_TRANSFER_LIMIT_EXCEEDED", -+ BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT = "BANK_TRANSFER_MISSING_ORIGINATION_ACCOUNT", -+ BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT = "BANK_TRANSFER_INVALID_ORIGINATION_ACCOUNT", -+ BANK_TRANSFER_ACCOUNT_BLOCKED = "BANK_TRANSFER_ACCOUNT_BLOCKED", -+ BANK_TRANSFER_INSUFFICIENT_FUNDS = "BANK_TRANSFER_INSUFFICIENT_FUNDS", -+ BANK_TRANSFER_NOT_CANCELLABLE = "BANK_TRANSFER_NOT_CANCELLABLE", -+ BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE = "BANK_TRANSFER_UNSUPPORTED_ACCOUNT_TYPE", -+ BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT = "BANK_TRANSFER_UNSUPPORTED_ENVIRONMENT", -+ -+ // SANDBOX_ERROR -+ SANDBOX_PRODUCT_NOT_ENABLED = "SANDBOX_PRODUCT_NOT_ENABLED", -+ SANDBOX_WEBHOOK_INVALID = "SANDBOX_WEBHOOK_INVALID", -+ SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID = "SANDBOX_BANK_TRANSFER_EVENT_TRANSITION_INVALID", -+ -+ // INVALID_REQUEST -+ MISSING_FIELDS = "MISSING_FIELDS", -+ UNKNOWN_FIELDS = "UNKNOWN_FIELDS", -+ INVALID_FIELD = "INVALID_FIELD", -+ INCOMPATIBLE_API_VERSION = "INCOMPATIBLE_API_VERSION", -+ INVALID_BODY = "INVALID_BODY", -+ INVALID_HEADERS = "INVALID_HEADERS", -+ NOT_FOUND = "NOT_FOUND", -+ NO_LONGER_AVAILABLE = "NO_LONGER_AVAILABLE", -+ SANDBOX_ONLY = "SANDBOX_ONLY", -+ INVALID_ACCOUNT_NUMBER = "INVALID_ACCOUNT_NUMBER", -+ -+ // INVALID_INPUT -+ // From above ITEM_LOGIN_REQUIRED = "INVALID_CREDENTIALS", -+ INCORRECT_DEPOSIT_AMOUNTS = "INCORRECT_DEPOSIT_AMOUNTS", -+ UNAUTHORIZED_ENVIRONMENT = "UNAUTHORIZED_ENVIRONMENT", -+ INVALID_PRODUCT = "INVALID_PRODUCT", -+ UNAUTHORIZED_ROUTE_ACCESS = "UNAUTHORIZED_ROUTE_ACCESS", -+ DIRECT_INTEGRATION_NOT_ENABLED = "DIRECT_INTEGRATION_NOT_ENABLED", -+ INVALID_API_KEYS = "INVALID_API_KEYS", -+ INVALID_ACCESS_TOKEN = "INVALID_ACCESS_TOKEN", -+ INVALID_PUBLIC_TOKEN = "INVALID_PUBLIC_TOKEN", -+ INVALID_LINK_TOKEN = "INVALID_LINK_TOKEN", -+ INVALID_PROCESSOR_TOKEN = "INVALID_PROCESSOR_TOKEN", -+ INVALID_AUDIT_COPY_TOKEN = "INVALID_AUDIT_COPY_TOKEN", -+ INVALID_ACCOUNT_ID = "INVALID_ACCOUNT_ID", -+ MICRODEPOSITS_ALREADY_VERIFIED = "MICRODEPOSITS_ALREADY_VERIFIED", -+ -+ // INVALID_RESULT -+ PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA = "PLAID_DIRECT_ITEM_IMPORT_RETURNED_INVALID_MFA", -+ -+ // RATE_LIMIT_EXCEEDED -+ ACCOUNTS_LIMIT = "ACCOUNTS_LIMIT", -+ ADDITION_LIMIT = "ADDITION_LIMIT", -+ AUTH_LIMIT = "AUTH_LIMIT", -+ BALANCE_LIMIT = "BALANCE_LIMIT", -+ IDENTITY_LIMIT = "IDENTITY_LIMIT", -+ ITEM_GET_LIMIT = "ITEM_GET_LIMIT", -+ RATE_LIMIT = "RATE_LIMIT", -+ TRANSACTIONS_LIMIT = "TRANSACTIONS_LIMIT", -+ -+ // RECAPTCHA_ERROR -+ RECAPTCHA_REQUIRED = "RECAPTCHA_REQUIRED", -+ RECAPTCHA_BAD = "RECAPTCHA_BAD", -+ -+ // OAUTH_ERROR -+ INCORRECT_OAUTH_NONCE = "INCORRECT_OAUTH_NONCE", -+ OAUTH_STATE_ID_ALREADY_PROCESSED = "OAUTH_STATE_ID_ALREADY_PROCESSED", -+} -+ -+export enum LinkErrorType { -+ BANK_TRANSFER = 'BANK_TRANSFER_ERROR', -+ INVALID_REQUEST = 'INVALID_REQUEST', -+ INVALID_RESULT = 'INVALID_RESULT', -+ INVALID_INPUT = 'INVALID_INPUT', -+ INSTITUTION_ERROR = 'INSTITUTION_ERROR', -+ RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', -+ API_ERROR = 'API_ERROR', -+ ITEM_ERROR = 'ITEM_ERROR', -+ AUTH_ERROR = 'AUTH_ERROR', -+ ASSET_REPORT_ERROR = 'ASSET_REPORT_ERROR', -+ SANDBOX_ERROR = 'SANDBOX_ERROR', -+ RECAPTCHA_ERROR = 'RECAPTCHA_ERROR', -+ OAUTH_ERROR = 'OAUTH_ERROR', -+} -+ -+export type LinkEventListener = (linkEvent: LinkEvent) => void -+ -+export interface LinkEvent { -+ eventName: LinkEventName; -+ metadata: LinkEventMetadata; -+} -+ -+export interface LinkEventMetadata { -+ accountNumberMask?: string; -+ linkSessionId: string; -+ mfaType?: string; -+ requestId?: string; -+ viewName: LinkEventViewName; -+ errorCode?: string; -+ errorMessage?: string; -+ errorType?: string; -+ exitStatus?: string; -+ institutionId?: string; -+ institutionName?: string; -+ institutionSearchQuery?: string; -+ isUpdateMode?: string; -+ matchReason?: string; -+ // see possible values for selection at https://plaid.com/docs/link/web/#link-web-onevent-selection -+ selection?: null | string; -+ timestamp: string; -+} -+ -+export enum LinkEventName { -+ BANK_INCOME_INSIGHTS_COMPLETED = "BANK_INCOME_INSIGHTS_COMPLETED", -+ CLOSE_OAUTH = 'CLOSE_OAUTH', -+ ERROR = 'ERROR', -+ EXIT = 'EXIT', -+ FAIL_OAUTH = 'FAIL_OAUTH', -+ HANDOFF = 'HANDOFF', -+ IDENTITY_VERIFICATION_START_STEP = 'IDENTITY_VERIFICATION_START_STEP', -+ IDENTITY_VERIFICATION_PASS_STEP = 'IDENTITY_VERIFICATION_PASS_STEP', -+ IDENTITY_VERIFICATION_FAIL_STEP = 'IDENTITY_VERIFICATION_FAIL_STEP', -+ IDENTITY_VERIFICATION_PENDING_REVIEW_STEP = 'IDENTITY_VERIFICATION_PENDING_REVIEW_STEP', -+ IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION = 'IDENTITY_VERIFICATION_PENDING_REVIEW_SESSION', -+ IDENTITY_VERIFICATION_CREATE_SESSION = 'IDENTITY_VERIFICATION_CREATE_SESSION', -+ IDENTITY_VERIFICATION_RESUME_SESSION = 'IDENTITY_VERIFICATION_RESUME_SESSION', -+ IDENTITY_VERIFICATION_PASS_SESSION = 'IDENTITY_VERIFICATION_PASS_SESSION', -+ IDENTITY_VERIFICATION_FAIL_SESSION = 'IDENTITY_VERIFICATION_FAIL_SESSION', -+ IDENTITY_VERIFICATION_OPEN_UI = 'IDENTITY_VERIFICATION_OPEN_UI', -+ IDENTITY_VERIFICATION_RESUME_UI = 'IDENTITY_VERIFICATION_RESUME_UI', -+ IDENTITY_VERIFICATION_CLOSE_UI = 'IDENTITY_VERIFICATION_CLOSE_UI', -+ MATCHED_CONSENT = 'MATCHED_CONSENT', -+ MATCHED_SELECT_INSTITUTION = 'MATCHED_SELECT_INSTITUTION', -+ MATCHED_SELECT_VERIFY_METHOD = 'MATCHED_SELECT_VERIFY_METHOD', -+ OPEN = 'OPEN', -+ OPEN_MY_PLAID = 'OPEN_MY_PLAID', -+ OPEN_OAUTH = 'OPEN_OAUTH', -+ SEARCH_INSTITUTION = 'SEARCH_INSTITUTION', -+ SELECT_DEGRADED_INSTITUTION = 'SELECT_DEGRADED_INSTITUTION', -+ SELECT_DOWN_INSTITUTION = 'SELECT_DOWN_INSTITUTION', -+ SELECT_FILTERED_INSTITUTION = 'SELECT_FILTERED_INSTITUTION', -+ SELECT_INSTITUTION = 'SELECT_INSTITUTION', -+ SELECT_BRAND = 'SELECT_BRAND', -+ SELECT_AUTH_TYPE = 'SELECT_AUTH_TYPE', -+ SUBMIT_ACCOUNT_NUMBER = 'SUBMIT_ACCOUNT_NUMBER', -+ SUBMIT_DOCUMENTS = 'SUBMIT_DOCUMENTS', -+ SUBMIT_DOCUMENTS_SUCCESS = 'SUBMIT_DOCUMENTS_SUCCESS', -+ SUBMIT_DOCUMENTS_ERROR = 'SUBMIT_DOCUMENTS_ERROR', -+ SUBMIT_ROUTING_NUMBER = 'SUBMIT_ROUTING_NUMBER', -+ VIEW_DATA_TYPES = 'VIEW_DATA_TYPES', -+ SUBMIT_PHONE = 'SUBMIT_PHONE', -+ SKIP_SUBMIT_PHONE = 'SKIP_SUBMIT_PHONE', -+ VERIFY_PHONE = 'VERIFY_PHONE', -+ SUBMIT_CREDENTIALS = 'SUBMIT_CREDENTIALS', -+ SUBMIT_MFA = 'SUBMIT_MFA', -+ TRANSITION_VIEW = 'TRANSITION_VIEW', -+ CONNECT_NEW_INSTITUTION = 'CONNECT_NEW_INSTITUTION', -+} -+ -+export enum LinkEventViewName { -+ ACCEPT_TOS = 'ACCEPT_TOS', -+ CONNECTED = 'CONNECTED', -+ CONSENT = 'CONSENT', -+ CREDENTIAL = 'CREDENTIAL', -+ DATA_TRANSPARENCY = 'DATA_TRANSPARENCY', -+ DATA_TRANSPARENCY_CONSENT = 'DATA_TRANSPARENCY_CONSENT', -+ DOCUMENTARY_VERIFICATION = 'DOCUMENTARY_VERIFICATION', -+ ERROR = 'ERROR', -+ EXIT = 'EXIT', -+ KYC_CHECK = 'KYC_CHECK', -+ SELFIE_CHECK = 'SELFIE_CHECK', -+ LOADING = 'LOADING', -+ MATCHED_CONSENT = 'MATCHED_CONSENT', -+ MATCHED_CREDENTIAL = 'MATCHED_CREDENTIAL', -+ MATCHED_MFA = 'MATCHED_MFA', -+ MFA = 'MFA', -+ NUMBERS = 'NUMBERS', -+ NUMBERS_SELECT_INSTITUTION = 'NUMBERS_SELECT_INSTITUTION', -+ OAUTH = 'OAUTH', -+ RECAPTCHA = 'RECAPTCHA', -+ RISK_CHECK = 'RISK_CHECK', -+ SCREENING = 'SCREENING', -+ SELECT_ACCOUNT = 'SELECT_ACCOUNT', -+ SELECT_AUTH_TYPE = 'SELECT_AUTH_TYPE', -+ SUBMIT_PHONE = 'SUBMIT_PHONE', -+ VERIFY_PHONE = 'VERIFY_PHONE', -+ SELECT_SAVED_INSTITUTION = 'SELECT_SAVED_INSTITUTION', -+ SELECT_SAVED_ACCOUNT = 'SELECT_SAVED_ACCOUNT', -+ SELECT_BRAND = 'SELECT_BRAND', -+ SELECT_INSTITUTION = 'SELECT_INSTITUTION', -+ SUBMIT_DOCUMENTS = 'SUBMIT_DOCUMENTS', -+ SUBMIT_DOCUMENTS_SUCCESS = 'SUBMIT_DOCUMENTS_SUCCESS', -+ SUBMIT_DOCUMENTS_ERROR = 'SUBMIT_DOCUMENTS_ERROR', -+ UPLOAD_DOCUMENTS = 'UPLOAD_DOCUMENTS', -+ VERIFY_SMS = 'VERIFY_SMS', -+} -+ -+/// Methods to present Link on iOS. -+/// FULL_SCREEN is the converts to UIModalPresentationOverFullScreen on the native side. -+/// MODAL will use the default presentation style for iOS which is UIModalPresentationAutomatic. -+export enum LinkIOSPresentationStyle { -+ FULL_SCREEN = 'FULL_SCREEN', -+ MODAL = 'MODAL' -+} -+ -+export type LinkSuccessListener = (LinkSuccess: LinkSuccess) => void -+ -+export type LinkExitListener = (LinkExit: LinkExit) => void -+ -+export type LinkOnEventListener = (LinkEvent: LinkEvent) => void -+ -+export interface PlaidLinkProps { -+ tokenConfig: LinkTokenConfiguration -+ onSuccess: LinkSuccessListener -+ onExit?: LinkExitListener -+ iOSPresentationStyle?: LinkIOSPresentationStyle -+ logLevel?: LinkLogLevel -+ onPress?(): any -+} -+ -+export type PlaidLinkComponentProps = (PlaidLinkProps & { -+ children: React.ReactNode -+}); -diff --git a/node_modules/react-native-plaid-link-sdk/src/__tests__/Types.tests.ts b/node_modules/react-native-plaid-link-sdk/src/__tests__/Types.tests.ts -new file mode 100644 -index 0000000..2dd7a54 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/__tests__/Types.tests.ts -@@ -0,0 +1,15 @@ -+const Types = require('./../Types'); -+ -+test('test token configuration', () => { -+ const linkTokenConfiguration = { -+ token: "test-token", -+ noLoadingState: false, -+ logLevel: Types.LinkLogLevel.DEBUG, -+ extras: null, -+ }; -+ -+ expect(linkTokenConfiguration.noLoadingState).toBe(false); -+ expect(linkTokenConfiguration.token).toBe("test-token"); -+ expect(linkTokenConfiguration.logLevel).toBe(Types.LinkLogLevel.DEBUG); -+ expect(linkTokenConfiguration.extras).toBe(null); -+}); -\ No newline at end of file -diff --git a/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleAndroid.ts b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleAndroid.ts -new file mode 100644 -index 0000000..d1e4062 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleAndroid.ts -@@ -0,0 +1,20 @@ -+// we use Object type because methods on the native side use NSDictionary and ReadableMap -+// and we want to stay compatible with those -+import {TurboModuleRegistry, TurboModule} from 'react-native'; -+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; -+ -+export interface Spec extends TurboModule { -+ startLinkActivityForResult( -+ token: string, -+ noLoadingState: boolean, -+ logLevel: string, -+ onSuccessCallback: (result: UnsafeObject) => void, -+ onExitCallback: (result: UnsafeObject) => void -+ ): void; -+ -+ // those two are here for event emitter methods -+ addListener(eventName: string): void; -+ removeListeners(count: Int32): void; -+} -+ -+export default TurboModuleRegistry.get('PlaidAndroid'); -diff --git a/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts -new file mode 100644 -index 0000000..d1b3565 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/fabric/NativePlaidLinkModuleiOS.ts -@@ -0,0 +1,19 @@ -+// we use Object type because methods on the native side use NSDictionary and ReadableMap -+// and we want to stay compatible with those -+import {TurboModuleRegistry, TurboModule} from 'react-native'; -+import { Int32, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; -+ -+export interface Spec extends TurboModule { -+ create(token: string, noLoadingState: boolean): void; -+ open( -+ fullScreen: boolean, -+ onSuccess: (success: UnsafeObject) => void, -+ onExit: (error: UnsafeObject, result: UnsafeObject) => void, -+ ): void; -+ dismiss(): void; -+ // those two are here for event emitter methods -+ addListener(eventName: string): void; -+ removeListeners(count: Int32): void; -+} -+ -+export default TurboModuleRegistry.get('RNLinksdk'); -diff --git a/node_modules/react-native-plaid-link-sdk/src/index.ts b/node_modules/react-native-plaid-link-sdk/src/index.ts -new file mode 100644 -index 0000000..23ef946 ---- /dev/null -+++ b/node_modules/react-native-plaid-link-sdk/src/index.ts -@@ -0,0 +1,21 @@ -+import { -+ openLink, -+ dismissLink, -+ usePlaidEmitter, -+ PlaidLink, -+} from './PlaidLink'; -+ -+export * from './Types'; -+ -+export default PlaidLink; -+ -+export { -+ PlaidLink, -+ openLink, -+ dismissLink, -+ usePlaidEmitter, -+}; -+ -+// Components -+ -+export { EmbeddedLinkView } from './EmbeddedLink/EmbeddedLinkView'; -\ No newline at end of file diff --git a/patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.8.1+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.8.1+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.7.2+002+copy-state.patch b/patches/react-native-reanimated+3.8.1+002+copy-state.patch similarity index 100% rename from patches/react-native-reanimated+3.7.2+002+copy-state.patch rename to patches/react-native-reanimated+3.8.1+002+copy-state.patch diff --git a/patches/react-native-reanimated+3.7.2.patch b/patches/react-native-reanimated+3.8.1.patch similarity index 100% rename from patches/react-native-reanimated+3.7.2.patch rename to patches/react-native-reanimated+3.8.1.patch 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 deleted file mode 100644 index f282ec58b07b..000000000000 --- a/patches/react-native-screens+3.30.1+001+fix-screen-type.patch +++ /dev/null @@ -1,12 +0,0 @@ -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-web+0.19.12+003+image-header-support.patch b/patches/react-native-web+0.19.12+003+image-header-support.patch index 6652f0345cc4..d0a490a4ed70 100644 --- a/patches/react-native-web+0.19.12+003+image-header-support.patch +++ b/patches/react-native-web+0.19.12+003+image-header-support.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 9649d27..3281cc8 100644 +index 9649d27..66ef95c 100644 --- a/node_modules/react-native-web/dist/exports/Image/index.js +++ b/node_modules/react-native-web/dist/exports/Image/index.js @@ -135,7 +135,22 @@ function resolveAssetUri(source) { @@ -47,7 +47,7 @@ index 9649d27..3281cc8 100644 }); } function abortPendingRequest() { -@@ -279,10 +288,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -279,10 +288,79 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { suppressHydrationWarning: true }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); }); @@ -64,24 +64,20 @@ index 9649d27..3281cc8 100644 + var _React$useState3 = React.useState(''), + blobUri = _React$useState3[0], + setBlobUri = _React$useState3[1]; -+ var request = React.useRef({ -+ cancel: () => {}, -+ source: { -+ uri: '', -+ headers: {} -+ }, -+ promise: Promise.resolve('') -+ }); ++ var request = React.useRef(null); + var onError = props.onError, + onLoadStart = props.onLoadStart, + onLoadEnd = props.onLoadEnd; + React.useEffect(() => { -+ if (!hasSourceDiff(nextSource, request.current.source)) { ++ if (request.current !== null && !hasSourceDiff(nextSource, request.current.source)) { + return; + } + + // When source changes we want to clean up any old/running requests -+ request.current.cancel(); ++ if (request.current !== null) { ++ request.current.cancel(); ++ } ++ + if (onLoadStart) { + onLoadStart(); + } @@ -96,7 +92,12 @@ index 9649d27..3281cc8 100644 + }, [nextSource, onLoadStart, onError, onLoadEnd]); + + // Cancel any request on unmount -+ React.useEffect(() => request.current.cancel, []); ++ React.useEffect(() => () => { ++ if (request.current !== null) { ++ request.current.cancel(); ++ request.current = null; ++ } ++ }, []); + var propsToPass = _objectSpread(_objectSpread({}, props), {}, { + // `onLoadStart` is called from the current component + // We skip passing it down to prevent BaseImage raising it a 2nd time diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh new file mode 100755 index 000000000000..4ce023755258 --- /dev/null +++ b/scripts/applyPatches.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# This script is a simple wrapper around patch-package that fails if any errors or warnings are detected. +# This is useful because patch-package does not fail on errors or warnings by default, +# which means that broken patches are easy to miss, and leads to developer frustration and wasted time. + +SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}") +source "$SCRIPTS_DIR/shellUtils.sh" + +# Wrapper to run patch-package. +function patchPackage { + OS="$(uname)" + if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then + npx patch-package --error-on-fail --color=always + else + error "Unsupported OS: $OS" + exit 1 + fi +} + +# Run patch-package and capture its output and exit code, while still displaying the original output to the terminal +TEMP_OUTPUT="$(mktemp)" +patchPackage 2>&1 | tee "$TEMP_OUTPUT" +EXIT_CODE=${PIPESTATUS[0]} +OUTPUT="$(cat "$TEMP_OUTPUT")" +rm -f "$TEMP_OUTPUT" + +# Check if the output contains a warning message +echo "$OUTPUT" | grep -q "Warning:" +WARNING_FOUND=$? + +printf "\n" + +# Determine the final exit code +if [ "$EXIT_CODE" -eq 0 ]; then + if [ $WARNING_FOUND -eq 0 ]; then + # patch-package succeeded but warning was found + error "It looks like you upgraded a dependency without upgrading the patch. Please review the patch, determine if it's still needed, and port it to the new version of the dependency." + exit 1 + else + # patch-package succeeded and no warning was found + success "patch-package succeeded without errors or warnings" + exit 0 + fi +else + # patch-package failed + error "patch-package failed to apply a patch" + exit "$EXIT_CODE" +fi diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index 339fdf25cb10..782c8ef5822c 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -1,11 +1,14 @@ #!/bin/bash +# Exit immediately if any command exits with a non-zero status +set -e + # Go to project root ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)") cd "$ROOT_DIR" || exit 1 -# Run patch-package -npx patch-package +# Apply packages using patch-package +scripts/applyPatches.sh # Install node_modules in subpackages, unless we're in a CI/CD environment, # where the node_modules for subpackages are cached separately. diff --git a/scripts/symbolicate-profile.ts b/scripts/symbolicate-profile.ts index a100c05029dd..a58c2894edb6 100755 --- a/scripts/symbolicate-profile.ts +++ b/scripts/symbolicate-profile.ts @@ -32,7 +32,7 @@ if (Object.keys(argsMap).length === 0 || argsMap.help !== undefined) { Logger.log('Options:'); Logger.log(' --profile= The .cpuprofile file to symbolicate'); Logger.log(' --platform= The platform for which the source map was uploaded'); - Logger.log(' --gh-token Token to use for requests send to the GitHub API. By default tries to pick up from the environment variable GITHUB_TOKEN'); + Logger.log(' --ghToken Token to use for requests send to the GitHub API. By default tries to pick up from the environment variable GITHUB_TOKEN'); Logger.log(' --help Display this help message'); process.exit(0); } @@ -53,7 +53,7 @@ if (argsMap.platform === undefined) { const githubToken = argsMap.ghToken ?? process.env.GITHUB_TOKEN; if (githubToken === undefined) { - Logger.error('No GitHub token provided. Either set a GITHUB_TOKEN environment variable or pass it using --gh-token'); + Logger.error('No GitHub token provided. Either set a GITHUB_TOKEN environment variable or pass it using --ghToken'); process.exit(1); } diff --git a/src/App.tsx b/src/App.tsx index 21025d34a661..98b5d4afeb1d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; +import {SearchContextProvider} from './components/Search/SearchContext'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; @@ -91,6 +92,7 @@ function App({url}: AppProps) { VolumeContextProvider, VideoPopoverMenuContextProvider, KeyboardProvider, + SearchContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 233b35e6ac4b..e2ba7ef0aa1d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -74,6 +74,10 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + RECENT_WAYPOINTS_NUMBER: 20, + DEFAULT_DB_NAME: 'OnyxDB', + DEFAULT_TABLE_NAME: 'keyvaluepairs', + DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace @@ -331,6 +335,8 @@ const CONST = { VERIFICATION_MAX_ATTEMPTS: 7, STATE: { VERIFYING: 'VERIFYING', + VALIDATING: 'VALIDATING', + SETUP: 'SETUP', PENDING: 'PENDING', OPEN: 'OPEN', }, @@ -357,13 +363,14 @@ const CONST = { DEFAULT_ROOMS: 'defaultRooms', VIOLATIONS: 'violations', DUPE_DETECTION: 'dupeDetection', - REPORT_FIELDS: 'reportFields', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', SPOTNANA_TRAVEL: 'spotnanaTravel', NETSUITE_ON_NEW_EXPENSIFY: 'netsuiteOnNewExpensify', REPORT_FIELDS_FEATURE: 'reportFieldsFeature', WORKSPACE_FEEDS: 'workspaceFeeds', + NETSUITE_USA_TAX: 'netsuiteUsaTax', + INTACCT_ON_NEW_EXPENSIFY: 'intacctOnNewExpensify', }, BUTTON_STATES: { DEFAULT: 'default', @@ -603,6 +610,7 @@ const CONST = { TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`, EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT: 'https://www.expensify.com/tools/integrations/downloadPackage', EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT_FILE_NAME: 'ExpensifyPackageForSageIntacct', + SAGE_INTACCT_INSTRUCTIONS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct', HOW_TO_CONNECT_TO_SAGE_INTACCT: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct#how-to-connect-to-sage-intacct', PRICING: `https://www.expensify.com/pricing`, @@ -656,11 +664,13 @@ const CONST = { MEMBER: 'member', }, MAX_COUNT_BEFORE_FOCUS_UPDATE: 30, + MIN_INITIAL_REPORT_ACTION_COUNT: 15, SPLIT_REPORTID: '-2', ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts TYPE: { + ACTIONABLE_ADD_PAYMENT_CARD: 'ACTIONABLEADDPAYMENTCARD', ACTIONABLE_JOIN_REQUEST: 'ACTIONABLEJOINREQUEST', ACTIONABLE_MENTION_WHISPER: 'ACTIONABLEMENTIONWHISPER', ACTIONABLE_REPORT_MENTION_WHISPER: 'ACTIONABLEREPORTMENTIONWHISPER', @@ -674,12 +684,12 @@ const CONST = { CLOSED: 'CLOSED', CREATED: 'CREATED', DELEGATE_SUBMIT: 'DELEGATESUBMIT', // OldDot Action - DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action + DELETED_ACCOUNT: 'DELETEDACCOUNT', // Deprecated OldDot Action DISMISSED_VIOLATION: 'DISMISSEDVIOLATION', - DONATION: 'DONATION', // OldDot Action + DONATION: 'DONATION', // Deprecated OldDot Action EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action EXPORTED_TO_INTEGRATION: 'EXPORTINTEGRATION', // OldDot Action - EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // OldDot Action + EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // Deprecated OldDot Action FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', HOLD_COMMENT: 'HOLDCOMMENT', @@ -699,9 +709,9 @@ const CONST = { REIMBURSEMENT_DELAYED: 'REIMBURSEMENTDELAYED', // OldDot Action REIMBURSEMENT_QUEUED: 'REIMBURSEMENTQUEUED', REIMBURSEMENT_DEQUEUED: 'REIMBURSEMENTDEQUEUED', - REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action - REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // OldDot Action - REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // OldDot Action + REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // Deprecated OldDot Action + REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // Deprecated OldDot Action + REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // Deprecated OldDot Action RENAMED: 'RENAMED', REPORT_PREVIEW: 'REPORTPREVIEW', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action @@ -714,7 +724,7 @@ const CONST = { TASK_EDITED: 'TASKEDITED', TASK_REOPENED: 'TASKREOPENED', TRIPPREVIEW: 'TRIPPREVIEW', - UNAPPROVED: 'UNAPPROVED', // OldDot Action + UNAPPROVED: 'UNAPPROVED', UNHOLD: 'UNHOLD', UNSHARE: 'UNSHARE', // OldDot Action UPDATE_GROUP_CHAT_MEMBER_ROLE: 'UPDATEGROUPCHATMEMBERROLE', @@ -785,6 +795,7 @@ const CONST = { UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED', UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE', LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY', + CORPORATE_UPGRADE: 'POLICYCHANGELOG_CORPORATE_UPGRADE', }, ROOM_CHANGE_LOG: { INVITE_TO_ROOM: 'INVITETOROOM', @@ -833,6 +844,10 @@ const CONST = { TASK: 'task', INVOICE: 'invoice', }, + UNSUPPORTED_TYPE: { + PAYCHECK: 'paycheck', + BILL: 'bill', + }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', @@ -885,6 +900,10 @@ const CONST = { INDIVIDUAL: 'individual', BUSINESS: 'policy', }, + EXPORT_OPTIONS: { + EXPORT_TO_INTEGRATION: 'exportToIntegration', + MARK_AS_EXPORTED: 'markAsExported', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -1115,8 +1134,6 @@ const CONST = { // around each header. EMOJI_NUM_PER_ROW: 8, - EMOJI_FREQUENT_ROW_COUNT: 3, - EMOJI_DEFAULT_SKIN_TONE: -1, // Amount of emojis to render ahead at the end of the update cycle @@ -1195,6 +1212,8 @@ const CONST = { NOTE: 'n', }, + IMAGE_HIGH_RESOLUTION_THRESHOLD: 7000, + IMAGE_OBJECT_POSITION: { TOP: 'top', INITIAL: 'initial', @@ -1237,7 +1256,7 @@ const CONST = { MAX_AMOUNT_OF_SUGGESTIONS: 20, MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5, HERE_TEXT: '@here', - SUGGESTION_BOX_MAX_SAFE_DISTANCE: 38, + SUGGESTION_BOX_MAX_SAFE_DISTANCE: 10, BIG_SCREEN_SUGGESTION_WIDTH: 300, }, COMPOSER_MAX_HEIGHT: 125, @@ -1287,6 +1306,7 @@ const CONST = { REPORT_FIELD: 'REPORT_FIELD', NOT_IMPORTED: 'NOT_IMPORTED', IMPORTED: 'IMPORTED', + NETSUITE_DEFAULT: 'NETSUITE_DEFAULT', }, QUICKBOOKS_ONLINE: 'quickbooksOnline', @@ -1337,8 +1357,46 @@ const CONST = { }, }, - NETSUITE_CONFIG: { - SUBSIDIARY: 'subsidiary', + SAGE_INTACCT_MAPPING_VALUE: { + NONE: 'NONE', + DEFAULT: 'DEFAULT', + TAG: 'TAG', + REPORT_FIELD: 'REPORT_FIELD', + }, + + SAGE_INTACCT_CONFIG: { + MAPPINGS: { + DEPARTMENTS: 'departments', + CLASSES: 'classes', + LOCATIONS: 'locations', + CUSTOMERS: 'customers', + PROJECTS: 'projects', + }, + SYNC_ITEMS: 'syncItems', + TAX: 'tax', + EXPORT: 'export', + EXPORT_DATE: 'exportDate', + NON_REIMBURSABLE_CREDIT_CARD_VENDOR: 'nonReimbursableCreditCardChargeDefaultVendor', + NON_REIMBURSABLE_VENDOR: 'nonReimbursableVendor', + REIMBURSABLE_VENDOR: 'reimbursableExpenseReportDefaultVendor', + NON_REIMBURSABLE_ACCOUNT: 'nonReimbursableAccount', + NON_REIMBURSABLE: 'nonReimbursable', + EXPORTER: 'exporter', + REIMBURSABLE: 'reimbursable', + AUTO_SYNC: 'autoSync', + AUTO_SYNC_ENABLED: 'enabled', + IMPORT_EMPLOYEES: 'importEmployees', + APPROVAL_MODE: 'approvalMode', + SYNC: 'sync', + SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports', + REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID', + ENTITY: 'entity', + }, + + SAGE_INTACCT: { + APPROVAL_MODE: { + APPROVAL_MANUAL: 'APPROVAL_MANUAL', + }, }, QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: { @@ -1347,12 +1405,263 @@ const CONST = { JOURNAL_ENTRY: 'journal_entry', }, + SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE: { + EXPENSE_REPORT: 'EXPENSE_REPORT', + VENDOR_BILL: 'VENDOR_BILL', + }, + + SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE: { + CREDIT_CARD_CHARGE: 'CREDIT_CARD_CHARGE', + VENDOR_BILL: 'VENDOR_BILL', + }, + XERO_EXPORT_DATE: { LAST_EXPENSE: 'LAST_EXPENSE', REPORT_EXPORTED: 'REPORT_EXPORTED', REPORT_SUBMITTED: 'REPORT_SUBMITTED', }, + SAGE_INTACCT_EXPORT_DATE: { + LAST_EXPENSE: 'LAST_EXPENSE', + EXPORTED: 'EXPORTED', + SUBMITTED: 'SUBMITTED', + }, + + NETSUITE_CONFIG: { + SUBSIDIARY: 'subsidiary', + EXPORTER: 'exporter', + EXPORT_DATE: 'exportDate', + REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'reimbursableExpensesExportDestination', + NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'nonreimbursableExpensesExportDestination', + DEFAULT_VENDOR: 'defaultVendor', + REIMBURSABLE_PAYABLE_ACCOUNT: 'reimbursablePayableAccount', + PAYABLE_ACCT: 'payableAcct', + JOURNAL_POSTING_PREFERENCE: 'journalPostingPreference', + RECEIVABLE_ACCOUNT: 'receivableAccount', + INVOICE_ITEM_PREFERENCE: 'invoiceItemPreference', + INVOICE_ITEM: 'invoiceItem', + TAX_POSTING_ACCOUNT: 'taxPostingAccount', + PROVINCIAL_TAX_POSTING_ACCOUNT: 'provincialTaxPostingAccount', + ALLOW_FOREIGN_CURRENCY: 'allowForeignCurrency', + EXPORT_TO_NEXT_OPEN_PERIOD: 'exportToNextOpenPeriod', + IMPORT_FIELDS: ['departments', 'classes', 'locations'], + AUTO_SYNC: 'autoSync', + REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID', + COLLECTION_ACCOUNT: 'collectionAccount', + AUTO_CREATE_ENTITIES: 'autoCreateEntities', + APPROVAL_ACCOUNT: 'approvalAccount', + CUSTOM_FORM_ID_OPTIONS: 'customFormIDOptions', + TOKEN_INPUT_STEP_NAMES: ['1', '2,', '3', '4', '5'], + TOKEN_INPUT_STEP_KEYS: { + 0: 'installBundle', + 1: 'enableTokenAuthentication', + 2: 'enableSoapServices', + 3: 'createAccessToken', + 4: 'enterCredentials', + }, + IMPORT_CUSTOM_FIELDS: { + CUSTOM_SEGMENTS: 'customSegments', + CUSTOM_LISTS: 'customLists', + }, + CUSTOM_SEGMENT_FIELDS: ['segmentName', 'internalID', 'scriptID', 'mapping'], + CUSTOM_LIST_FIELDS: ['listName', 'internalID', 'transactionFieldID', 'mapping'], + CUSTOM_FORM_ID_TYPE: { + REIMBURSABLE: 'reimbursable', + NON_REIMBURSABLE: 'nonReimbursable', + }, + SYNC_OPTIONS: { + SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports', + SYNC_PEOPLE: 'syncPeople', + ENABLE_NEW_CATEGORIES: 'enableNewCategories', + EXPORT_REPORTS_TO: 'exportReportsTo', + EXPORT_VENDOR_BILLS_TO: 'exportVendorBillsTo', + EXPORT_JOURNALS_TO: 'exportJournalsTo', + SYNC_TAX: 'syncTax', + CROSS_SUBSIDIARY_CUSTOMERS: 'crossSubsidiaryCustomers', + CUSTOMER_MAPPINGS: { + CUSTOMERS: 'customers', + JOBS: 'jobs', + }, + }, + NETSUITE_CUSTOM_LIST_LIMIT: 8, + NETSUITE_ADD_CUSTOM_LIST_STEP_NAMES: ['1', '2,', '3', '4'], + NETSUITE_ADD_CUSTOM_SEGMENT_STEP_NAMES: ['1', '2,', '3', '4', '5', '6,'], + }, + + NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES: { + CUSTOM_LISTS: { + CUSTOM_LIST_PICKER: 0, + TRANSACTION_FIELD_ID: 1, + MAPPING: 2, + CONFIRM: 3, + }, + CUSTOM_SEGMENTS: { + SEGMENT_TYPE: 0, + SEGMENT_NAME: 1, + INTERNAL_ID: 2, + SCRIPT_ID: 3, + MAPPING: 4, + CONFIRM: 5, + }, + }, + + NETSUITE_CUSTOM_RECORD_TYPES: { + CUSTOM_SEGMENT: 'customSegment', + CUSTOM_RECORD: 'customRecord', + }, + + NETSUITE_FORM_STEPS_HEADER_HEIGHT: 40, + + NETSUITE_IMPORT: { + HELP_LINKS: { + CUSTOM_SEGMENTS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite#custom-segments', + CUSTOM_LISTS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite#custom-lists', + }, + }, + + NETSUITE_EXPORT_DATE: { + LAST_EXPENSE: 'LAST_EXPENSE', + EXPORTED: 'EXPORTED', + SUBMITTED: 'SUBMITTED', + }, + + NETSUITE_EXPORT_DESTINATION: { + EXPENSE_REPORT: 'EXPENSE_REPORT', + VENDOR_BILL: 'VENDOR_BILL', + JOURNAL_ENTRY: 'JOURNAL_ENTRY', + }, + + NETSUITE_MAP_EXPORT_DESTINATION: { + EXPENSE_REPORT: 'expenseReport', + VENDOR_BILL: 'vendorBill', + JOURNAL_ENTRY: 'journalEntry', + }, + + NETSUITE_INVOICE_ITEM_PREFERENCE: { + CREATE: 'create', + SELECT: 'select', + }, + + NETSUITE_JOURNAL_POSTING_PREFERENCE: { + JOURNALS_POSTING_INDIVIDUAL_LINE: 'JOURNALS_POSTING_INDIVIDUAL_LINE', + JOURNALS_POSTING_TOTAL_LINE: 'JOURNALS_POSTING_TOTAL_LINE', + }, + + NETSUITE_EXPENSE_TYPE: { + REIMBURSABLE: 'reimbursable', + NON_REIMBURSABLE: 'nonreimbursable', + }, + + NETSUITE_REPORTS_APPROVAL_LEVEL: { + REPORTS_APPROVED_NONE: 'REPORTS_APPROVED_NONE', + REPORTS_SUPERVISOR_APPROVED: 'REPORTS_SUPERVISOR_APPROVED', + REPORTS_ACCOUNTING_APPROVED: 'REPORTS_ACCOUNTING_APPROVED', + REPORTS_APPROVED_BOTH: 'REPORTS_APPROVED_BOTH', + }, + + NETSUITE_VENDOR_BILLS_APPROVAL_LEVEL: { + VENDOR_BILLS_APPROVED_NONE: 'VENDOR_BILLS_APPROVED_NONE', + VENDOR_BILLS_APPROVAL_PENDING: 'VENDOR_BILLS_APPROVAL_PENDING', + VENDOR_BILLS_APPROVED: 'VENDOR_BILLS_APPROVED', + }, + + NETSUITE_JOURNALS_APPROVAL_LEVEL: { + JOURNALS_APPROVED_NONE: 'JOURNALS_APPROVED_NONE', + JOURNALS_APPROVAL_PENDING: 'JOURNALS_APPROVAL_PENDING', + JOURNALS_APPROVED: 'JOURNALS_APPROVED', + }, + + NETSUITE_ACCOUNT_TYPE: { + ACCOUNTS_PAYABLE: '_accountsPayable', + ACCOUNTS_RECEIVABLE: '_accountsReceivable', + OTHER_CURRENT_LIABILITY: '_otherCurrentLiability', + CREDIT_CARD: '_creditCard', + BANK: '_bank', + OTHER_CURRENT_ASSET: '_otherCurrentAsset', + LONG_TERM_LIABILITY: '_longTermLiability', + EXPENSE: '_expense', + }, + + NETSUITE_APPROVAL_ACCOUNT_DEFAULT: 'APPROVAL_ACCOUNT_DEFAULT', + + /** + * Countries where tax setting is permitted (Strings are in the format of Netsuite's Country type/enum) + * + * Should mirror the list on the OldDot. + */ + NETSUITE_TAX_COUNTRIES: [ + '_canada', + '_unitedKingdomGB', + '_unitedKingdom', + '_australia', + '_southAfrica', + '_india', + '_france', + '_netherlands', + '_germany', + '_singapore', + '_spain', + '_ireland', + '_denmark', + '_brazil', + '_japan', + '_philippines', + '_china', + '_argentina', + '_newZealand', + '_switzerland', + '_sweden', + '_portugal', + '_mexico', + '_israel', + '_thailand', + '_czechRepublic', + '_egypt', + '_ghana', + '_indonesia', + '_iranIslamicRepublicOf', + '_jordan', + '_kenya', + '_kuwait', + '_lebanon', + '_malaysia', + '_morocco', + '_myanmar', + '_nigeria', + '_pakistan', + '_saudiArabia', + '_sriLanka', + '_unitedArabEmirates', + '_vietnam', + '_austria', + '_bulgaria', + '_greece', + '_cyprus', + '_norway', + '_romania', + '_poland', + '_hongKong', + '_luxembourg', + '_lithuania', + '_malta', + '_finland', + '_koreaRepublicOf', + '_italy', + '_georgia', + '_hungary', + '_latvia', + '_estonia', + '_slovenia', + '_serbia', + '_croatiaHrvatska', + '_belgium', + '_turkey', + '_taiwan', + '_azerbaijan', + '_slovakRepublic', + '_costaRica', + ] as string[], + QUICKBOOKS_EXPORT_DATE: { LAST_EXPENSE: 'LAST_EXPENSE', REPORT_EXPORTED: 'REPORT_EXPORTED', @@ -1721,6 +2030,11 @@ const CONST = { MAKE_MEMBER: 'makeMember', MAKE_ADMIN: 'makeAdmin', }, + BULK_ACTION_TYPES: { + DELETE: 'delete', + DISABLE: 'disable', + ENABLE: 'enable', + }, MORE_FEATURES: { ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled', ARE_TAGS_ENABLED: 'areTagsEnabled', @@ -1728,23 +2042,9 @@ const CONST = { ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled', ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled', ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled', + ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled', ARE_TAXES_ENABLED: 'tax', }, - CATEGORIES_BULK_ACTION_TYPES: { - DELETE: 'delete', - DISABLE: 'disable', - ENABLE: 'enable', - }, - TAGS_BULK_ACTION_TYPES: { - DELETE: 'delete', - DISABLE: 'disable', - ENABLE: 'enable', - }, - DISTANCE_RATES_BULK_ACTION_TYPES: { - DELETE: 'delete', - DISABLE: 'disable', - ENABLE: 'enable', - }, DEFAULT_CATEGORIES: [ 'Advertising', 'Benefits', @@ -1775,11 +2075,6 @@ const CONST = { DUPLICATE_SUBSCRIPTION: 'duplicateSubscription', FAILED_TO_CLEAR_BALANCE: 'failedToClearBalance', }, - TAX_RATES_BULK_ACTION_TYPES: { - DELETE: 'delete', - DISABLE: 'disable', - ENABLE: 'enable', - }, COLLECTION_KEYS: { DESCRIPTION: 'description', REIMBURSER: 'reimburser', @@ -1801,8 +2096,12 @@ const CONST = { NAME_USER_FRIENDLY: { netsuite: 'NetSuite', quickbooksOnline: 'Quickbooks Online', + quickbooksDesktop: 'Quickbooks Desktop', xero: 'Xero', intacct: 'Sage Intacct', + financialForce: 'FinancialForce', + billCom: 'Bill.com', + zenefits: 'Zenefits', }, SYNC_STAGE_NAME: { STARTING_IMPORT_QBO: 'startingImportQBO', @@ -1862,7 +2161,11 @@ const CONST = { ACCESS_VARIANTS: { PAID: 'paid', ADMIN: 'admin', + CONTROL: 'control', }, + DEFAULT_MAX_EXPENSE_AGE: 90, + DEFAULT_MAX_EXPENSE_AMOUNT: 200000, + DEFAULT_MAX_AMOUNT_NO_RECEIPT: 2500, }, CUSTOM_UNITS: { @@ -1941,6 +2244,10 @@ const CONST = { CARD_NAME: 'CardName', CONFIRMATION: 'Confirmation', }, + CARD_TYPE: { + PHYSICAL: 'physical', + VIRTUAL: 'virtual', + }, }, AVATAR_ROW_SIZE: { DEFAULT: 4, @@ -2042,6 +2349,8 @@ const CONST = { POLICY_ID_FROM_PATH: /\/w\/([a-zA-Z0-9]+)(\/|$)/, SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#@]+(?:\\.[\\w\\-\\'\\+]+)*(?![^\`]*\`)`, 'gim'), + + REPORT_ID_FROM_PATH: /\/r\/(\d+)/, }, PRONOUNS: { @@ -2058,6 +2367,7 @@ const CONST = { WORKSPACE_INVOICES: 'WorkspaceSendInvoices', WORKSPACE_TRAVEL: 'WorkspaceBookTravel', WORKSPACE_MEMBERS: 'WorkspaceManageMembers', + WORKSPACE_EXPENSIFY_CARD: 'WorkspaceExpensifyCard', WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows', WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount', WORKSPACE_SETTINGS: 'WorkspaceSettings', @@ -2100,7 +2410,7 @@ const CONST = { this.ACCOUNT_ID.REWARDS, this.ACCOUNT_ID.STUDENT_AMBASSADOR, this.ACCOUNT_ID.SVFG, - ]; + ].filter((id) => id !== -1); }, // Emails that profile view is prohibited @@ -2126,6 +2436,7 @@ const CONST = { LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, TAG_NAME_LIMIT: 256, + WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH: 256, REPORT_NAME_LIMIT: 100, TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, @@ -2158,8 +2469,11 @@ const CONST = { SETTINGS: 'settings', LEAVE_ROOM: 'leaveRoom', PRIVATE_NOTES: 'privateNotes', + EXPORT: 'export', DELETE: 'delete', MARK_AS_INCOMPLETE: 'markAsIncomplete', + CANCEL_PAYMENT: 'cancelPayment', + UNAPPROVE: 'unapprove', }, EDIT_REQUEST_FIELD: { AMOUNT: 'amount', @@ -3604,6 +3918,7 @@ const CONST = { }, EVENTS: { SCROLLING: 'scrolling', + ON_RETURN_TO_OLD_DOT: 'onReturnToOldDot', }, CHAT_HEADER_LOADER_HEIGHT: 36, @@ -3714,6 +4029,15 @@ const CONST = { WARNING: 'warning', }, + /** + * Constants with different types for the modifiedAmount violation + */ + MODIFIED_AMOUNT_VIOLATION_DATA: { + DISTANCE: 'distance', + CARD: 'card', + SMARTSCAN: 'smartscan', + }, + /** * Constants for types of violation names. * Defined here because they need to be referenced by the type system to generate the @@ -3755,6 +4079,7 @@ const CONST = { TAX_REQUIRED: 'taxRequired', HOLD: 'hold', }, + REVIEW_DUPLICATES_ORDER: ['merchant', 'category', 'tag', 'description', 'taxCode', 'billable', 'reimbursable'], /** Context menu types */ CONTEXT_MENU_TYPES: { @@ -3948,13 +4273,13 @@ const CONST = { type: 'setupCategories', autoCompleted: false, title: 'Set up categories', - description: + description: ({workspaceLink}: {workspaceLink: string}) => '*Set up categories* so your team can code expenses for easy reporting.\n' + '\n' + 'Here’s how to set up categories:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Go to *Workspaces* > [your workspace].\n' + + `2. Go to [*Workspaces* > [your workspace]](${workspaceLink}).\n` + '3. Click *Categories*.\n' + '4. Enable and disable default categories.\n' + '5. Click *Add categories* to make your own.\n' + @@ -3965,13 +4290,13 @@ const CONST = { type: 'addExpenseApprovals', autoCompleted: false, title: 'Add expense approvals', - description: + description: ({workspaceLink}: {workspaceLink: string}) => '*Add expense approvals* to review your team’s spend and keep it under control.\n' + '\n' + 'Here’s how to add expense approvals:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Go to *Workspaces* > [your workspace].\n' + + `2. Go to [*Workspaces* > [your workspace]](${workspaceLink}).\n` + '3. Click *More features*.\n' + '4. Enable *Workflows*.\n' + '5. In *Workflows*, enable *Add approvals*.\n' + @@ -3982,13 +4307,13 @@ const CONST = { type: 'inviteTeam', autoCompleted: false, title: 'Invite your team', - description: + description: ({workspaceLink}: {workspaceLink: string}) => '*Invite your team* to Expensify so they can start tracking expenses today.\n' + '\n' + 'Here’s how to invite your team:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Go to *Workspaces* > [your workspace].\n' + + `2. Go to [*Workspaces* > [your workspace]](${workspaceLink}).\n` + '3. Click *Members* > *Invite member*.\n' + '4. Enter emails or phone numbers. \n' + '5. Add an invite message if you want.\n' + @@ -4866,9 +5191,16 @@ const CONST = { REPORT: 'report', }, ACTION_TYPES: { + VIEW: 'view', + REVIEW: 'review', DONE: 'done', PAID: 'paid', - VIEW: 'view', + }, + BULK_ACTION_TYPES: { + EXPORT: 'export', + HOLD: 'hold', + UNHOLD: 'unhold', + DELETE: 'delete', }, TRANSACTION_TYPE: { CASH: 'cash', @@ -4899,6 +5231,39 @@ const CONST = { ACTION: 'action', TAX_AMOUNT: 'taxAmount', }, + SYNTAX_OPERATORS: { + AND: 'and', + OR: 'or', + EQUAL_TO: 'eq', + NOT_EQUAL_TO: 'neq', + GREATER_THAN: 'gt', + GREATER_THAN_OR_EQUAL_TO: 'gte', + LOWER_THAN: 'lt', + LOWER_THAN_OR_EQUAL_TO: 'lte', + }, + SYNTAX_ROOT_KEYS: { + TYPE: 'type', + STATUS: 'status', + SORT_BY: 'sortBy', + SORT_ORDER: 'sortOrder', + OFFSET: 'offset', + }, + SYNTAX_FILTER_KEYS: { + DATE: 'date', + AMOUNT: 'amount', + EXPENSE_TYPE: 'expenseType', + CURRENCY: 'currency', + MERCHANT: 'merchant', + DESCRIPTION: 'description', + FROM: 'from', + TO: 'to', + CATEGORY: 'category', + TAG: 'tag', + TAX_RATE: 'taxRate', + CARD_ID: 'cardID', + REPORT_ID: 'reportID', + KEYWORD: 'keyword', + }, }, REFERRER: { @@ -4907,18 +5272,16 @@ const CONST = { SUBSCRIPTION_SIZE_LIMIT: 20000, + PAGINATION_START_ID: '-1', + PAGINATION_END_ID: '-2', + PAYMENT_CARD_CURRENCY: { USD: 'USD', AUD: 'AUD', - GBP: 'GBP', NZD: 'NZD', }, SUBSCRIPTION_PRICE_FACTOR: 2, - SUBSCRIPTION_POSSIBLE_COST_SAVINGS: { - COLLECT_PLAN: 10, - CONTROL_PLAN: 18, - }, FEEDBACK_SURVEY_OPTIONS: { TOO_LIMITED: { ID: 'tooLimited', @@ -4938,7 +5301,72 @@ const CONST = { }, }, + MAX_LENGTH_256: 256, + WORKSPACE_CARDS_LIST_LABEL_TYPE: { + CURRENT_BALANCE: 'currentBalance', + REMAINING_LIMIT: 'remainingLimit', + CASH_BACK: 'cashBack', + }, + EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], + EMPTY_STATE_MEDIA: { + ANIMATION: 'animation', + ILLUSTRATION: 'illustration', + VIDEO: 'video', + }, + get UPGRADE_FEATURE_INTRO_MAPPING() { + return { + reportFields: { + id: 'reportFields' as const, + alias: 'report-fields', + name: 'Report Fields', + title: 'workspace.upgrade.reportFields.title' as const, + description: 'workspace.upgrade.reportFields.description' as const, + icon: 'Pencil', + }, + [this.POLICY.CONNECTIONS.NAME.NETSUITE]: { + id: this.POLICY.CONNECTIONS.NAME.NETSUITE, + alias: 'netsuite', + name: this.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.netsuite, + title: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.NETSUITE}.title` as const, + description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.NETSUITE}.description` as const, + icon: 'NetSuiteSquare', + }, + [this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: { + id: this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, + alias: 'sage-intacct', + name: this.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.intacct, + title: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.title` as const, + description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.SAGE_INTACCT}.description` as const, + icon: 'IntacctSquare', + }, + glCodes: { + id: 'glCodes' as const, + alias: 'gl-codes', + name: 'GL codes', + title: 'workspace.upgrade.glCodes.title' as const, + description: 'workspace.upgrade.glCodes.description' as const, + icon: 'Tag', + }, + glAndPayrollCodes: { + id: 'glAndPayrollCodes' as const, + alias: 'gl-and-payroll-codes', + name: 'GL & Payroll codes', + title: 'workspace.upgrade.glAndPayrollCodes.title' as const, + description: 'workspace.upgrade.glAndPayrollCodes.description' as const, + icon: 'FolderOpen', + }, + }; + }, + REPORT_FIELD_TYPES: { + TEXT: 'text', + DATE: 'date', + LIST: 'dropdown', + }, + + NAVIGATION_ACTIONS: { + RESET: 'RESET', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index bfe4db13d9c4..f9fd379d94ce 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -3,12 +3,13 @@ import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat import type {NativeEventSubscription} from 'react-native'; import {AppState, Linking, NativeModules} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; import ConfirmModal from './components/ConfirmModal'; import DeeplinkWrapper from './components/DeeplinkWrapper'; import EmojiPicker from './components/EmojiPicker/EmojiPicker'; import FocusModeNotification from './components/FocusModeNotification'; import GrowlNotification from './components/GrowlNotification'; +import RequireTwoFactorAuthenticationModal from './components/RequireTwoFactorAuthenticationModal'; import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; import UpdateAppModal from './components/UpdateAppModal'; @@ -19,6 +20,7 @@ import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; import * as ActiveClientManager from './libs/ActiveClientManager'; import BootSplash from './libs/BootSplash'; +import FS from './libs/Fullstory'; import * as Growl from './libs/Growl'; import Log from './libs/Log'; import migrateOnyx from './libs/migrateOnyx'; @@ -36,6 +38,7 @@ import ONYXKEYS from './ONYXKEYS'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; import type {Route} from './ROUTES'; +import ROUTES from './ROUTES'; import type {ScreenShareRequest, Session} from './types/onyx'; Onyx.registerLogger(({level, message}) => { @@ -100,6 +103,16 @@ function Expensify({ const [isSplashHidden, setIsSplashHidden] = useState(false); const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false); const {translate} = useLocalize(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); + + useEffect(() => { + if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) { + return; + } + setShouldShowRequire2FAModal(true); + }, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth]); + const [initialUrl, setInitialUrl] = useState(null); useEffect(() => { @@ -147,6 +160,9 @@ function Expensify({ // Initialize this client as being an active client ActiveClientManager.init(); + // Initialize Fullstory lib + FS.init(); + // Used for the offline indicator appearing when someone is offline const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); @@ -208,7 +224,7 @@ function Expensify({ } appStateChangeListener.current.remove(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run again }, []); // This is being done since we want to play sound even when iOS device is on silent mode, to align with other platforms. @@ -249,6 +265,16 @@ function Expensify({ /> ) : null} {focusModeNotification ? : null} + {shouldShowRequire2FAModal ? ( + { + setShouldShowRequire2FAModal(false); + Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.HOME)); + }} + isVisible + description={translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsDescription')} + /> + ) : null} )} diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6f94a23acad8..091a6eceae9b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -134,7 +134,7 @@ const ONYXKEYS = { NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod', /** This NVP holds to most recent waypoints that a person has used when creating a distance expense */ - NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', + NVP_RECENT_WAYPOINTS: 'nvp_expensify_recentWaypoints', /** This NVP contains the choice that the user made on the engagement modal */ NVP_INTRO_SELECTED: 'nvp_introSelected', @@ -162,6 +162,15 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store the stripe id status */ + NVP_PRIVATE_STRIPE_CUSTOMER_ID: 'nvp_private_stripeCustomerID', + + /** Store the billing dispute status */ + NVP_PRIVATE_BILLING_DISPUTE_PENDING: 'nvp_private_billingDisputePending', + + /** Store the billing status */ + NVP_PRIVATE_BILLING_STATUS: 'nvp_private_billingStatus', + /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', @@ -187,7 +196,7 @@ const ONYXKEYS = { NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID', /** The amount owed by the workspace’s owner. */ - NVP_PRIVATE_AMOUNT_OWNED: 'nvp_private_amountOwed', + NVP_PRIVATE_AMOUNT_OWED: 'nvp_private_amountOwed', /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', @@ -311,6 +320,9 @@ const ONYXKEYS = { /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + /** Onboarding error message to be displayed to the user */ + ONBOARDING_ERROR_MESSAGE: 'onboardingErrorMessage', + /** Onboarding policyID selected by the user during Onboarding flow */ ONBOARDING_POLICY_ID: 'onboardingPolicyID', @@ -338,6 +350,9 @@ const ONYXKEYS = { /** Indicates whether we should store logs or not */ SHOULD_STORE_LOGS: 'shouldStoreLogs', + /** Indicates whether we should mask fragile user data while exporting onyx state or not */ + SHOULD_MASK_ONYX_STATE: 'shouldMaskOnyxState', + /** Stores new group chat draft */ NEW_GROUP_CHAT_DRAFT: 'newGroupChatDraft', @@ -350,9 +365,23 @@ const ONYXKEYS = { /** Holds the checks used while transferring the ownership of the workspace */ POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks', + // These statuses below are in separate keys on purpose - it allows us to have different behaviours of the banner based on the status + + /** Indicates whether ClearOutstandingBalance failed */ + SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + + /** Indicates whether ClearOutstandingBalance was successful */ + SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', + + /** Indicates whether ClearOutstandingBalance is pending */ + SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING: 'subscriptionRetryBillingStatusPending', + /** Stores info during review duplicates flow */ REVIEW_DUPLICATES: 'reviewDuplicates', + /** Stores the last export method for policy */ + LAST_EXPORT_METHOD: 'lastExportMethod', + /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard', @@ -385,6 +414,7 @@ const ONYXKEYS = { REPORT_METADATA: 'reportMetadata_', REPORT_ACTIONS: 'reportActions_', REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', + REPORT_ACTIONS_PAGES: 'reportActionsPages_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', @@ -412,6 +442,21 @@ const ONYXKEYS = { // Shared NVPs /** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */ SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_', + + /** Expensify cards settings */ + SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS: 'sharedNVP_private_expensifyCardSettings_', + + /** + * Stores the card list for a given fundID and feed in the format: card__ + * So for example: card_12345_Expensify Card + */ + WORKSPACE_CARDS_LIST: 'card_', + + /** The bank account that Expensify Card payments will be reconciled against */ + SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'sharedNVP_expensifyCard_continuousReconciliationConnection_', + + /** If continuous reconciliation is enabled */ + SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION: 'sharedNVP_expensifyCard_useContinuousReconciliation_', }, /** List of Form ids */ @@ -430,6 +475,8 @@ const ONYXKEYS = { WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft', WORKSPACE_TAX_CUSTOM_NAME: 'workspaceTaxCustomName', WORKSPACE_TAX_CUSTOM_NAME_DRAFT: 'workspaceTaxCustomNameDraft', + WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldForm', + WORKSPACE_REPORT_FIELDS_FORM_DRAFT: 'workspaceReportFieldFormDraft', POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm', POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft', POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm', @@ -500,8 +547,8 @@ const ONYXKEYS = { REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft', GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm', GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft', - REPORT_FIELD_EDIT_FORM: 'reportFieldEditForm', - REPORT_FIELD_EDIT_FORM_DRAFT: 'reportFieldEditFormDraft', + REPORT_FIELDS_EDIT_FORM: 'reportFieldsEditForm', + REPORT_FIELDS_EDIT_FORM_DRAFT: 'reportFieldsEditFormDraft', REIMBURSEMENT_ACCOUNT_FORM: 'reimbursementAccount', REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT_FORM: 'personalBankAccount', @@ -517,6 +564,8 @@ const ONYXKEYS = { WORKSPACE_NEW_TAX_FORM: 'workspaceNewTaxForm', WORKSPACE_NEW_TAX_FORM_DRAFT: 'workspaceNewTaxFormDraft', WORKSPACE_TAX_NAME_FORM: 'workspaceTaxNameForm', + WORKSPACE_TAX_CODE_FORM: 'workspaceTaxCodeForm', + WORKSPACE_TAX_CODE_FORM_DRAFT: 'workspaceTaxCodeFormDraft', WORKSPACE_TAX_NAME_FORM_DRAFT: 'workspaceTaxNameFormDraft', WORKSPACE_TAX_VALUE_FORM: 'workspaceTaxValueForm', WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft', @@ -524,10 +573,26 @@ const ONYXKEYS = { NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', - ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCardForm', - ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardFormDraft', + ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', + ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', + EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName', + EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft', + EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit', + EDIT_EXPENSIFY_CARD_LIMIT_DRAFT_FORM: 'editExpensifyCardLimitDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', + NETSUITE_CUSTOM_FIELD_FORM: 'netSuiteCustomFieldForm', + NETSUITE_CUSTOM_FIELD_FORM_DRAFT: 'netSuiteCustomFieldFormDraft', + NETSUITE_CUSTOM_SEGMENT_ADD_FORM: 'netSuiteCustomSegmentAddForm', + NETSUITE_CUSTOM_SEGMENT_ADD_FORM_DRAFT: 'netSuiteCustomSegmentAddFormDraft', + NETSUITE_CUSTOM_LIST_ADD_FORM: 'netSuiteCustomListAddForm', + NETSUITE_CUSTOM_LIST_ADD_FORM_DRAFT: 'netSuiteCustomListAddFormDraft', + NETSUITE_TOKEN_INPUT_FORM: 'netsuiteTokenInputForm', + NETSUITE_TOKEN_INPUT_FORM_DRAFT: 'netsuiteTokenInputFormDraft', + NETSUITE_CUSTOM_FORM_ID_FORM: 'netsuiteCustomFormIDForm', + NETSUITE_CUSTOM_FORM_ID_FORM_DRAFT: 'netsuiteCustomFormIDFormDraft', + SAGE_INTACCT_DIMENSION_TYPE_FORM: 'sageIntacctDimensionTypeForm', + SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft', }, } as const; @@ -540,6 +605,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; + [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; @@ -574,7 +640,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm; - [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm; + [ONYXKEYS.FORMS.REPORT_FIELDS_EDIT_FORM]: FormTypes.ReportFieldsEditForm; [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; @@ -585,11 +651,20 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm; [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM]: FormTypes.PolicyDistanceRateTaxReclaimableOnEditForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; + [ONYXKEYS.FORMS.WORKSPACE_TAX_CODE_FORM]: FormTypes.WorkspaceTaxCodeForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; + [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm; + [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; + [ONYXKEYS.FORMS.NETSUITE_TOKEN_INPUT_FORM]: FormTypes.NetSuiteTokenInputForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FORM_ID_FORM]: FormTypes.NetSuiteCustomFormIDForm; + [ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm; }; type OnyxFormDraftValuesMapping = { @@ -614,6 +689,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; @@ -635,6 +711,10 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings; + [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; + [ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.BankAccount; + [ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; }; type OnyxValuesMapping = { @@ -687,6 +767,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean; [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; + [ONYXKEYS.LAST_EXPORT_METHOD]: OnyxTypes.LastExportMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; @@ -702,6 +783,9 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; + [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: OnyxTypes.StripeCustomerID; + [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: number; + [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: OnyxTypes.BillingStatus; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; @@ -737,6 +821,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string; [ONYXKEYS.ONBOARDING_POLICY_ID]: string; [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; @@ -747,16 +832,20 @@ type OnyxValuesMapping = { [ONYXKEYS.PLAID_CURRENT_EVENT]: string; [ONYXKEYS.LOGS]: OnyxTypes.CapturedLogs; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; + [ONYXKEYS.SHOULD_MASK_ONYX_STATE]: boolean; [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING]: boolean; [ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings; [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_BILLING_FUND_ID]: number; - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: number; + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; }; @@ -768,6 +857,7 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping; type OnyxValueKey = keyof OnyxValuesMapping; type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey; +type OnyxPagesKey = typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES; type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude}`; /** If this type errors, it means that the `OnyxKey` type is missing some keys. */ @@ -775,4 +865,4 @@ type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: type AssertOnyxKeys = AssertTypesEqual; export default ONYXKEYS; -export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxValueKey, OnyxValues}; +export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues}; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 33eb78dc300d..4f9f2b7b8df6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,8 +1,9 @@ -import type {ValueOf} from 'type-fest'; +import type {TupleToUnion, ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; import type {AuthScreensParamList} from './libs/Navigation/types'; +import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy'; import type {SearchQuery} from './types/onyx/SearchResults'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; @@ -35,7 +36,7 @@ const ROUTES = { ALL_SETTINGS: 'all-settings', - SEARCH: { + SEARCH_CENTRAL_PANE: { route: '/search/:query', getRoute: (searchQuery: SearchQuery, queryParams?: AuthScreensParamList['Search_Central_Pane']) => { const {sortBy, sortOrder} = queryParams ?? {}; @@ -48,11 +49,22 @@ const ROUTES = { }, }, + SEARCH_ADVANCED_FILTERS: 'search/filters', + + SEARCH_ADVANCED_FILTERS_DATE: 'search/filters/date', + + SEARCH_ADVANCED_FILTERS_TYPE: 'search/filters/type', + SEARCH_REPORT: { - route: '/search/:query/view/:reportID', + route: 'search/:query/view/:reportID', getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, }, + TRANSACTION_HOLD_REASON_RHP: { + route: 'search/:query/hold', + getRoute: (query: string) => `search/${query}/hold` 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: { @@ -192,7 +204,8 @@ const ROUTES = { }, SETTINGS_2FA: { route: 'settings/security/two-factor-auth', - getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo), + getRoute: (backTo?: string, forwardTo?: string) => + getUrlWithBackToParam(forwardTo ? `settings/security/two-factor-auth?forwardTo=${encodeURIComponent(forwardTo)}` : 'settings/security/two-factor-auth', backTo), }, SETTINGS_STATUS: 'settings/profile/status', @@ -241,7 +254,12 @@ const ROUTES = { }, REPORT_AVATAR: { route: 'r/:reportID/avatar', - getRoute: (reportID: string) => `r/${reportID}/avatar` as const, + getRoute: (reportID: string, isNewGroupChat?: boolean) => { + if (isNewGroupChat) { + return `r/${reportID}/avatar?isNewGroupChat=${isNewGroupChat}` as const; + } + return `r/${reportID}/avatar` as const; + }, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', @@ -280,6 +298,10 @@ const ROUTES = { route: 'r/:reportID/details', getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), }, + REPORT_WITH_ID_DETAILS_EXPORT: { + route: 'r/:reportID/details/export/:connectionName', + getRoute: (reportID: string, connectionName: ConnectionName) => `r/${reportID}/details/export/${connectionName}` as const, + }, REPORT_SETTINGS: { route: 'r/:reportID/settings', getRoute: (reportID: string) => `r/${reportID}/settings` as const, @@ -670,6 +692,15 @@ const ROUTES = { 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_ACCOUNTING_CARD_RECONCILIATION: { + route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation', + getRoute: (policyID: string, connection: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const, + }, + WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { + route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account', + getRoute: (policyID: string, connection: ValueOf) => + `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const, + }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, @@ -678,6 +709,11 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}` as const, }, + WORKSPACE_UPGRADE: { + route: 'settings/workspaces/:policyID/upgrade/:featureName', + getRoute: (policyID: string, featureName: string, backTo?: string) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), + }, WORKSPACE_CATEGORIES_SETTINGS: { route: 'settings/workspaces/:policyID/categories/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, @@ -690,6 +726,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName/edit', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const, }, + WORKSPACE_CATEGORY_PAYROLL_CODE: { + route: 'settings/workspaces/:policyID/categories/:categoryName/payroll-code', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/payroll-code` as const, + }, + WORKSPACE_CATEGORY_GL_CODE: { + route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` as const, + }, WORKSPACE_MORE_FEATURES: { route: 'settings/workspaces/:policyID/more-features', getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, @@ -722,6 +766,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tag-list/${orderWeight}` as const, }, + WORKSPACE_TAG_GL_CODE: { + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/gl-code', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const, + }, WORKSPACE_TAXES: { route: 'settings/workspaces/:policyID/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const, @@ -779,21 +827,67 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tax/:taxID/value', getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURIComponent(taxID)}/value` as const, }, + WORKSPACE_TAX_CODE: { + route: 'settings/workspaces/:policyID/tax/:taxID/tax-code', + getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURIComponent(taxID)}/tax-code` as const, + }, WORKSPACE_REPORT_FIELDS: { route: 'settings/workspaces/:policyID/reportFields', getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const, }, - // TODO: uncomment after development is done - // WORKSPACE_EXPENSIFY_CARD: { - // route: 'settings/workspaces/:policyID/expensify-card', - // getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, - // }, - // WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { - // route: 'settings/workspaces/:policyID/expensify-card/issues-new', - // getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, - // }, - // TODO: remove after development is done - this one is for testing purposes - WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: 'settings/workspaces/expensify-card/issue-new', + WORKSPACE_CREATE_REPORT_FIELD: { + route: 'settings/workspaces/:policyID/reportFields/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const, + }, + WORKSPACE_REPORT_FIELDS_SETTINGS: { + route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit', + getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit` as const, + }, + WORKSPACE_REPORT_FIELDS_LIST_VALUES: { + route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, + }, + WORKSPACE_REPORT_FIELDS_ADD_VALUE: { + route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, + }, + WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: { + route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?', + getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => + `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, + }, + WORKSPACE_REPORT_FIELDS_EDIT_VALUE: { + route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', + getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const, + }, + WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE: { + route: 'settings/workspaces/:policyID/reportFields/:reportFieldID/edit/initialValue', + getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportFields/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const, + }, + WORKSPACE_EXPENSIFY_CARD: { + route: 'settings/workspaces/:policyID/expensify-card', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, + }, + WORKSPACE_EXPENSIFY_CARD_DETAILS: { + route: 'settings/workspaces/:policyID/expensify-card/:cardID', + getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo), + }, + WORKSPACE_EXPENSIFY_CARD_NAME: { + route: 'settings/workspaces/:policyID/expensify-card/:cardID/edit/name', + getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/edit/name` as const, + }, + WORKSPACE_EXPENSIFY_CARD_LIMIT: { + route: 'settings/workspaces/:policyID/expensify-card/:cardID/edit/limit', + getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/edit/limit` as const, + }, + WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { + route: 'settings/workspaces/:policyID/expensify-card/issue-new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, + }, + WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT: { + route: 'settings/workspaces/:policyID/expensify-card/choose-bank-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, @@ -846,7 +940,38 @@ const ROUTES = { route: 'r/:threadReportID/duplicates/review', getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review` as const, }, - + TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { + route: 'r/:threadReportID/duplicates/review/merchant', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { + route: 'r/:threadReportID/duplicates/review/category', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { + route: 'r/:threadReportID/duplicates/review/tag', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE: { + route: 'r/:threadReportID/duplicates/review/tax-code', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tax-code` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { + route: 'r/:threadReportID/duplicates/review/description', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE: { + route: 'r/:threadReportID/duplicates/review/reimbursable', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/reimbursable` as const, + }, + TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE: { + route: 'r/:threadReportID/duplicates/review/billable', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/billable` as const, + }, + TRANSACTION_DUPLICATE_CONFIRMATION_PAGE: { + route: 'r/:threadReportID/duplicates/confirm', + getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const, + }, POLICY_ACCOUNTING_XERO_IMPORT: { route: 'settings/workspaces/:policyID/accounting/xero/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import` as const, @@ -932,14 +1057,152 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/taxes` as const, }, - POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: { - route: 'settings/workspaces/:policyID/accounting/net-suite/subsidiary-selector', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/net-suite/subsidiary-selector` as const, - }, RESTRICTED_ACTION: { route: 'restricted-action/workspace/:policyID', getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const, }, + POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/netsuite/subsidiary-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/subsidiary-selector` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXISTING_CONNECTIONS: { + route: 'settings/workspaces/:policyID/accounting/netsuite/existing-connections', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/existing-connections` as const, + }, + POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT: { + route: 'settings/workspaces/:policyID/accounting/netsuite/token-input', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/token-input` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_MAPPING: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/mapping/:importField', + getRoute: (policyID: string, importField: TupleToUnion) => + `settings/workspaces/${policyID}/accounting/netsuite/import/mapping/${importField}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField', + getRoute: (policyID: string, importCustomField: ValueOf) => + `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField/view/:valueIndex', + getRoute: (policyID: string, importCustomField: ValueOf, valueIndex: number) => + `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}/view/${valueIndex}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField/edit/:valueIndex/:fieldName', + getRoute: (policyID: string, importCustomField: ValueOf, valueIndex: number, fieldName: string) => + `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}/edit/${valueIndex}/${fieldName}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_ADD: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom-list/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/custom-list/new` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom-segment/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/custom-segment/new` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/customer-projects', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/customer-projects` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/customer-projects/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/customer-projects/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/` as const, + }, + POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/preferred-exporter/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/preferred-exporter/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_DATE_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/date/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/date/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/destination/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/destination/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/vendor/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/vendor/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/payable-account/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/payable-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/journal-posting-preference/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/journal-posting-preference/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_RECEIVABLE_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/receivable-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/receivable-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/invoice-item-preference/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/invoice-item-preference/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/invoice-item-preference/invoice-item/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/invoice-item-preference/invoice-item/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_TAX_POSTING_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/tax-posting-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/tax-posting-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/provincial-tax-posting-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/provincial-tax-posting-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_ADVANCED: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/` as const, + }, + POLICY_ACCOUNTING_NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/reimbursement-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/reimbursement-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_COLLECTION_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/collection-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/collection-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/expense-report-approval-level/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/expense-report-approval-level/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/vendor-bill-approval-level/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/vendor-bill-approval-level/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/journal-entry-approval-level/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/journal-entry-approval-level/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_APPROVAL_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/approval-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/approval-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID: { + route: 'settings/workspaces/:policyID/connections/netsuite/advanced/custom-form-id/:expenseType', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/advanced/custom-form-id/${expenseType}` as const, + }, POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES: { route: 'settings/workspaces/:policyID/accounting/sage-intacct/prerequisites', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/prerequisites` as const, @@ -952,6 +1215,70 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/sage-intacct/existing-connections', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/existing-connections` as const, }, + POLICY_ACCOUNTING_SAGE_INTACCT_ENTITY: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/entity', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/entity` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/toggle-mapping/:mapping', + getRoute: (policyID: string, mapping: SageIntacctMappingName) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/toggle-mapping/${mapping}` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/mapping-type/:mapping', + getRoute: (policyID: string, mapping: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/mapping-type/${mapping}` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/user-dimensions', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/user-dimensions` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_ADD_USER_DIMENSION: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/add-user-dimension', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/add-user-dimension` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_EDIT_USER_DIMENSION: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/edit-user-dimension/:dimensionName', + getRoute: (policyID: string, dimensionName: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/edit-user-dimension/${dimensionName}` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/preferred-exporter', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/preferred-exporter` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/date', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/date` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/reimbursable', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/reimbursable` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/:reimbursable/default-vendor', + getRoute: (policyID: string, reimbursable: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/${reimbursable}/default-vendor` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable/credit-card-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable/credit-card-account` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced/payment-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced/payment-account` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1807c9bb0bab..eddf11d95e4f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -30,6 +30,10 @@ const SCREENS = { SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', REPORT_RHP: 'Search_Report_RHP', + ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', + ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP', + ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP', + TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, SETTINGS: { @@ -144,8 +148,10 @@ const SCREENS = { TRANSACTION_DUPLICATE: 'TransactionDuplicate', TRAVEL: 'Travel', SEARCH_REPORT: 'SearchReport', + SEARCH_ADVANCED_FILTERS: 'SearchAdvancedFilters', SETTINGS_CATEGORIES: 'SettingsCategories', RESTRICTED_ACTION: 'RestrictedAction', + REPORT_EXPORT: 'Report_Export', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -186,6 +192,14 @@ const SCREENS = { TRANSACTION_DUPLICATE: { REVIEW: 'Transaction_Duplicate_Review', + MERCHANT: 'Transaction_Duplicate_Merchant', + CATEGORY: 'Transaction_Duplicate_Category', + TAG: 'Transaction_Duplicate_Tag', + DESCRIPTION: 'Transaction_Duplicate_Description', + TAX_CODE: 'Transaction_Duplicate_Tax_Code', + REIMBURSABLE: 'Transaction_Duplicate_Reimburable', + BILLABLE: 'Transaction_Duplicate_Billable', + CONFIRMATION: 'Transaction_Duplicate_Confirmation', }, IOU_SEND: { @@ -232,6 +246,7 @@ const SCREENS = { REPORT_DETAILS: { ROOT: 'Report_Details_Root', SHARE_CODE: 'Report_Details_Share_Code', + EXPORT: 'Report_Details_Export', }, WORKSPACE: { @@ -272,10 +287,60 @@ const SCREENS = { XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', XERO_EXPORT_BANK_ACCOUNT_SELECT: 'Policy_Accounting_Xero_Export_Bank_Account_Select', - NETSUITE_SUBSIDIARY_SELECTOR: 'Policy_Accounting_Net_Suite_Subsidiary_Selector', + NETSUITE_IMPORT_MAPPING: 'Policy_Accounting_NetSuite_Import_Mapping', + NETSUITE_IMPORT_CUSTOM_FIELD: 'Policy_Accounting_NetSuite_Import_Custom_Field', + NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: 'Policy_Accounting_NetSuite_Import_Custom_Field_View', + NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: 'Policy_Accounting_NetSuite_Import_Custom_Field_Edit', + NETSUITE_IMPORT_CUSTOM_LIST_ADD: 'Policy_Accounting_NetSuite_Import_Custom_List_Add', + NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: 'Policy_Accounting_NetSuite_Import_Custom_Segment_Add', + NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects', + NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects_Select', + NETSUITE_REUSE_EXISTING_CONNECTIONS: 'Policy_Accounting_NetSuite_Reuse_Existing_Connections', + NETSUITE_TOKEN_INPUT: 'Policy_Accounting_NetSuite_Token_Input', + NETSUITE_SUBSIDIARY_SELECTOR: 'Policy_Accounting_NetSuite_Subsidiary_Selector', + NETSUITE_IMPORT: 'Policy_Accounting_NetSuite_Import', + NETSUITE_EXPORT: 'Policy_Accounting_NetSuite_Export', + NETSUITE_PREFERRED_EXPORTER_SELECT: 'Policy_Accounting_NetSuite_Preferred_Exporter_Select', + NETSUITE_DATE_SELECT: 'Policy_Accounting_NetSuite_Date_Select', + NETSUITE_EXPORT_EXPENSES: 'Policy_Accounting_NetSuite_Export_Expenses', + NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Destination_Select', + NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Vendor_Select', + NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Payable_Account_Select', + NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Journal_Posting_Preference_Select', + NETSUITE_RECEIVABLE_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Receivable_Account_Select', + NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT: 'Policy_Accounting_NetSuite_Invoice_Item_Preference_Select', + NETSUITE_INVOICE_ITEM_SELECT: 'Policy_Accounting_NetSuite_Invoice_Item_Select', + NETSUITE_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Tax_Posting_Account_Select', + NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Provincial_Tax_Posting_Account_Select', + NETSUITE_ADVANCED: 'Policy_Accounting_NetSuite_Advanced', + NETSUITE_REIMBURSEMENT_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Reimbursement_Account_Select', + NETSUITE_COLLECTION_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Collection_Account_Select', + NETSUITE_EXPENSE_REPORT_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Expense_Report_Approval_Level_Select', + NETSUITE_VENDOR_BILL_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Vendor_Bill_Approval_Level_Select', + NETSUITE_JOURNAL_ENTRY_APPROVAL_LEVEL_SELECT: 'Policy_Accounting_NetSuite_Journal_Entry_Approval_Level_Select', + NETSUITE_APPROVAL_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Approval_Account_Select', + NETSUITE_CUSTOM_FORM_ID: 'Policy_Accounting_NetSuite_Custom_Form_ID', SAGE_INTACCT_PREREQUISITES: 'Policy_Accounting_Sage_Intacct_Prerequisites', ENTER_SAGE_INTACCT_CREDENTIALS: 'Policy_Enter_Sage_Intacct_Credentials', EXISTING_SAGE_INTACCT_CONNECTIONS: 'Policy_Existing_Sage_Intacct_Connections', + SAGE_INTACCT_ENTITY: 'Policy_Sage_Intacct_Entity', + SAGE_INTACCT_IMPORT: 'Policy_Accounting_Sage_Intacct_Import', + SAGE_INTACCT_TOGGLE_MAPPING: 'Policy_Accounting_Sage_Intacct_Toggle_Mapping', + SAGE_INTACCT_MAPPING_TYPE: 'Policy_Accounting_Sage_Intacct_Mapping_Type', + SAGE_INTACCT_USER_DIMENSIONS: 'Policy_Accounting_Sage_Intacct_User_Dimensions', + SAGE_INTACCT_ADD_USER_DIMENSION: 'Policy_Accounting_Sage_Intacct_Add_User_Dimension', + SAGE_INTACCT_EDIT_USER_DIMENSION: 'Policy_Accounting_Sage_Intacct_Edit_User_Dimension', + SAGE_INTACCT_EXPORT: 'Policy_Accounting_Sage_Intacct_Export', + SAGE_INTACCT_PREFERRED_EXPORTER: 'Policy_Accounting_Sage_Intacct_Preferred_Exporter', + SAGE_INTACCT_EXPORT_DATE: 'Policy_Accounting_Sage_Intacct_Export_Date', + SAGE_INTACCT_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Reimbursable_Expenses', + SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Expenses', + SAGE_INTACCT_DEFAULT_VENDOR: 'Policy_Accounting_Sage_Intacct_Default_Vendor', + SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account', + SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced', + SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account', + CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation', + RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', @@ -285,7 +350,11 @@ const SCREENS = { RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate', RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', + EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details', + EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit', EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New', + EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name', + EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount', BILLS: 'Workspace_Bills', INVOICES: 'Workspace_Invoices', TRAVEL: 'Workspace_Travel', @@ -299,9 +368,17 @@ const SCREENS = { TAG_EDIT: 'Tag_Edit', TAXES: 'Workspace_Taxes', REPORT_FIELDS: 'Workspace_ReportFields', + REPORT_FIELDS_SETTINGS: 'Workspace_ReportFields_Settings', + REPORT_FIELDS_CREATE: 'Workspace_ReportFields_Create', + REPORT_FIELDS_LIST_VALUES: 'Workspace_ReportFields_ListValues', + REPORT_FIELDS_ADD_VALUE: 'Workspace_ReportFields_AddValue', + REPORT_FIELDS_VALUE_SETTINGS: 'Workspace_ReportFields_ValueSettings', + REPORT_FIELDS_EDIT_VALUE: 'Workspace_ReportFields_EditValue', + REPORT_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_ReportFields_EditInitialValue', TAX_EDIT: 'Workspace_Tax_Edit', TAX_NAME: 'Workspace_Tax_Name', TAX_VALUE: 'Workspace_Tax_Value', + TAX_CODE: 'Workspace_Tax_Code', TAXES_SETTINGS: 'Workspace_Taxes_Settings', TAXES_SETTINGS_CUSTOM_TAX_NAME: 'Workspace_Taxes_Settings_CustomTaxName', TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT: 'Workspace_Taxes_Settings_WorkspaceCurrency', @@ -310,6 +387,7 @@ const SCREENS = { TAG_CREATE: 'Tag_Create', TAG_SETTINGS: 'Tag_Settings', TAG_LIST_VIEW: 'Tag_List_View', + TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', WORKFLOWS: 'Workspace_Workflows', @@ -322,6 +400,8 @@ const SCREENS = { NAME: 'Workspace_Profile_Name', CATEGORY_CREATE: 'Category_Create', CATEGORY_EDIT: 'Category_Edit', + CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code', + CATEGORY_GL_CODE: 'Category_GL_Code', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', MORE_FEATURES: 'Workspace_More_Features', @@ -336,6 +416,7 @@ const SCREENS = { DISTANCE_RATE_EDIT: 'Distance_Rate_Edit', DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit', DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit', + UPGRADE: 'Workspace_Upgrade', }, EDIT_REQUEST: { diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx index b977903d3adc..dbe8ada6c4b7 100644 --- a/src/components/AccountingListSkeletonView.tsx +++ b/src/components/AccountingListSkeletonView.tsx @@ -4,12 +4,14 @@ import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; type AccountingListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacityEnabled?: boolean; }; -function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) { +function AccountingListSkeletonView({shouldAnimate = true, gradientOpacityEnabled = false}: AccountingListSkeletonViewProps) { return ( ( <> ; + initialCurrency?: ValueOf; isSecurityCodeRequired?: boolean; - changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void; + changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void; }; const REQUIRED_FIELDS = [INPUT_IDS.SECURITY_CODE]; @@ -30,7 +30,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq const {translate} = useLocalize(); const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); - const [currency, setCurrency] = useState>(initialCurrency ?? CONST.CURRENCY.USD); + const [currency, setCurrency] = useState>(initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD); const validate = (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS); @@ -46,7 +46,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq () => ({ sections: [ { - data: (Object.keys(CONST.CURRENCY) as Array>).map((currencyItem) => ({ + data: (Object.keys(CONST.PAYMENT_CARD_CURRENCY) as Array>).map((currencyItem) => ({ text: currencyItem, value: currencyItem, keyForList: currencyItem, @@ -62,13 +62,13 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq setIsCurrencyModalVisible(true); }, []); - const changeCurrency = useCallback((selectedCurrency: ValueOf) => { + const changeCurrency = useCallback((selectedCurrency: ValueOf) => { setCurrency(selectedCurrency); setIsCurrencyModalVisible(false); }, []); const selectCurrency = useCallback( - (selectedCurrency: ValueOf) => { + (selectedCurrency: ValueOf) => { setCurrency(selectedCurrency); changeBillingCurrency(selectedCurrency); }, @@ -109,7 +109,7 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq >} + currencies={Object.keys(CONST.PAYMENT_CARD_CURRENCY) as Array>} currentCurrency={currency} onCurrencyChange={changeCurrency} onClose={() => setIsCurrencyModalVisible(false)} diff --git a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx index 19b3399caea8..c3c38c4aec72 100644 --- a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx +++ b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx @@ -15,19 +15,19 @@ type PaymentCardCurrencyModalProps = { isVisible: boolean; /** The list of currencies to render */ - currencies: Array>; + currencies: Array>; /** Currently selected currency */ - currentCurrency: ValueOf; + currentCurrency: ValueOf; /** Function to call when the user selects a currency */ - onCurrencyChange?: (currency: ValueOf) => void; + onCurrencyChange?: (currency: ValueOf) => void; /** Function to call when the user closes the currency picker */ onClose?: () => void; }; -function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) { +function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.PAYMENT_CARD_CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) { const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const {translate} = useLocalize(); diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 6cd967e55bbf..f38ea60f1aad 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -32,7 +32,7 @@ type PaymentCardFormProps = { showCurrencyField?: boolean; showStateSelector?: boolean; isDebitCard?: boolean; - addPaymentCard: (values: FormOnyxValues, currency?: ValueOf) => void; + addPaymentCard: (values: FormOnyxValues, currency?: ValueOf) => void; submitButtonText: string; /** Custom content to display in the footer after card form */ footerContent?: ReactNode; @@ -295,7 +295,7 @@ function PaymentCardForm({ diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx index a112b36705c3..4de286183ea8 100644 --- a/src/components/AddPlaidBankAccount.tsx +++ b/src/components/AddPlaidBankAccount.tsx @@ -153,7 +153,7 @@ function AddPlaidBankAccount({ return unsubscribeToNavigationShortcuts; // disabling this rule, as we want this to run only on the first render - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); useEffect(() => { diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 27822fb390a6..7ca4cc3273ca 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -182,6 +182,7 @@ function AddressForm({ InputComponent={CountrySelector} inputID={INPUT_IDS.COUNTRY} value={country} + onValueChange={onAddressChanged} shouldSaveDraft={shouldSaveDraft} /> diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 9bd6142b5604..2679a550f72f 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -24,7 +24,24 @@ import CONST from '@src/CONST'; import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; -import type {AddressSearchProps} from './types'; +import type {AddressSearchProps, PredefinedPlace} from './types'; + +/** + * Check if the place matches the search by the place name or description. + * @param search The search string for a place + * @param place The place to check for a match on the search + * @returns true if search is related to place, otherwise it returns false. + */ +function isPlaceMatchForSearch(search: string, place: PredefinedPlace): boolean { + if (!search) { + return true; + } + if (!place) { + return false; + } + const fullSearchSentence = `${place.name ?? ''} ${place.description}`; + return search.split(' ').every((searchTerm) => !searchTerm || fullSearchSentence.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())); +} // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -42,6 +59,7 @@ function AddressSearch( isLimitedToUSA = false, label, maxInputLength, + onFocus, onBlur, onInputChange, onPress, @@ -72,7 +90,7 @@ function AddressSearch( const [isTyping, setIsTyping] = useState(false); const [isFocused, setIsFocused] = useState(false); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const [searchValue, setSearchValue] = useState(value || defaultValue || ''); + const [searchValue, setSearchValue] = useState(''); const [locationErrorCode, setLocationErrorCode] = useState(null); const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false); const shouldTriggerGeolocationCallbacks = useRef(true); @@ -282,7 +300,7 @@ function AddressSearch( // eslint-disable-next-line react/jsx-no-useless-fragment <> {(predefinedPlaces?.length ?? 0) > 0 && ( - <> + {/* This will show current location button in list if there are some recent destinations */} {shouldShowCurrentLocationButton && ( )} {!value && {translate('common.recentDestinations')}} - + )} ); @@ -304,10 +322,16 @@ function AddressSearch( }; }, []); + const filteredPredefinedPlaces = useMemo(() => { + if (!searchValue) { + return predefinedPlaces ?? []; + } + return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []; + }, [predefinedPlaces, searchValue]); + const listEmptyComponent = useCallback( - () => - !!isOffline || !isTyping ? null : {translate('common.noResultsFound')}, - [isOffline, isTyping, styles, translate], + () => (!isTyping ? null : {translate('common.noResultsFound')}), + [isTyping, styles, translate], ); const listLoader = useCallback( @@ -348,7 +372,7 @@ function AddressSearch( fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={predefinedPlaces ?? undefined} + predefinedPlaces={filteredPredefinedPlaces} listEmptyComponent={listEmptyComponent} listLoaderComponent={listLoader} renderHeaderComponent={renderHeaderComponent} @@ -357,7 +381,7 @@ function AddressSearch( const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text; return ( - {!!title && {title}} + {!!title && {title}} {subtitle} ); @@ -391,6 +415,7 @@ function AddressSearch( shouldSaveDraft, onFocus: () => { setIsFocused(true); + onFocus?.(); }, onBlur: (event) => { if (!isCurrentTargetInsideContainer(event, containerRef)) { @@ -420,10 +445,11 @@ function AddressSearch( }} styles={{ textInputContainer: [styles.flexColumn], - listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.overflowAuto, styles.borderLeft, styles.borderRight, !isFocused && {height: 0}], + listView: [StyleUtils.getGoogleListViewStyle(displayListViewBorder), styles.borderLeft, styles.borderRight, !isFocused && styles.h0], row: [styles.pv4, styles.ph3, styles.overflowAuto], description: [styles.googleSearchText], - separator: [styles.googleSearchSeparator], + separator: [styles.googleSearchSeparator, styles.overflowAuto], + container: [styles.mh100], }} numberOfLines={2} isRowScrollable={false} @@ -447,11 +473,13 @@ function AddressSearch( ) } placeholder="" - /> - setLocationErrorCode(null)} - locationErrorCode={locationErrorCode} - /> + listViewDisplayed + > + setLocationErrorCode(null)} + locationErrorCode={locationErrorCode} + /> + {isFetchingCurrentLocation && } diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 82e4c3c3fc37..b654fcad99da 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -23,6 +23,10 @@ type StreetValue = { street: string; }; +type PredefinedPlace = Place & { + name?: string; +}; + type AddressSearchProps = { /** The ID used to uniquely identify the input in a Form */ inputID?: string; @@ -30,6 +34,9 @@ type AddressSearchProps = { /** Saves a draft of the input value when used in a form */ shouldSaveDraft?: boolean; + /** Callback that is called when the text input is focused */ + onFocus?: () => void; + /** Callback that is called when the text input is blurred */ onBlur?: () => void; @@ -64,7 +71,7 @@ type AddressSearchProps = { canUseCurrentLocation?: boolean; /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces?: Place[] | null; + predefinedPlaces?: PredefinedPlace[] | null; /** A map of inputID key names */ renamedInputKeys?: Address; @@ -84,4 +91,4 @@ type AddressSearchProps = { type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; -export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue}; +export type {CurrentLocationButtonProps, AddressSearchProps, IsCurrentTargetInsideContainerType, StreetValue, PredefinedPlace}; diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index 3319a28c58b9..1eb272dce49a 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -135,7 +135,7 @@ function AmountForm( setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount)); // we want to update only when decimals change (setNewAmount also changes when decimals change). - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [decimals]); /** diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 48604ec364c7..b9aeceeb3621 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -41,7 +41,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow return ( - {({anchor, report, action, checkIfContextMenuActive}) => ( + {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => ( { @@ -53,7 +53,9 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow }} onPressIn={onPressIn} onPressOut={onPressOut} - onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => + showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)) + } shouldUseHapticsOnLongPress accessibilityLabel={displayName} role={CONST.ROLE.BUTTON} diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx index 2fb3e3167ff8..6de7d0c2b013 100644 --- a/src/components/AnimatedStep/index.tsx +++ b/src/components/AnimatedStep/index.tsx @@ -37,6 +37,7 @@ function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN, }} duration={CONST.ANIMATED_TRANSITION} animation={animationStyle} + // eslint-disable-next-line react-compiler/react-compiler useNativeDriver={useNativeDriver} style={style} > diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index df027ed6edb4..b6ea09f32436 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -265,7 +265,7 @@ function AttachmentModal({ } setIsModalOpen(false); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isModalOpen, isConfirmButtonDisabled, onConfirm, file, sourceState]); /** @@ -320,7 +320,7 @@ function AttachmentModal({ } let fileObject = data; if ('getAsFile' in data && typeof data.getAsFile === 'function') { - fileObject = data.getAsFile(); + fileObject = data.getAsFile() as FileObject; } if (!fileObject) { return; @@ -367,7 +367,7 @@ function AttachmentModal({ onModalClose(); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [onModalClose]); /** @@ -428,7 +428,7 @@ function AttachmentModal({ }); } return menuItems; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isReceiptAttachment, 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. @@ -529,6 +529,7 @@ function AttachmentModal({ fallbackSource={fallbackSource} isUsedInAttachmentModal transactionID={transaction?.transactionID} + isUploaded={!isEmptyObject(report)} /> ) diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 154fcf838c86..7d4fbd97f4f7 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -222,6 +222,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s * @param onCanceledHandler A callback that will be called without a selected attachment */ const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => { + // eslint-disable-next-line react-compiler/react-compiler completeAttachmentSelection.current = onPickedHandler; onCanceled.current = onCanceledHandler; setIsVisible(true); diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index a6ff9cb8d27a..669b26724a02 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -46,6 +46,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE}: // Cleanup after selecting a file to start from a fresh state if (fileInput.current) { + // eslint-disable-next-line react-compiler/react-compiler fileInput.current.value = ''; } }} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 2ec1883fd7de..a9b5dfb7feb6 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -74,6 +74,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr ; + + /** Ref to the active attachment */ + pagerRef?: ForwardedRef; + + /** Indicates if the pager is currently scrolling */ isPagerScrolling: SharedValue; + + /** Indicates if scrolling is enabled for the attachment */ isScrollEnabled: SharedValue; + + /** Function to call after a tap event */ onTap: () => void; + + /** Function to call when the scale changes */ onScaleChanged: (scale: number) => void; + + /** Function to call after a swipe down event */ onSwipeDown: () => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx index b7ef9309eb10..0b6b67b38110 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx @@ -1,4 +1,4 @@ -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, SetStateAction} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; @@ -8,6 +8,7 @@ import type {PagerViewProps} from 'react-native-pager-view'; import PagerView from 'react-native-pager-view'; import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated'; import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem'; +import useCarouselContextEvents from '@components/Attachments/AttachmentCarousel/useCarouselContextEvents'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import useThemeStyles from '@hooks/useThemeStyles'; import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext'; @@ -41,24 +42,21 @@ type AttachmentCarouselPagerProps = { >, ) => void; - /** - * A callback that can be used to toggle the attachment carousel arrows, when the scale of the image changes. - * @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows. - */ - onRequestToggleArrows: (showArrows?: boolean) => void; - /** A callback that is called when swipe-down-to-close gesture happens */ onClose: () => void; + + /** Sets the visibility of the arrows. */ + setShouldShowArrows: (show?: SetStateAction) => void; }; function AttachmentCarouselPager( - {items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps, + {items, activeSource, initialPage, setShouldShowArrows, onPageSelected, onClose}: AttachmentCarouselPagerProps, ref: ForwardedRef, ) { + const {handleTap, handleScaleChange} = useCarouselContextEvents(setShouldShowArrows); const styles = useThemeStyles(); const pagerRef = useRef(null); - const scale = useRef(1); const isPagerScrolling = useSharedValue(false); const isScrollEnabled = useSharedValue(true); @@ -68,6 +66,7 @@ function AttachmentCarouselPager( const pageScrollHandler = usePageScrollHandler((e) => { 'worklet'; + // eslint-disable-next-line react-compiler/react-compiler activePage.value = e.position; isPagerScrolling.value = e.offset !== 0; }, []); @@ -78,44 +77,11 @@ function AttachmentCarouselPager( }, [activePage, initialPage]); /** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */ - const pagerItems = useMemo(() => items.map((item, index) => ({source: item.source, index, isActive: index === activePageIndex})), [activePageIndex, items]); - - /** - * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. - * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager, - * as well as enabling/disabling the carousel buttons. - */ - const handleScaleChange = useCallback( - (newScale: number) => { - if (newScale === scale.current) { - return; - } - - scale.current = newScale; - - const newIsScrollEnabled = newScale === 1; - if (isScrollEnabled.value === newIsScrollEnabled) { - return; - } - - isScrollEnabled.value = newIsScrollEnabled; - onRequestToggleArrows(newIsScrollEnabled); - }, - [isScrollEnabled, onRequestToggleArrows], + const pagerItems = useMemo( + () => items.map((item, index) => ({source: item.source, previewSource: item.previewSource, index, isActive: index === activePageIndex})), + [activePageIndex, items], ); - /** - * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. - * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox. - */ - const handleTap = useCallback(() => { - if (!isScrollEnabled.value) { - return; - } - - onRequestToggleArrows(); - }, [isScrollEnabled.value, onRequestToggleArrows]); - const extractItemKey = useCallback( (item: Attachment, index: number) => typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`, diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 1e9c67cf84ac..40438d47ecc7 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -52,6 +52,7 @@ function extractAttachments( if (name === 'img' && attribs.src) { const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); + const previewSource = tryResolveUrlFromApiRoot(attribs.src); if (uniqueSources.has(source)) { return; } @@ -59,6 +60,9 @@ function extractAttachments( uniqueSources.add(source); let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`); + const width = (attribs['data-expensify-width'] && parseInt(attribs['data-expensify-width'], 10)) || undefined; + const height = (attribs['data-expensify-height'] && parseInt(attribs['data-expensify-height'], 10)) || undefined; + // Public image URLs might lack a file extension in the source URL, without an extension our // AttachmentView fails to recognize them as images and renders fallback content instead. // We apply this small hack to add an image extension and ensure AttachmentView renders the image. @@ -72,8 +76,9 @@ function extractAttachments( attachments.unshift({ reportActionID: attribs['data-id'], source, + previewSource, isAuthTokenRequired: !!expensifySource, - file: {name: fileName}, + file: {name: fileName, width, height}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', }); diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index 15740725c42e..e0f7571af8c7 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -57,7 +57,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate(targetAttachments[initialPage]); } } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [reportActions, compareImage]); /** Updates the page state when the user navigates between attachments */ @@ -96,22 +96,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [autoHideArrows, page, updatePage], ); - /** - * Toggles the arrows visibility - * @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value - */ - const toggleArrows = useCallback( - (showArrows?: boolean) => { - if (showArrows === undefined) { - setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows); - return; - } - - setShouldShowArrows(showArrows); - }, - [setShouldShowArrows], - ); - const containerStyles = [styles.flex1, styles.attachmentCarouselContainer]; if (page == null) { @@ -147,7 +131,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, items={attachments} initialPage={page} activeSource={activeSource} - onRequestToggleArrows={toggleArrows} + setShouldShowArrows={setShouldShowArrows} onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)} onClose={onClose} ref={pagerRef} diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index eeac97bc5fa5..f7ef2c6529ce 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,10 +1,12 @@ import isEqual from 'lodash/isEqual'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import type {MutableRefObject} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; import {Keyboard, PixelRatio, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; -import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated'; +import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -22,8 +24,10 @@ import CarouselActions from './CarouselActions'; import CarouselButtons from './CarouselButtons'; import CarouselItem from './CarouselItem'; import extractAttachments from './extractAttachments'; +import AttachmentCarouselPagerContext from './Pager/AttachmentCarouselPagerContext'; import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types'; import useCarouselArrows from './useCarouselArrows'; +import useCarouselContextEvents from './useCarouselContextEvents'; const viewabilityConfig = { // To facilitate paging through the attachments, we want to consider an item "viewable" when it is @@ -33,13 +37,15 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const {isFullScreenRef} = useFullScreenContext(); const scrollRef = useAnimatedRef>>(); + const nope = useSharedValue(false); + const pagerRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -52,6 +58,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); + const {handleTap, handleScaleChange, scale} = useCarouselContextEvents(setShouldShowArrows); + + useEffect(() => { + if (!canUseTouchScreen) { + return; + } + setShouldShowArrows(true); + }, [canUseTouchScreen, page, setShouldShowArrows]); const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]); @@ -101,7 +115,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, scrollRef.current.scrollToIndex({index: page, animated: false}); // The hook is not supposed to run on page change, so we keep the page out of the dependencies - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [cellWidth]); /** Updates the page state when the user navigates between attachments */ @@ -121,7 +135,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, return; } - const item: Attachment = entry.item; + const item = entry.item as Attachment; if (entry.index !== null) { setPage(entry.index); setActiveSource(item.source); @@ -169,6 +183,20 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, [cellWidth], ); + const context = useMemo( + () => ({ + pagerItems: [{source, index: 0, isActive: true}], + activePage: 0, + pagerRef, + isPagerScrolling: nope, + isScrollEnabled: nope, + onTap: handleTap, + onScaleChanged: handleScaleChange, + onSwipeDown: onClose, + }), + [source, nope, handleTap, handleScaleChange, onClose], + ); + /** Defines how a single attachment should be rendered */ const renderItem = useCallback( ({item}: ListRenderItemInfo) => ( @@ -176,20 +204,30 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setShouldShowArrows((oldState) => !oldState) : undefined} + onPress={canUseTouchScreen ? handleTap : undefined} isModalHovered={shouldShowArrows} /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, handleTap, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( () => Gesture.Pan() .enabled(canUseTouchScreen) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) + .onUpdate(({translationX}) => { + if (scale.current !== 1) { + return; + } + + scrollTo(scrollRef, page * cellWidth - translationX, 0, false); + }) .onEnd(({translationX, velocityX}) => { + if (scale.current !== 1) { + return; + } + let newIndex; if (velocityX > MIN_FLING_VELOCITY) { // User flung to the right @@ -204,8 +242,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } scrollTo(scrollRef, newIndex * cellWidth, 0, true); - }), - [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef], + }) + .withRef(pagerRef as MutableRefObject), + [attachments.length, canUseTouchScreen, cellWidth, page, scale, scrollRef], ); return ( @@ -233,27 +272,28 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, autoHideArrow={autoHideArrows} cancelAutoHideArrow={cancelAutoHideArrows} /> - - - - + + + + + diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts index 12ca3db4e2ff..ed195fd943f1 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts +++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts @@ -32,6 +32,9 @@ function useCarouselArrows() { }, CONST.ARROW_HIDE_DELAY); }, [canUseTouchScreen, cancelAutoHideArrows]); + /** + * Sets the visibility of the arrows. + */ const setShouldShowArrows = useCallback( (show: SetStateAction = true) => { setShouldShowArrowsInternal(show); @@ -42,7 +45,7 @@ function useCarouselArrows() { useEffect(() => { autoHideArrows(); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows}; diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts new file mode 100644 index 000000000000..cc2c3c5c8229 --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts @@ -0,0 +1,64 @@ +import {useCallback, useRef} from 'react'; +import type {SetStateAction} from 'react'; +import {useSharedValue} from 'react-native-reanimated'; + +function useCarouselContextEvents(setShouldShowArrows: (show?: SetStateAction) => void) { + const scale = useRef(1); + const isScrollEnabled = useSharedValue(true); + + /** + * Toggles the arrows visibility + */ + const onRequestToggleArrows = useCallback( + (showArrows?: boolean) => { + if (showArrows === undefined) { + setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows); + return; + } + + setShouldShowArrows(showArrows); + }, + [setShouldShowArrows], + ); + + /** + * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. + * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager, + * as well as enabling/disabling the carousel buttons. + */ + const handleScaleChange = useCallback( + (newScale: number) => { + if (newScale === scale.current) { + return; + } + + scale.current = newScale; + + const newIsScrollEnabled = newScale === 1; + if (isScrollEnabled.value === newIsScrollEnabled) { + return; + } + + // eslint-disable-next-line react-compiler/react-compiler + isScrollEnabled.value = newIsScrollEnabled; + onRequestToggleArrows(newIsScrollEnabled); + }, + [isScrollEnabled, onRequestToggleArrows], + ); + + /** + * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext. + * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox. + */ + const handleTap = useCallback(() => { + if (!isScrollEnabled.value) { + return; + } + + onRequestToggleArrows(); + }, [isScrollEnabled.value, onRequestToggleArrows]); + + return {handleTap, handleScaleChange, scale}; +} + +export default useCarouselContextEvents; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx index 765bc2b2a4f2..742b1213fa31 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx @@ -25,7 +25,7 @@ function BaseAttachmentViewPdf({ return; } attachmentCarouselPagerContext.onScaleChanged(1); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); /** diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx index 9f74b5c7bfe8..8c4af3275bd8 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx @@ -41,6 +41,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) { + // eslint-disable-next-line react-compiler/react-compiler isScrollEnabled.value = true; } else if (translateY > SCROLL_THRESHOLD) { isScrollEnabled.value = false; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index e06ea3064150..ee594f66aabc 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -8,6 +8,7 @@ import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; type DefaultAttachmentViewProps = { /** The name of the file */ @@ -21,9 +22,11 @@ type DefaultAttachmentViewProps = { /** Additional styles for the container */ containerStyles?: StyleProp; + + icon?: IconAsset; }; -function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -33,7 +36,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa diff --git a/src/components/Attachments/AttachmentView/HighResolutionInfo.tsx b/src/components/Attachments/AttachmentView/HighResolutionInfo.tsx new file mode 100644 index 000000000000..7ea3c83aa96f --- /dev/null +++ b/src/components/Attachments/AttachmentView/HighResolutionInfo.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; + +function HighResolutionInfo({isUploaded}: {isUploaded: boolean}) { + const theme = useTheme(); + const styles = useThemeStyles(); + const stylesUtils = useStyleUtils(); + const {translate} = useLocalize(); + + return ( + + + {isUploaded ? translate('attachmentPicker.attachmentImageResized') : translate('attachmentPicker.attachmentImageTooLarge')} + + ); +} + +export default HighResolutionInfo; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index a7409e57f846..39c25706bbfe 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -8,6 +8,7 @@ import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; @@ -17,6 +18,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import type {ColorValue} from '@styles/utils/types'; import variables from '@styles/variables'; @@ -26,6 +28,7 @@ import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; import AttachmentViewVideo from './AttachmentViewVideo'; import DefaultAttachmentView from './DefaultAttachmentView'; +import HighResolutionInfo from './HighResolutionInfo'; type AttachmentViewOnyxProps = { transaction: OnyxEntry; @@ -70,10 +73,14 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /** Whether the attachment is used as a chat attachment */ isUsedAsChatAttachment?: boolean; + + /* Flag indicating whether the attachment has been uploaded. */ + isUploaded?: boolean; }; function AttachmentView({ source, + previewSource, file, isAuthTokenRequired, onPress, @@ -92,6 +99,7 @@ function AttachmentView({ isHovered, duration, isUsedAsChatAttachment, + isUploaded = true, }: AttachmentViewProps) { const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -99,6 +107,7 @@ function AttachmentView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); + const [isHighResolution, setIsHighResolution] = useState(false); const [hasPDFFailedToLoad, setHasPDFFailedToLoad] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name)); @@ -113,6 +122,12 @@ function AttachmentView({ useNetwork({onReconnect: () => setImageError(false)}); + useEffect(() => { + FileUtils.getFileResolution(file).then((resolution) => { + setIsHighResolution(FileUtils.isHighResolutionImage(resolution)); + }); + }, [file]); + // Handles case where source is a component (ex: SVG) or a number // Number may represent a SVG or an image if (typeof source === 'function' || (maybeIcon && typeof source === 'number')) { @@ -196,35 +211,61 @@ function AttachmentView({ // For this check we use both source and file.name since temporary file source is a blob // both PDFs and images will appear as images when pasted into the text field. // We also check for numeric source since this is how static images (used for preview) are represented in RN. - const isImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source)); - if (isImage || (file?.name && Str.isImage(file.name))) { - if (imageError) { - // AttachmentViewImage can't handle icon fallbacks, so we need to handle it here - if (typeof fallbackSource === 'number' || typeof fallbackSource === 'function') { + const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source)); + const isFileNameImage = file?.name && Str.isImage(file.name); + const isFileImage = isSourceImage || isFileNameImage; + + if (isFileImage) { + if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) { + return ( + + ); + } + let imageSource = imageError && fallbackSource ? (fallbackSource as string) : (source as string); + + if (isHighResolution) { + if (!isUploaded) { return ( - + <> + + + + + ); } + imageSource = previewSource?.toString() ?? imageSource; } return ( - { - setImageError(true); - }} - /> + <> + + { + setImageError(true); + }} + /> + + {isHighResolution && } + ); } diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index 835482ca99d9..8bac4cc53af6 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -13,6 +13,9 @@ type Attachment = { /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ source: AttachmentSource; + /** URL to preview-sized attachment that is also used for the thumbnail */ + previewSource?: AttachmentSource; + /** File object can be an instance of File or Object */ file?: FileObject; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx new file mode 100644 index 000000000000..c761faccad39 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx @@ -0,0 +1,47 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import type {PointerEvent} from 'react-native'; +import type PressableProps from '@components/Pressable/GenericPressable/types'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; + +type TransparentOverlayProps = { + resetSuggestions: () => void; +}; + +type OnPressHandler = PressableProps['onPress']; + +function TransparentOverlay({resetSuggestions}: TransparentOverlayProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const onResetSuggestions = useCallback>( + (event) => { + event?.preventDefault(); + resetSuggestions(); + }, + [resetSuggestions], + ); + + const handlePointerDown = useCallback((e: PointerEvent) => { + e?.preventDefault(); + }, []); + + return ( + + + + ); +} + +export default TransparentOverlay; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts index 3ad9bbe7b152..acdc643b6b70 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts @@ -1,5 +1,5 @@ function getBottomSuggestionPadding(): number { - return 0; + return 6; } export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx index 9848d77e479e..9ac43c4d8830 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx @@ -4,9 +4,10 @@ import {View} from 'react-native'; import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; import useStyleUtils from '@hooks/useStyleUtils'; import getBottomSuggestionPadding from './getBottomSuggestionPadding'; +import TransparentOverlay from './TransparentOverlay/TransparentOverlay'; import type {AutoCompleteSuggestionsPortalProps} from './types'; -function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps) { +function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, resetSuggestions = () => {}, ...props}: AutoCompleteSuggestionsPortalProps) { const StyleUtils = useStyleUtils(); const styles = useMemo(() => StyleUtils.getBaseAutoCompleteSuggestionContainerStyle({left, width, bottom: bottom + getBottomSuggestionPadding()}), [StyleUtils, left, width, bottom]); @@ -16,6 +17,7 @@ function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom return ( + {/* eslint-disable-next-line react/jsx-props-no-spreading */} diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx index 2d1d533c2859..d26dd0422368 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx @@ -5,6 +5,7 @@ import {View} from 'react-native'; import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; import useStyleUtils from '@hooks/useStyleUtils'; import getBottomSuggestionPadding from './getBottomSuggestionPadding'; +import TransparentOverlay from './TransparentOverlay/TransparentOverlay'; import type {AutoCompleteSuggestionsPortalProps} from './types'; /** @@ -14,7 +15,13 @@ import type {AutoCompleteSuggestionsPortalProps} from './types'; * On the native platform, tapping on auto-complete suggestions will not blur the main input. */ -function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false { +function AutoCompleteSuggestionsPortal({ + left = 0, + width = 0, + bottom = 0, + resetSuggestions = () => {}, + ...props +}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false { const StyleUtils = useStyleUtils(); const bodyElement = document.querySelector('body'); @@ -31,7 +38,10 @@ function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom !!width && bodyElement && ReactDOM.createPortal( - {componentToRender}, + <> + + {componentToRender} + , bodyElement, ) ); diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 70d70a8c1844..2d22a2560bb0 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -56,6 +56,7 @@ function BaseAutoCompleteSuggestions({ useEffect(() => { if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) { + // eslint-disable-next-line react-compiler/react-compiler fadeInOpacity.value = withTiming(1, { duration: 70, easing: Easing.inOut(Easing.ease), diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 8634d6dd0ca0..41a01fa27c46 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -22,10 +22,25 @@ const measureHeightOfSuggestionRows = (numRows: number, canBeBig: boolean): numb } return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; }; -function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSpaceAboveForSmall: boolean): boolean { - return isEnoughSpaceAboveForBig || isEnoughSpaceAboveForSmall; +function isSuggestionMenuRenderedAbove(isEnoughSpaceAboveForBigMenu: boolean, isEnoughSpaceAboveForSmallMenu: boolean): boolean { + return isEnoughSpaceAboveForBigMenu || isEnoughSpaceAboveForSmallMenu; } +type IsEnoughSpaceToRenderMenuAboveCursor = Pick & { + contentHeight: number; + topInset: number; +}; +function isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight, topInset}: IsEnoughSpaceToRenderMenuAboveCursor): boolean { + return y + (cursorCoordinates.y - scrollValue) > contentHeight + topInset + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; +} + +const initialContainerState = { + width: 0, + left: 0, + bottom: 0, + cursorCoordinates: {x: 0, y: 0}, +}; + /** * On the mobile-web platform, when long-pressing on auto-complete suggestions, * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). @@ -35,20 +50,16 @@ function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSp function AutoCompleteSuggestions({measureParentContainerAndReportCursor = () => {}, ...props}: AutoCompleteSuggestionsProps) { const containerRef = React.useRef(null); const isInitialRender = React.useRef(true); - const isSuggestionAboveRef = React.useRef(false); + const isSuggestionMenuAboveRef = React.useRef(false); const leftValue = React.useRef(0); const prevLeftValue = React.useRef(0); const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions(); const [suggestionHeight, setSuggestionHeight] = React.useState(0); - const [containerState, setContainerState] = React.useState({ - width: 0, - left: 0, - bottom: 0, - }); + const [containerState, setContainerState] = React.useState(initialContainerState); const StyleUtils = useStyleUtils(); const insets = useSafeAreaInsets(); const {keyboardHeight} = useKeyboardState(); - const {paddingBottom: bottomInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); + const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); useEffect(() => { const container = containerRef.current; @@ -71,53 +82,58 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu return; } + if (!windowHeight || !windowWidth || !suggestionsLength) { + setContainerState(initialContainerState); + return; + } + measureParentContainerAndReportCursor(({x, y, width, scrollValue, cursorCoordinates}: MeasureParentContainerAndCursor) => { const xCoordinatesOfCursor = x + cursorCoordinates.x; - const leftValueForBigScreen = + const bigScreenLeftOffset = xCoordinatesOfCursor + CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH > windowWidth ? windowWidth - CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH : xCoordinatesOfCursor; - - let bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - (keyboardHeight || bottomInset); - const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; - const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true); const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false); - const isEnoughSpaceAboveForBig = windowHeight - bottomValue - contentMaxHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; - const isEnoughSpaceAboveForSmall = windowHeight - bottomValue - contentMinHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; + let bottomValue = windowHeight - (cursorCoordinates.y - scrollValue + y) - keyboardHeight; + const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; + + const isEnoughSpaceToRenderMenuAboveForBig = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMaxHeight, topInset}); + const isEnoughSpaceToRenderMenuAboveForSmall = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMinHeight, topInset}); - const newLeftValue = isSmallScreenWidth ? x : leftValueForBigScreen; + const newLeftOffset = isSmallScreenWidth ? x : bigScreenLeftOffset; // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup - const isAdjustmentNeeded = Math.abs(prevLeftValue.current - leftValueForBigScreen) > 150; + const isAdjustmentNeeded = Math.abs(prevLeftValue.current - bigScreenLeftOffset) > 150; if (isInitialRender.current || isAdjustmentNeeded) { - isSuggestionAboveRef.current = isSuggestionRenderedAbove(isEnoughSpaceAboveForBig, isEnoughSpaceAboveForSmall); - leftValue.current = newLeftValue; + isSuggestionMenuAboveRef.current = isSuggestionMenuRenderedAbove(isEnoughSpaceToRenderMenuAboveForBig, isEnoughSpaceToRenderMenuAboveForSmall); + leftValue.current = newLeftOffset; isInitialRender.current = false; - prevLeftValue.current = newLeftValue; + prevLeftValue.current = newLeftOffset; } let measuredHeight = 0; - if (isSuggestionAboveRef.current && isEnoughSpaceAboveForBig) { + if (isSuggestionMenuAboveRef.current && isEnoughSpaceToRenderMenuAboveForBig) { // calculation for big suggestion box above the cursor measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - } else if (isSuggestionAboveRef.current && isEnoughSpaceAboveForSmall) { + } else if (isSuggestionMenuAboveRef.current && isEnoughSpaceToRenderMenuAboveForSmall) { // calculation for small suggestion box above the cursor measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, false); } else { // calculation for big suggestion box below the cursor measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT - keyboardHeight; } setSuggestionHeight(measuredHeight); setContainerState({ left: leftValue.current, bottom: bottomValue, width: widthValue, + cursorCoordinates, }); }); - }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset]); + }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset, topInset]); - if (containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) { + if ((containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) || (containerState.cursorCoordinates.x === 0 && containerState.cursorCoordinates.y === 0)) { return null; } return ( diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 48bb6b713032..57347cd65abe 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -42,6 +42,9 @@ type AutoCompleteSuggestionsProps = { /** Measures the parent container's position and dimensions. Also add a cursor coordinates */ measureParentContainerAndReportCursor?: (props: MeasureParentContainerAndCursorCallback) => void; + + /** Reset the emoji suggestions */ + resetSuggestions?: () => void; }; export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps, MeasureParentContainerAndCursorCallback, MeasureParentContainerAndCursor}; diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 66814a44cf95..1a606b35f6d2 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -123,6 +123,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose ImageSize.getSize(imageUri).then(({width, height, rotation: orginalRotation}) => { // On Android devices ImageSize library returns also rotation parameter. if (orginalRotation === 90 || orginalRotation === 270) { + // eslint-disable-next-line react-compiler/react-compiler originalImageHeight.value = width; originalImageWidth.value = height; } else { diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index b7f2a64090d6..5bfb0d5f6557 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -57,6 +57,8 @@ function ImageCropView({imageUri = '', containerSize = 0, panGesture = Gesture.P // A reanimated memoized style, which updates when the image's size or scale changes. const imageStyle = useAnimatedStyle(() => { + 'worklet'; + const height = originalImageHeight.value; const width = originalImageWidth.value; const aspectRatio = height > width ? height / width : width / height; diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 9a9da65befa0..bac581da25e6 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -7,6 +7,7 @@ import type {SharedValue} from 'react-native-reanimated'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as Browser from '@libs/Browser'; import ControlSelection from '@libs/ControlSelection'; type SliderProps = { @@ -29,9 +30,13 @@ function Slider({sliderValue, gestureCallbacks}: SliderProps) { // A reanimated memoized style, which tracks // a translateX shared value and updates the slider position. - const rSliderStyle = useAnimatedStyle(() => ({ - transform: [{translateX: sliderValue.value}], - })); + const rSliderStyle = useAnimatedStyle(() => { + 'worklet'; + + return { + transform: [{translateX: sliderValue.value}], + }; + }); const panGesture = Gesture.Pan() .minDistance(5) @@ -62,7 +67,7 @@ function Slider({sliderValue, gestureCallbacks}: SliderProps) { shiftVertical={-2} > {/* pointerEventsNone is a workaround to make sure the pan gesture works correctly on mobile safari */} - + )} diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 15a004ba9b87..eac60bc4ad6d 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -121,9 +121,6 @@ type AvatarWithImagePickerProps = { /** Allows to open an image without Attachment Picker. */ enablePreview?: boolean; - /** Hard disables the "View photo" option */ - shouldDisableViewPhoto?: boolean; - /** Optionally override the default "Edit" icon */ editIcon?: IconAsset; @@ -157,7 +154,6 @@ function AvatarWithImagePicker({ disabled = false, onViewPhotoPress, enablePreview = false, - shouldDisableViewPhoto = false, editIcon = Expensicons.Pencil, shouldUseStyleUtilityForAnchorPosition = false, }: AvatarWithImagePickerProps) { @@ -195,15 +191,15 @@ function AvatarWithImagePicker({ /** * Check if the attachment extension is allowed. */ - const isValidExtension = (image: FileObject): boolean => { + const isValidExtension = useCallback((image: FileObject): boolean => { const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? ''); return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase()); - }; + }, []); /** * Check if the attachment size is less than allowed size. */ - const isValidSize = (image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; + const isValidSize = useCallback((image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE, []); /** * Check if the attachment resolution matches constraints. @@ -216,37 +212,40 @@ function AvatarWithImagePicker({ /** * Validates if an image has a valid resolution and opens an avatar crop modal */ - const showAvatarCropModal = (image: FileObject) => { - if (!isValidExtension(image)) { - setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); - return; - } - if (!isValidSize(image)) { - setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)}); - return; - } - - isValidResolution(image).then((isValid) => { - if (!isValid) { - setError('avatarWithImagePicker.resolutionConstraints', { - minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX, - minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX, - maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX, - maxWidthInPx: CONST.AVATAR_MAX_WIDTH_PX, - }); + const showAvatarCropModal = useCallback( + (image: FileObject) => { + if (!isValidExtension(image)) { + setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); + return; + } + if (!isValidSize(image)) { + setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)}); return; } - setIsAvatarCropModalOpen(true); - setError(null, {}); - setIsMenuVisible(false); - setImageData({ - uri: image.uri ?? '', - name: image.name ?? '', - type: image.type ?? '', + isValidResolution(image).then((isValid) => { + if (!isValid) { + setError('avatarWithImagePicker.resolutionConstraints', { + minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX, + minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX, + maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX, + maxWidthInPx: CONST.AVATAR_MAX_WIDTH_PX, + }); + return; + } + + setIsAvatarCropModalOpen(true); + setError(null, {}); + setIsMenuVisible(false); + setImageData({ + uri: image.uri ?? '', + name: image.name ?? '', + type: image.type ?? '', + }); }); - }); - }; + }, + [isValidExtension, isValidSize], + ); const hideAvatarCropModal = () => { setIsAvatarCropModalOpen(false); @@ -302,61 +301,26 @@ function AvatarWithImagePicker({ }); }, [isMenuVisible, windowWidth]); + const onPressAvatar = useCallback( + (openPicker: OpenPicker) => { + if (isUsingDefaultAvatar) { + openPicker({ + onPicked: showAvatarCropModal, + }); + return; + } + if (disabled && enablePreview && onViewPhotoPress) { + onViewPhotoPress(); + return; + } + setIsMenuVisible((prev) => !prev); + }, + [disabled, enablePreview, isUsingDefaultAvatar, onViewPhotoPress, showAvatarCropModal], + ); + return ( - - - { - if (disabled && enablePreview && onViewPhotoPress) { - onViewPhotoPress(); - return; - } - setIsMenuVisible((prev) => !prev); - }} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={translate('avatarWithImagePicker.editImage')} - disabled={isAvatarCropModalOpen || (disabled && !enablePreview)} - disabledStyle={disabledStyle} - style={[styles.pRelative, avatarStyle, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter]} - ref={anchorRef} - > - - {source ? ( - - ) : ( - - )} - - {!disabled && ( - - - - )} - - - setIsMenuVisible(false)} - onItemSelected={(item, index) => { - setIsMenuVisible(false); - // In order for the file picker to open dynamically, the click - // function must be called from within an event handler that was initiated - // by the user on Safari. - if (index === 0 && Browser.isSafari()) { - openPicker({ - onPicked: showAvatarCropModal, - }); - } - }} - menuItems={menuItems} - anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition} - anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} - withoutOverlay - anchorRef={anchorRef} - /> + <> + + + onPressAvatar(openPicker)} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityLabel={translate('avatarWithImagePicker.editImage')} + disabled={isAvatarCropModalOpen || (disabled && !enablePreview)} + disabledStyle={disabledStyle} + style={[styles.pRelative, avatarStyle, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter]} + ref={anchorRef} + > + + {source ? ( + + ) : ( + + )} + + {!disabled && ( + + + + )} + + + + setIsMenuVisible(false)} + onItemSelected={(item, index) => { + setIsMenuVisible(false); + // In order for the file picker to open dynamically, the click + // function must be called from within an event handler that was initiated + // by the user on Safari. + if (index === 0 && Browser.isSafari()) { + openPicker({ + onPicked: showAvatarCropModal, + }); + } + }} + menuItems={menuItems} + anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition} + anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} + withoutOverlay + anchorRef={anchorRef} + /> + ); }} diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index e641a0c2218a..4cbf85cb0014 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -36,10 +36,11 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { const theme = useTheme(); const styles = useThemeStyles(); const [primaryBreadcrumb, secondaryBreadcrumb] = breadcrumbs; + const isRootBreadcrumb = primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT; const fontScale = PixelRatio.getFontScale() > CONST.LOGO_MAX_SCALE ? CONST.LOGO_MAX_SCALE : PixelRatio.getFontScale(); return ( - {primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT ? ( + {isRootBreadcrumb ? (
      / {secondaryBreadcrumb.text} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 88ae8d48a871..126c81961cee 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,6 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {ActivityIndicator, View} from 'react-native'; import Icon from '@components/Icon'; @@ -89,6 +89,9 @@ type ButtonProps = Partial & { /** Whether we should use the danger theme color */ danger?: boolean; + /** Whether we should display the button as a link */ + link?: boolean; + /** Should we remove the right border radius top + bottom? */ shouldRemoveRightBorderRadius?: boolean; @@ -118,6 +121,9 @@ type ButtonProps = Partial & { /** Whether the button should use split style or not */ isSplitButton?: boolean; + + /** Whether button's content should be centered */ + isContentCentered?: boolean; }; type KeyboardShortcutComponentProps = Pick; @@ -147,7 +153,7 @@ function KeyboardShortcutComponent({isDisabled = false, isLoading = false, onPre priority: enterKeyEventListenerPriority, shouldPreventDefault: false, }), - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps [shouldDisableEnterShortcut, isFocused], ); @@ -202,6 +208,8 @@ function Button( id = '', accessibilityLabel = '', isSplitButton = false, + link = false, + isContentCentered = false, ...rest }: ButtonProps, ref: ForwardedRef, @@ -209,6 +217,7 @@ function Button( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const [isHovered, setIsHovered] = useState(false); const renderContent = () => { if ('children' in rest) { @@ -229,6 +238,10 @@ function Button( danger && styles.buttonDangerText, !!icon && styles.textAlignLeft, textStyles, + link && styles.link, + link && isHovered && StyleUtils.getColorStyle(theme.linkHover), + link && styles.fontWeightNormal, + link && styles.fontSizeLabel, ]} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > @@ -239,7 +252,7 @@ function Button( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (icon || shouldShowRightIcon) { return ( - + {icon && ( @@ -339,6 +352,7 @@ function Button( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing text && shouldShowRightIcon ? styles.alignItemsStretch : undefined, innerStyles, + link && styles.bgTransparent, ]} hoverStyle={[ shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined, @@ -349,6 +363,8 @@ function Button( accessibilityLabel={accessibilityLabel} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} + onHoverIn={() => setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} > {renderContent()} {isLoading && ( diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 094c26a2b387..d1e36327a1a3 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -4,6 +4,7 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -33,6 +34,7 @@ function ButtonWithDropdownMenu({ onOptionSelected, enterKeyEventListenerPriority = 0, wrapperStyle, + useKeyboardShortcuts = false, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -65,6 +67,26 @@ function ButtonWithDropdownMenu({ }); } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, + (e) => { + onPress(e, selectedItem.value); + }, + { + captureOnInputs: true, + shouldBubble: false, + isActive: useKeyboardShortcuts, + }, + ); + + useEffect(() => { + if (!!caretButton.current || !buttonRef?.current || !(shouldAlwaysShowDropdownMenu || options.length > 1)) { + return; + } + caretButton.current = buttonRef.current; + }, [buttonRef, options.length, shouldAlwaysShowDropdownMenu]); + return ( {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -72,9 +94,7 @@ function ButtonWithDropdownMenu({ + )} + + + + + ); +} + +EmptyStateComponent.displayName = 'EmptyStateComponent'; +export default EmptyStateComponent; diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts new file mode 100644 index 000000000000..326b25542f42 --- /dev/null +++ b/src/components/EmptyStateComponent/types.ts @@ -0,0 +1,41 @@ +import type {ImageStyle} from 'expo-image'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import type DotLottieAnimation from '@components/LottieAnimations/types'; +import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; +import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton'; +import type CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton; +type MediaTypes = ValueOf; + +type SharedProps = { + SkeletonComponent: ValidSkeletons; + title: string; + subtitle: string; + buttonText?: string; + buttonAction?: () => void; + headerStyles?: StyleProp; + headerMediaType: T; + headerContentStyles?: StyleProp; +}; + +type MediaType = SharedProps & { + headerMedia: HeaderMedia; +}; + +type VideoProps = MediaType; +type IllustrationProps = MediaType; +type AnimationProps = MediaType; + +type EmptyStateComponentProps = VideoProps | IllustrationProps | AnimationProps; + +type VideoLoadedEventType = { + srcElement: { + videoWidth: number; + videoHeight: number; + }; +}; + +export type {EmptyStateComponentProps, VideoLoadedEventType}; diff --git a/src/components/ErrorMessageRow.tsx b/src/components/ErrorMessageRow.tsx new file mode 100644 index 000000000000..2e6e41449274 --- /dev/null +++ b/src/components/ErrorMessageRow.tsx @@ -0,0 +1,43 @@ +import {mapValues} from 'lodash'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import MessagesRow from './MessagesRow'; + +type ErrorMessageRowProps = { + /** The errors to display */ + errors?: OnyxCommon.Errors | ReceiptErrors | null; + + /** Additional style object for the error row */ + errorRowStyles?: StyleProp; + + /** A function to run when the X button next to the error is clicked */ + onClose?: () => void; + + /** Whether we can dismiss the error message */ + canDismissError?: boolean; +}; + +function ErrorMessageRow({errors, errorRowStyles, onClose, canDismissError = true}: ErrorMessageRowProps) { + // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. + const errorEntries = Object.entries(errors ?? {}); + const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null); + const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error); + const hasErrorMessages = !isEmptyObject(errorMessages); + + return hasErrorMessages ? ( + + ) : null; +} + +ErrorMessageRow.displayName = 'ErrorMessageRow'; + +export default ErrorMessageRow; diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx index 1246367d29e8..c8ce7ee10d6b 100644 --- a/src/components/FlatList/index.android.tsx +++ b/src/components/FlatList/index.android.tsx @@ -22,7 +22,7 @@ function CustomFlatList(props: FlatListProps, ref: ForwardedRef) } }, [scrollPosition?.offset, ref]); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent) => setScrollPosition({offset: event.nativeEvent.contentOffset.y}), []); useFocusEffect( diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx index f54eddcbeb79..b45a8418d9a3 100644 --- a/src/components/FlatList/index.tsx +++ b/src/components/FlatList/index.tsx @@ -54,7 +54,6 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false return horizontal ? getScrollableNode(scrollRef.current)?.scrollLeft ?? 0 : getScrollableNode(scrollRef.current)?.scrollTop ?? 0; }, [horizontal]); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return const getContentView = useCallback(() => getScrollableNode(scrollRef.current)?.childNodes[0], []); const scrollToOffset = useCallback( @@ -151,10 +150,13 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false if (!isListRenderedRef.current) { return; } - requestAnimationFrame(() => { + const animationFrame = requestAnimationFrame(() => { prepareForMaintainVisibleContentPosition(); setupMutationObserver(); }); + return () => { + cancelAnimationFrame(animationFrame); + }; }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, ref); @@ -177,6 +179,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false const mutationObserver = mutationObserverRef.current; return () => { mutationObserver?.disconnect(); + mutationObserverRef.current = null; }; }, []); @@ -200,6 +203,10 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false ref={onRef} onLayout={(e) => { isListRenderedRef.current = true; + if (!mutationObserverRef.current) { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + } props.onLayout?.(e); }} /> diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 93ffa52bc80b..7c2f5579332a 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -63,6 +63,7 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const buttonRef = ref; useEffect(() => { + // eslint-disable-next-line react-compiler/react-compiler sharedValue.value = withTiming(isActive ? 1 : 0, { duration: 340, easing: Easing.inOut(Easing.ease), diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx index be5da8c49a78..00dcedd32aa2 100644 --- a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx @@ -1,6 +1,7 @@ import FocusTrap from 'focus-trap-react'; import React from 'react'; import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import type FocusTrapForModalProps from './FocusTrapForModalProps'; function FocusTrapForModal({children, active}: FocusTrapForModalProps) { @@ -12,6 +13,12 @@ function FocusTrapForModal({children, active}: FocusTrapForModalProps) { clickOutsideDeactivates: true, initialFocus: false, fallbackFocus: document.body, + setReturnFocus: (element) => { + if (ReportActionComposeFocusManager.isFocused()) { + return false; + } + return element; + }, }} > {children} diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index 6a1409ab4a93..e7fe135c952c 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -1,15 +1,14 @@ -import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; +import {useIsFocused, useRoute} from '@react-navigation/native'; import FocusTrap from 'focus-trap-react'; -import React, {useCallback, useMemo} from 'react'; +import React, {useMemo} from 'react'; import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; -import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; import type FocusTrapProps from './FocusTrapProps'; -let activeRouteName = ''; function FocusTrapForScreen({children}: FocusTrapProps) { const isFocused = useIsFocused(); const route = useRoute(); @@ -33,12 +32,6 @@ function FocusTrapForScreen({children}: FocusTrapProps) { return true; }, [isFocused, isSmallScreenWidth, route.name]); - useFocusEffect( - useCallback(() => { - activeRouteName = route.name; - }, [route]), - ); - return ( { - if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + delayInitialFocus: CONST.ANIMATED_TRANSITION, + initialFocus: (focusTrapContainers) => { + const isFocusedElementInsideContainer = focusTrapContainers?.some((container) => container.contains(document.activeElement)); + if (isFocusedElementInsideContainer) { return false; } return undefined; }, setReturnFocus: (element) => { - if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + if (document.activeElement && document.activeElement !== document.body) { return false; } return element; diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts deleted file mode 100644 index 2a77b52e3116..000000000000 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {CENTRAL_PANE_WORKSPACE_SCREENS} from '@libs/Navigation/AppNavigator/Navigators/FullScreenNavigator'; -import SCREENS from '@src/SCREENS'; - -const SCREENS_WITH_AUTOFOCUS: string[] = [ - ...Object.keys(CENTRAL_PANE_WORKSPACE_SCREENS), - SCREENS.REPORT, - SCREENS.REPORT_DESCRIPTION_ROOT, - SCREENS.PRIVATE_NOTES.EDIT, - SCREENS.SETTINGS.PROFILE.STATUS, - SCREENS.SETTINGS.PROFILE.PRONOUNS, - SCREENS.NEW_TASK.DETAILS, - SCREENS.MONEY_REQUEST.CREATE, -]; - -export default SCREENS_WITH_AUTOFOCUS; diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index be772c6ae10c..551a30cba46e 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -31,6 +31,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.WORKSPACE.TAGS, SCREENS.WORKSPACE.TAXES, SCREENS.WORKSPACE.REPORT_FIELDS, + SCREENS.WORKSPACE.EXPENSIFY_CARD, SCREENS.WORKSPACE.DISTANCE_RATES, SCREENS.SEARCH.CENTRAL_PANE, SCREENS.SETTINGS.TROUBLESHOOT, diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 9df94e4c6114..793154d95b02 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -73,6 +73,9 @@ type FormProviderProps = FormProvider /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; + + /** Whether button is disabled */ + isSubmitDisabled?: boolean; }; function FormProvider( @@ -176,7 +179,7 @@ function FormProvider( onValidate(trimmedStringValues, !hasServerError); // Only run when locales change - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [preferredLocale]); /** @param inputID - The inputID of the input being touched */ @@ -239,6 +242,7 @@ function FormProvider( inputRefs.current[inputID] = newRef; } if (inputProps.value !== undefined) { + // eslint-disable-next-line react-compiler/react-compiler inputValues[inputID] = inputProps.value; } else if (inputProps.shouldSaveDraft && draftValues?.[inputID] !== undefined && inputValues[inputID] === undefined) { inputValues[inputID] = draftValues[inputID]; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 5c74fd466a15..77ef44343792 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -38,6 +38,9 @@ type FormWrapperProps = ChildrenProps & /** Assuming refs are React refs */ inputRefs: RefObject; + /** Whether the submit button is disabled */ + isSubmitDisabled?: boolean; + /** Callback to submit the form */ onSubmit: () => void; }; @@ -57,9 +60,11 @@ function FormWrapper({ enabledWhenOffline, isSubmitActionDangerous = false, formID, + shouldUseScrollView = true, scrollContextEnabled = false, shouldHideFixErrorsAlert = false, disablePressOnEnter = true, + isSubmitDisabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -108,6 +113,7 @@ function FormWrapper({ {isSubmitButtonVisible && ( {({safeAreaPaddingBottomStyle}) => diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index b5535a2fe6c1..c966dd4456e9 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -54,7 +54,7 @@ function computeComponentSpecificRegistrationParams({ shouldSetTouchedOnBlurOnly: false, // Forward the originally provided value blurOnSubmit, - shouldSubmitForm: false, + shouldSubmitForm: !!shouldSubmitForm, }; } diff --git a/src/components/Form/SafariFormWrapper.tsx b/src/components/Form/SafariFormWrapper.tsx new file mode 100644 index 000000000000..8ad411e547be --- /dev/null +++ b/src/components/Form/SafariFormWrapper.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {isSafari} from '@libs/Browser'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type SafariFormWrapperProps = ChildrenProps; + +/** + * If we used any without
      wrapper, Safari 11+ would show the auto-fill suggestion popup. + */ +function SafariFormWrapper({children}: SafariFormWrapperProps) { + if (isSafari()) { + return {children}
      ; + } + + return children; +} + +export default SafariFormWrapper; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 6245fdcf7b49..5f56bbeceea6 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -20,6 +20,10 @@ import type TextInput from '@components/TextInput'; import type TextPicker from '@components/TextPicker'; import type ValuePicker from '@components/ValuePicker'; import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker'; +import type DimensionTypeSelector from '@pages/workspace/accounting/intacct/import/DimensionTypeSelector'; +import type NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker'; +import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker'; +import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm'; import type {Country} from '@src/CONST'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {BaseForm} from '@src/types/form/Form'; @@ -39,6 +43,7 @@ type ValidInputs = | typeof CurrencySelector | typeof AmountForm | typeof BusinessTypePicker + | typeof DimensionTypeSelector | typeof StateSelector | typeof RoomNameInput | typeof ValuePicker @@ -47,14 +52,19 @@ type ValidInputs = | typeof AmountPicker | typeof TextPicker | typeof AddPlaidBankAccount - | typeof EmojiPickerButtonDropdown; + | typeof EmojiPickerButtonDropdown + | typeof NetSuiteCustomListPicker + | typeof NetSuiteCustomFieldMappingPicker + | typeof NetSuiteMenuWithTopDescriptionForm; -type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country'; +type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues'; type ValueTypeMap = { string: string; boolean: boolean; date: Date; country: Country | ''; + reportFields: string[]; + disabledListValues: boolean[]; }; type FormValue = ValueOf; @@ -124,6 +134,9 @@ type FormProps = { /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ scrollContextEnabled?: boolean; + /** Whether to use ScrollView */ + shouldUseScrollView?: boolean; + /** Container styles */ style?: StyleProp; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index d95fc9e11a31..bab66dfab911 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -42,6 +42,11 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {...styles.colorMuted, ...styles.mb0}, contentModel: HTMLContentModel.block, }), + 'muted-text-label': HTMLElementModel.fromCustomModel({ + tagName: 'muted-text-label', + mixedUAStyles: {...styles.mutedNormalTextLabel, ...styles.mb0}, + contentModel: HTMLContentModel.block, + }), comment: HTMLElementModel.fromCustomModel({ tagName: 'comment', mixedUAStyles: {whiteSpace: 'pre'}, @@ -83,7 +88,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim contentModel: HTMLContentModel.block, }), }), - [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4], + [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4, styles.mutedNormalTextLabel], ); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index 67bbc7986bc7..1e73cce1630f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -78,7 +78,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { thumbnailImageComponent ) : ( - {({anchor, report, action, checkIfContextMenuActive}) => ( + {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => ( {({reportID, accountID, type}) => ( showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => + showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)) + } shouldUseHapticsOnLongPress accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index e5481c5d9094..ffab2434c83c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -83,10 +83,12 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona return ( - {({anchor, report, action, checkIfContextMenuActive}) => ( + {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => ( showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => + showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)) + } onPress={(event) => { event.preventDefault(); Navigation.navigate(navigationRoute); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index 4d1e58a42830..14666798e8c7 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -34,12 +34,14 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d return ( - {({anchor, report, action, checkIfContextMenuActive}) => ( + {({anchor, report, reportNameValuePairs, action, checkIfContextMenuActive}) => ( {})} onPressIn={onPressIn} onPressOut={onPressOut} - onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => + showContextMenuForReport(event, anchor, report?.reportID ?? '-1', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report, reportNameValuePairs)) + } shouldUseHapticsOnLongPress role={CONST.ROLE.PRESENTATION} accessibilityLabel={translate('accessibilityHints.prestyledText')} diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index abd48d432953..fd3d4f3d19e8 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -48,7 +48,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez return; } - const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { + const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling: boolean) => { isScrollingRef.current = scrolling; if (!isScrollingRef.current) { setIsHovered(isHoveredRef.current); @@ -102,7 +102,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez const child = useMemo(() => getReturnValue(children, !isScrollingRef.current && isHovered), [children, isHovered]); - const {onMouseEnter, onMouseLeave, onMouseMove, onBlur}: OnMouseEvents = child.props; + const {onMouseEnter, onMouseLeave, onMouseMove, onBlur} = child.props as OnMouseEvents; const hoverAndForwardOnMouseEnter = useCallback( (e: MouseEvent) => { diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware.tsx deleted file mode 100644 index 5c6934f4fc3d..000000000000 --- a/src/components/HybridAppMiddleware.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import {useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {NativeModules} from 'react-native'; -import useSplashScreen from '@hooks/useSplashScreen'; -import BootSplash from '@libs/BootSplash'; -import Log from '@libs/Log'; -import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import * as Welcome from '@userActions/Welcome'; -import CONST from '@src/CONST'; -import type {Route} from '@src/ROUTES'; - -type HybridAppMiddlewareProps = { - children: React.ReactNode; -}; - -type HybridAppMiddlewareContextType = { - navigateToExitUrl: (exitUrl: Route) => void; - showSplashScreenOnNextStart: () => void; -}; -const HybridAppMiddlewareContext = React.createContext({ - navigateToExitUrl: () => {}, - showSplashScreenOnNextStart: () => {}, -}); - -/* - * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. - * It is crucial to make transitions between OldDot and NewDot look smooth. - */ -function HybridAppMiddleware(props: HybridAppMiddlewareProps) { - const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); - const [startedTransition, setStartedTransition] = useState(false); - const [finishedTransition, setFinishedTransition] = useState(false); - const navigation = useNavigation>(); - - /* - * Handles navigation during transition from OldDot. For ordinary NewDot app it is just pure navigation. - */ - const navigateToExitUrl = useCallback((exitUrl: Route) => { - if (NativeModules.HybridAppModule) { - setStartedTransition(true); - Log.info(`[HybridApp] Started transition to ${exitUrl}`, true); - } - - Navigation.navigate(exitUrl); - }, []); - - /** - * This function only affects iOS. If during a single app lifecycle we frequently transition from OldDot to NewDot, - * we need to artificially show the bootsplash because the app is only booted once. - */ - const showSplashScreenOnNextStart = useCallback(() => { - setIsSplashHidden(false); - setStartedTransition(false); - setFinishedTransition(false); - }, [setIsSplashHidden]); - - useEffect(() => { - if (!finishedTransition || isSplashHidden) { - return; - } - - Log.info('[HybridApp] Finished transtion', true); - BootSplash.hide().then(() => { - setIsSplashHidden(true); - Log.info('[HybridApp] Handling onboarding flow', true); - Welcome.handleHybridAppOnboarding(); - }); - }, [finishedTransition, isSplashHidden, setIsSplashHidden]); - - useEffect(() => { - if (!startedTransition) { - return; - } - - // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout. - const timeout = setTimeout(() => { - setFinishedTransition(true); - }, CONST.SCREEN_TRANSITION_END_TIMEOUT); - - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - clearTimeout(timeout); - setFinishedTransition(true); - }); - - return () => { - clearTimeout(timeout); - unsubscribeTransitionEnd(); - }; - }, [navigation, startedTransition]); - - const contextValue = useMemo( - () => ({ - navigateToExitUrl, - showSplashScreenOnNextStart, - }), - [navigateToExitUrl, showSplashScreenOnNextStart], - ); - - return {props.children}; -} - -HybridAppMiddleware.displayName = 'HybridAppMiddleware'; - -export default HybridAppMiddleware; -export type {HybridAppMiddlewareContextType}; -export {HybridAppMiddlewareContext}; diff --git a/src/components/HybridAppMiddleware/index.ios.tsx b/src/components/HybridAppMiddleware/index.ios.tsx new file mode 100644 index 000000000000..1ea8e62ab17e --- /dev/null +++ b/src/components/HybridAppMiddleware/index.ios.tsx @@ -0,0 +1,161 @@ +import type React from 'react'; +import {useContext, useEffect, useState} from 'react'; +import {NativeEventEmitter, NativeModules} from 'react-native'; +import type {NativeModule} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import useExitTo from '@hooks/useExitTo'; +import useSplashScreen from '@hooks/useSplashScreen'; +import BootSplash from '@libs/BootSplash'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SessionUtils from '@libs/SessionUtils'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridAppRoute, Route} from '@src/ROUTES'; +import type {TryNewDot} from '@src/types/onyx'; + +type HybridAppMiddlewareProps = { + authenticated: boolean; + children: React.ReactNode; +}; + +const onboardingStatusSelector = (tryNewDot: OnyxEntry) => { + let completedHybridAppOnboarding = tryNewDot?.classicRedirect?.completedHybridAppOnboarding; + + if (typeof completedHybridAppOnboarding === 'string') { + completedHybridAppOnboarding = completedHybridAppOnboarding === 'true'; + } + + return completedHybridAppOnboarding; +}; + +/* + * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. + * It is crucial to make transitions between OldDot and NewDot look smooth. + * The middleware assumes that the entry point for HybridApp is the /transition route. + */ +function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) { + const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); + const [startedTransition, setStartedTransition] = useState(false); + const [finishedTransition, setFinishedTransition] = useState(false); + + const initialURL = useContext(InitialURLContext); + const exitToParam = useExitTo(); + const [exitTo, setExitTo] = useState(); + + const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector}); + + /** + * This useEffect tracks changes of `nvp_tryNewDot` value. + * We propagate it from OldDot to NewDot with native method due to limitations of old app. + */ + useEffect(() => { + if (completedHybridAppOnboarding === undefined) { + return; + } + + if (!NativeModules.HybridAppModule) { + Log.hmmm(`[HybridApp] Onboarding status has changed, but the HybridAppModule is not defined`); + return; + } + + Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true, {completedHybridAppOnboarding}); + NativeModules.HybridAppModule.completeOnboarding(completedHybridAppOnboarding); + }, [completedHybridAppOnboarding]); + + // In iOS, the HybridApp defines the `onReturnToOldDot` event. + // If we frequently transition from OldDot to NewDot during a single app lifecycle, + // we need to artificially display the bootsplash since the app is booted only once. + // Therefore, isSplashHidden needs to be updated at the appropriate time. + useEffect(() => { + if (!NativeModules.HybridAppModule) { + return; + } + const HybridAppEvents = new NativeEventEmitter(NativeModules.HybridAppModule as unknown as NativeModule); + const listener = HybridAppEvents.addListener(CONST.EVENTS.ON_RETURN_TO_OLD_DOT, () => { + Log.info('[HybridApp] `onReturnToOldDot` event received. Resetting state of HybridAppMiddleware', true); + setIsSplashHidden(false); + setStartedTransition(false); + setFinishedTransition(false); + setExitTo(undefined); + }); + + return () => { + listener.remove(); + }; + }, [setIsSplashHidden]); + + // Save `exitTo` when we reach /transition route. + // `exitTo` should always exist during OldDot -> NewDot transitions. + useEffect(() => { + if (!NativeModules.HybridAppModule || !exitToParam || exitTo) { + return; + } + + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam}); + setExitTo(exitToParam); + + Log.info(`[HybridApp] Started transition`, true); + setStartedTransition(true); + }, [exitTo, exitToParam]); + + useEffect(() => { + if (!startedTransition || finishedTransition) { + return; + } + + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); + + // We need to wait with navigating to exitTo until all login-related actions are complete. + if (!authenticated || isLoggingInAsNewUser || isAccountLoading) { + return; + } + + if (exitTo) { + Navigation.isNavigationReady().then(() => { + // We need to remove /transition from route history. + // `useExitTo` returns undefined for routes other than /transition. + if (exitToParam) { + Log.info('[HybridApp] Removing /transition route from history', true); + Navigation.goBack(); + } + + Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); + Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setExitTo(undefined); + + setTimeout(() => { + Log.info('[HybridApp] Setting `finishedTransition` to true', true); + setFinishedTransition(true); + }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + }); + } + }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]); + + useEffect(() => { + if (!finishedTransition || isSplashHidden) { + return; + } + + Log.info('[HybridApp] Finished transition, hiding BootSplash', true); + BootSplash.hide().then(() => { + setIsSplashHidden(true); + if (authenticated) { + Log.info('[HybridApp] Handling onboarding flow', true); + Welcome.handleHybridAppOnboarding(); + } + }); + }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); + + return children; +} + +HybridAppMiddleware.displayName = 'HybridAppMiddleware'; + +export default HybridAppMiddleware; diff --git a/src/components/HybridAppMiddleware/index.tsx b/src/components/HybridAppMiddleware/index.tsx new file mode 100644 index 000000000000..5a8d8d6dfebe --- /dev/null +++ b/src/components/HybridAppMiddleware/index.tsx @@ -0,0 +1,133 @@ +import type React from 'react'; +import {useContext, useEffect, useState} from 'react'; +import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import useExitTo from '@hooks/useExitTo'; +import useSplashScreen from '@hooks/useSplashScreen'; +import BootSplash from '@libs/BootSplash'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SessionUtils from '@libs/SessionUtils'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridAppRoute, Route} from '@src/ROUTES'; +import type {TryNewDot} from '@src/types/onyx'; + +type HybridAppMiddlewareProps = { + authenticated: boolean; + children: React.ReactNode; +}; + +const onboardingStatusSelector = (tryNewDot: OnyxEntry) => { + let completedHybridAppOnboarding = tryNewDot?.classicRedirect?.completedHybridAppOnboarding; + + if (typeof completedHybridAppOnboarding === 'string') { + completedHybridAppOnboarding = completedHybridAppOnboarding === 'true'; + } + + return completedHybridAppOnboarding; +}; + +/* + * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. + * It is crucial to make transitions between OldDot and NewDot look smooth. + * The middleware assumes that the entry point for HybridApp is the /transition route. + */ +function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) { + const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); + const [startedTransition, setStartedTransition] = useState(false); + const [finishedTransition, setFinishedTransition] = useState(false); + + const initialURL = useContext(InitialURLContext); + const exitToParam = useExitTo(); + const [exitTo, setExitTo] = useState(); + + const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector}); + + /** + * This useEffect tracks changes of `nvp_tryNewDot` value. + * We propagate it from OldDot to NewDot with native method due to limitations of old app. + */ + useEffect(() => { + if (completedHybridAppOnboarding === undefined || !NativeModules.HybridAppModule) { + return; + } + + Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true, {completedHybridAppOnboarding}); + NativeModules.HybridAppModule.completeOnboarding(completedHybridAppOnboarding); + }, [completedHybridAppOnboarding]); + + // Save `exitTo` when we reach /transition route. + // `exitTo` should always exist during OldDot -> NewDot transitions. + useEffect(() => { + if (!NativeModules.HybridAppModule || !exitToParam || exitTo) { + return; + } + + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam}); + setExitTo(exitToParam); + + Log.info(`[HybridApp] Started transition`, true); + setStartedTransition(true); + }, [exitTo, exitToParam]); + + useEffect(() => { + if (!startedTransition || finishedTransition) { + return; + } + + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); + + // We need to wait with navigating to exitTo until all login-related actions are complete. + if (!authenticated || isLoggingInAsNewUser || isAccountLoading) { + return; + } + + if (exitTo) { + Navigation.isNavigationReady().then(() => { + // We need to remove /transition from route history. + // `useExitTo` returns undefined for routes other than /transition. + if (exitToParam) { + Log.info('[HybridApp] Removing /transition route from history', true); + Navigation.goBack(); + } + + Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); + Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setExitTo(undefined); + + setTimeout(() => { + Log.info('[HybridApp] Setting `finishedTransition` to true', true); + setFinishedTransition(true); + }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + }); + } + }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]); + + useEffect(() => { + if (!finishedTransition || isSplashHidden) { + return; + } + + Log.info('[HybridApp] Finished transition, hiding BootSplash', true); + BootSplash.hide().then(() => { + setIsSplashHidden(true); + if (authenticated) { + Log.info('[HybridApp] Handling onboarding flow', true); + Welcome.handleHybridAppOnboarding(); + } + }); + }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); + + return children; +} + +HybridAppMiddleware.displayName = 'HybridAppMiddleware'; + +export default HybridAppMiddleware; diff --git a/src/components/IFrame.tsx b/src/components/IFrame.tsx index 05da3a1edb9c..f492df0f3866 100644 --- a/src/components/IFrame.tsx +++ b/src/components/IFrame.tsx @@ -17,7 +17,7 @@ function getNewDotURL(url: string): string { let params: Record; try { - params = JSON.parse(paramString); + params = JSON.parse(paramString) as Record; } catch { params = {}; } diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index a0d7a5cb8883..487df5594212 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -42,6 +42,7 @@ import ChatBubbles from '@assets/images/chatbubbles.svg'; import CheckCircle from '@assets/images/check-circle.svg'; import CheckmarkCircle from '@assets/images/checkmark-circle.svg'; import Checkmark from '@assets/images/checkmark.svg'; +import CircularArrowBackwards from '@assets/images/circular-arrow-backwards.svg'; import Close from '@assets/images/close.svg'; import ClosedSign from '@assets/images/closed-sign.svg'; import Coins from '@assets/images/coins.svg'; @@ -201,6 +202,7 @@ export { Wrench, BackArrow, Bank, + CircularArrowBackwards, Bill, Bell, BellSlash, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index e699badc43ec..6499e8eceb6e 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -1,3 +1,5 @@ +import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg'; +import LaptopwithSecondScreenandHourglass from '@assets/images/LaptopwithSecondScreenandHourglass.svg'; import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg'; import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg'; import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg'; @@ -7,6 +9,7 @@ import ConciergeExclamation from '@assets/images/product-illustrations/concierge import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg'; import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg'; import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg'; +import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg'; import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg'; import Hands from '@assets/images/product-illustrations/home-illustration-hands.svg'; import InvoiceOrange from '@assets/images/product-illustrations/invoice--orange.svg'; @@ -51,6 +54,7 @@ import ConciergeNew from '@assets/images/simple-illustrations/simple-illustratio import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg'; import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg'; +import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg'; import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg'; import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg'; import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg'; @@ -76,6 +80,7 @@ import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__ import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; +import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg'; import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg'; import ReceiptUpload from '@assets/images/simple-illustrations/simple-illustration__receiptupload.svg'; import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg'; @@ -88,9 +93,11 @@ import SubscriptionPPU from '@assets/images/simple-illustrations/simple-illustra import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg'; import TeachersUnite from '@assets/images/simple-illustrations/simple-illustration__teachers-unite.svg'; import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; +import Tire from '@assets/images/simple-illustrations/simple-illustration__tire.svg'; import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; +import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg'; import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg'; import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg'; import ExpensifyApprovedLogoLight from '@assets/images/subscription-details__approvedlogo--light.svg'; @@ -140,6 +147,7 @@ export { PinkBill, CreditCardsNew, InvoiceBlue, + LaptopwithSecondScreenandHourglass, LockOpen, Luggage, MoneyIntoWallet, @@ -176,6 +184,7 @@ export { Binoculars, CompanyCard, ReceiptUpload, + ExpensifyCardIllustration, SplitBill, PiggyBank, Accounting, @@ -184,6 +193,7 @@ export { Pencil, Tag, CarIce, + ReceiptLocationMarker, Lightbulb, EmptyStateTravel, SubscriptionAnnual, @@ -194,4 +204,8 @@ export { CheckmarkCircle, CreditCardEyes, LockClosedOrange, + EmptyState, + FolderWithPapers, + VirtualCard, + Tire, }; diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index f3cbc332c995..5fe1ba306400 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -58,7 +58,7 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, onLoa } return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); /** diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index c74d9bd5aa52..e12be53d01ae 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -7,6 +7,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; +import Lightbox from '@components/Lightbox'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -200,25 +201,11 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV if (canUseTouchScreen) { return ( - - 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} - onLoadStart={imageLoadingStart} - onLoad={imageLoad} - onError={onError} - /> - {((isLoading && (!isOffline || isLocalFile)) || (!isLoading && zoomScale === 0)) && } - {isLoading && !isLocalFile && } - + ); } return ( diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 8830681bc55f..0d269d1ca593 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as UserUtils from '@libs/UserUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,13 +55,14 @@ function Indicator({reimbursementAccount, policies, bankAccountList, fundList, u () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), () => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError), + () => SubscriptionUtils.hasSubscriptionRedDotError(), () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, () => !!loginList && UserUtils.hasLoginListError(loginList), // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) () => Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID, ]; - const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList)]; + const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList), () => SubscriptionUtils.hasSubscriptionGreenDotInfo()]; const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod()); const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()); diff --git a/src/components/InteractiveStepSubHeader.tsx b/src/components/InteractiveStepSubHeader.tsx index 20b3f6bc79a4..d8899a317df5 100644 --- a/src/components/InteractiveStepSubHeader.tsx +++ b/src/components/InteractiveStepSubHeader.tsx @@ -25,6 +25,9 @@ type InteractiveStepSubHeaderProps = { type InteractiveStepSubHeaderHandle = { /** Move to the next step */ moveNext: () => void; + + /** Move to the previous step */ + movePrevious: () => void; }; const MIN_AMOUNT_FOR_EXPANDING = 3; @@ -45,6 +48,9 @@ function InteractiveStepSubHeader({stepNames, startStepIndex = 0, onStepSelected moveNext: () => { setCurrentStep((actualStep) => actualStep + 1); }, + movePrevious: () => { + setCurrentStep((actualStep) => actualStep - 1); + }, }), [], ); diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index c7797a37fd12..431a12d00106 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Badge from '@components/Badge'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -20,11 +21,12 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; -import {parseHtmlToText} from '@libs/OnyxAwareParser'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Parser from '@libs/Parser'; import Performance from '@libs/Performance'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -227,6 +229,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti ReportUtils.isSystemChat(report) } /> + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} {isStatusVisible && ( {}, opti numberOfLines={1} accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')} > - {parseHtmlToText(optionItem.alternateText)} + {Parser.htmlToText(optionItem.alternateText)} ) : null}
      diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 8d61058ed5be..bb5fdb580aa7 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -47,6 +47,7 @@ function OptionRowLHNData({ policy, parentReportAction, hasViolations: !!shouldDisplayViolations, + transactionViolations, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -57,7 +58,7 @@ function OptionRowLHNData({ return item; // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [ fullReport, lastReportActionTransaction, diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index ea10e104a59d..c5a77f9d5ec4 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -78,7 +78,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan }; } - const foundPage = attachmentCarouselPagerContext.pagerItems.findIndex((item) => item.source === uri); + const foundPage = attachmentCarouselPagerContext.pagerItems.findIndex((item) => item.source === uri || item.previewSource === uri); return { ...attachmentCarouselPagerContext, isUsedInCarousel: !!attachmentCarouselPagerContext.pagerRef, diff --git a/src/components/LocationPermissionModal/index.android.tsx b/src/components/LocationPermissionModal/index.android.tsx new file mode 100644 index 000000000000..811537e00e67 --- /dev/null +++ b/src/components/LocationPermissionModal/index.android.tsx @@ -0,0 +1,90 @@ +import React, {useEffect, useState} from 'react'; +import {Linking} from 'react-native'; +import {RESULTS} from 'react-native-permissions'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Illustrations from '@components/Icon/Illustrations'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLocationPermission, requestLocationPermission} from '@pages/iou/request/step/IOURequestStepScan/LocationPermission'; +import type {LocationPermissionModalProps} from './types'; + +function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDeny, onGrant}: LocationPermissionModalProps) { + const [hasError, setHasError] = useState(false); + const [showModal, setShowModal] = useState(false); + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + useEffect(() => { + if (!startPermissionFlow) { + return; + } + + getLocationPermission().then((status) => { + if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { + return onGrant(); + } + + setShowModal(true); + setHasError(status === RESULTS.BLOCKED); + }); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes + }, [startPermissionFlow]); + + const handledBlockedPermission = (cb: () => void) => () => { + if (hasError && Linking.openSettings) { + Linking.openSettings(); + setShowModal(false); + setHasError(false); + resetPermissionFlow(); + return; + } + cb(); + }; + + const grantLocationPermission = handledBlockedPermission(() => { + requestLocationPermission().then((status) => { + if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { + onGrant(); + } else if (status === RESULTS.BLOCKED) { + setHasError(true); + return; + } else { + onDeny(status); + } + setShowModal(false); + setHasError(false); + }); + }); + + const skipLocationPermission = () => { + onDeny(RESULTS.DENIED); + setShowModal(false); + setHasError(false); + }; + + return ( + + ); +} + +LocationPermissionModal.displayName = 'LocationPermissionModal'; + +export default LocationPermissionModal; diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx new file mode 100644 index 000000000000..2bc4a7393822 --- /dev/null +++ b/src/components/LocationPermissionModal/index.tsx @@ -0,0 +1,90 @@ +import React, {useEffect, useState} from 'react'; +import {Linking} from 'react-native'; +import {RESULTS} from 'react-native-permissions'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Illustrations from '@components/Icon/Illustrations'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLocationPermission, requestLocationPermission} from '@pages/iou/request/step/IOURequestStepScan/LocationPermission'; +import type {LocationPermissionModalProps} from './types'; + +function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDeny, onGrant}: LocationPermissionModalProps) { + const [hasError, setHasError] = useState(false); + const [showModal, setShowModal] = useState(false); + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + useEffect(() => { + if (!startPermissionFlow) { + return; + } + + getLocationPermission().then((status) => { + if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { + return onGrant(); + } + + setShowModal(true); + setHasError(status === RESULTS.BLOCKED); + }); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes + }, [startPermissionFlow]); + + const handledBlockedPermission = (cb: () => void) => () => { + if (hasError && Linking.openSettings) { + Linking.openSettings(); + setShowModal(false); + setHasError(false); + resetPermissionFlow(); + return; + } + cb(); + }; + + const grantLocationPermission = handledBlockedPermission(() => { + requestLocationPermission() + .then((status) => { + if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { + onGrant(); + } else { + onDeny(status); + } + }) + .finally(() => { + setShowModal(false); + setHasError(false); + }); + }); + + const skipLocationPermission = () => { + onDeny(RESULTS.DENIED); + setShowModal(false); + setHasError(false); + }; + + return ( + + ); +} + +LocationPermissionModal.displayName = 'LocationPermissionModal'; + +export default LocationPermissionModal; diff --git a/src/components/LocationPermissionModal/types.ts b/src/components/LocationPermissionModal/types.ts new file mode 100644 index 000000000000..ec603bfdb8c1 --- /dev/null +++ b/src/components/LocationPermissionModal/types.ts @@ -0,0 +1,19 @@ +import type {PermissionStatus} from 'react-native-permissions'; + +type LocationPermissionModalProps = { + /** A callback to call when the permission has been granted */ + onGrant: () => void; + + /** A callback to call when the permission has been denied */ + onDeny: (permission: PermissionStatus) => void; + + /** Should start the permission flow? */ + startPermissionFlow: boolean; + + /** Reset the permission flow */ + resetPermissionFlow: () => void; +}; + +export default {}; + +export type {LocationPermissionModalProps}; diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index 6395a715f339..a9b223a87a54 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -1,7 +1,7 @@ -import type {LottieViewProps} from 'lottie-react-native'; +import type {AnimationObject, LottieViewProps} from 'lottie-react-native'; import LottieView from 'lottie-react-native'; import type {ForwardedRef} from 'react'; -import React, {forwardRef} from 'react'; +import React, {forwardRef, useEffect, useState} from 'react'; import {View} from 'react-native'; import type DotLottieAnimation from '@components/LottieAnimations/types'; import useAppState from '@hooks/useAppState'; @@ -19,6 +19,12 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef setIsError(false)}); + const [animationFile, setAnimationFile] = useState(); + + useEffect(() => { + setAnimationFile(source.file); + }, [setAnimationFile, source.file]); + const aspectRatioStyle = styles.aspectRatioLottie(source); // If the image fails to load or app is in background state, we'll just render an empty view @@ -28,17 +34,17 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef; } - return ( + return animationFile ? ( setIsError(true)} /> - ); + ) : null; } Lottie.displayName = 'Lottie'; diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index 477ce02cd740..afbc9cd56e28 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -1,80 +1,81 @@ +import type {LottieViewProps} from 'lottie-react-native'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import type DotLottieAnimation from './types'; const DotLottieAnimations = { Abracadabra: { - file: require('@assets/animations/Abracadabra.lottie'), + file: require('@assets/animations/Abracadabra.lottie'), w: 375, h: 400, }, FastMoney: { - file: require('@assets/animations/FastMoney.lottie'), + file: require('@assets/animations/FastMoney.lottie'), w: 375, h: 240, }, Fireworks: { - file: require('@assets/animations/Fireworks.lottie'), + file: require('@assets/animations/Fireworks.lottie'), w: 360, h: 360, }, Hands: { - file: require('@assets/animations/Hands.lottie'), + file: require('@assets/animations/Hands.lottie'), w: 375, h: 375, }, PreferencesDJ: { - file: require('@assets/animations/PreferencesDJ.lottie'), + file: require('@assets/animations/PreferencesDJ.lottie'), w: 375, h: 240, backgroundColor: colors.blue500, }, ReviewingBankInfo: { - file: require('@assets/animations/ReviewingBankInfo.lottie'), + file: require('@assets/animations/ReviewingBankInfo.lottie'), w: 280, h: 280, }, WorkspacePlanet: { - file: require('@assets/animations/WorkspacePlanet.lottie'), + file: require('@assets/animations/WorkspacePlanet.lottie'), w: 375, h: 240, backgroundColor: colors.pink800, }, SaveTheWorld: { - file: require('@assets/animations/SaveTheWorld.lottie'), + file: require('@assets/animations/SaveTheWorld.lottie'), w: 375, h: 240, }, Safe: { - file: require('@assets/animations/Safe.lottie'), + file: require('@assets/animations/Safe.lottie'), w: 625, h: 400, backgroundColor: colors.ice500, }, Magician: { - file: require('@assets/animations/Magician.lottie'), + file: require('@assets/animations/Magician.lottie'), w: 853, h: 480, }, Update: { - file: require('@assets/animations/Update.lottie'), + file: require('@assets/animations/Update.lottie'), w: variables.updateAnimationW, h: variables.updateAnimationH, }, Coin: { - file: require('@assets/animations/Coin.lottie'), + file: require('@assets/animations/Coin.lottie'), w: 375, h: 240, backgroundColor: colors.yellow600, }, Desk: { - file: require('@assets/animations/Desk.lottie'), + file: require('@assets/animations/Desk.lottie'), w: 200, h: 120, backgroundColor: colors.blue700, }, Plane: { - file: require('@assets/animations/Plane.lottie'), + file: require('@assets/animations/Plane.lottie'), w: 180, h: 200, }, diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 6239243cb5ab..2fae3cc89597 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -192,7 +192,7 @@ function MagicCodeInput( // We have not added: // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. // + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [value, shouldSubmitOnComplete]); /** @@ -298,7 +298,7 @@ function MagicCodeInput( // Fill the array with empty characters if there are no inputs. if (focusedIndex === 0 && !hasInputs) { - numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); // Deletes the value of the previous input and focuses on it. } else if (focusedIndex && focusedIndex !== 0) { @@ -353,7 +353,7 @@ function MagicCodeInput( // We have not added: // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [lastPressedDigit, isDisableKeyboard]); return ( diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 283f7c396edb..974f58636977 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -37,6 +37,7 @@ const MapView = forwardRef( const currentPosition = userLocation ?? initialLocation; const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const shouldInitializeCurrentPosition = useRef(true); + const [isAccessTokenSet, setIsAccessTokenSet] = useState(false); // Determines if map can be panned to user's detected // location without bothering the user. It will return @@ -138,7 +139,12 @@ const MapView = forwardRef( }, [navigation]); useEffect(() => { - setAccessToken(accessToken); + setAccessToken(accessToken).then((token) => { + if (!token) { + return; + } + setIsAccessTokenSet(true); + }); }, [accessToken]); const setMapIdle = (e: MapState) => { @@ -151,7 +157,8 @@ const MapView = forwardRef( } }; const centerMap = useCallback(() => { - if (directionCoordinates && directionCoordinates.length > 1) { + const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? []; + if (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1) { const {southWest, northEast} = utils.getBounds(waypoints?.map((waypoint) => waypoint.coordinate) ?? [], directionCoordinates); cameraRef.current?.fitBounds(southWest, northEast, mapPadding, CONST.MAPBOX.ANIMATION_DURATION_ON_CENTER_ME); return; @@ -198,7 +205,7 @@ const MapView = forwardRef( const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]); const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]); - return !isOffline && !!accessToken && !!defaultSettings ? ( + return !isOffline && isAccessTokenSet && !!defaultSettings ? ( ( pitchEnabled={pitchEnabled} attributionPosition={{...styles.r2, ...styles.b2}} scaleBarEnabled={false} + // We use scaleBarPosition with top: -32 to hide the scale bar on iOS because scaleBarEnabled={false} not work on iOS + scaleBarPosition={{...styles.tn8, left: 0}} + compassEnabled + compassPosition={{...styles.l2, ...styles.t5}} logoPosition={{...styles.l2, ...styles.b2}} // eslint-disable-next-line react/jsx-props-no-spreading {...responder.panHandlers} diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 01d7134a96da..618dd5b24cf5 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -157,7 +157,7 @@ const MapView = forwardRef( resetBoundaries(); setShouldResetBoundaries(false); - // eslint-disable-next-line react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced }, [shouldResetBoundaries]); useEffect(() => { @@ -194,7 +194,8 @@ const MapView = forwardRef( if (!mapRef) { return; } - if (directionCoordinates && directionCoordinates.length > 1) { + const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? []; + if (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1) { const {northEast, southWest} = utils.getBounds(waypoints?.map((waypoint) => waypoint.coordinate) ?? [], directionCoordinates); const map = mapRef?.getMap(); map?.fitBounds([southWest, northEast], {padding: mapPadding, animate: true, duration: CONST.MAPBOX.ANIMATION_DURATION_ON_CENTER_ME}); diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 1142a90c87d1..bdc19316d491 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -55,6 +55,9 @@ type MentionSuggestionsProps = { /** Measures the parent container's position and dimensions. Also add cursor coordinates */ measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; + + /** Reset the emoji suggestions */ + resetSuggestions: () => void; }; /** @@ -62,7 +65,15 @@ type MentionSuggestionsProps = { */ const keyExtractor = (item: Mention) => item.alternateText; -function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainerAndReportCursor = () => {}}: MentionSuggestionsProps) { +function MentionSuggestions({ + prefix, + mentions, + highlightedMentionIndex = 0, + onSelect, + isMentionPickerLarge, + measureParentContainerAndReportCursor = () => {}, + resetSuggestions, +}: MentionSuggestionsProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -149,6 +160,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe isSuggestionPickerLarge={isMentionPickerLarge} accessibilityLabelExtractor={keyExtractor} measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} + resetSuggestions={resetSuggestions} /> ); } diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8d5fe98bb99d..473806aac3af 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import type {ImageContentFit} from 'expo-image'; import type {ReactElement, ReactNode} from 'react'; import React, {forwardRef, useContext, useMemo} from 'react'; @@ -14,6 +13,7 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; +import Parser from '@libs/Parser'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -254,12 +254,18 @@ type MenuItemBaseProps = { /** Whether should render title as HTML or as Text */ shouldParseTitle?: boolean; + /** Whether should render helper text as HTML or as Text */ + shouldParseHelperText?: boolean; + /** Should check anonymous user in onPress function */ shouldCheckActionAllowedOnPress?: boolean; /** Text to display under the main item */ furtherDetails?: string; + /** Render custom content under the main item */ + furtherDetailsComponent?: ReactElement; + /** The function that should be called when this component is LongPressed or right-clicked. */ onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void; @@ -335,6 +341,7 @@ function MenuItem( iconRight = Expensicons.ArrowRight, furtherDetailsIcon, furtherDetails, + furtherDetailsComponent, description, helperText, helperTextStyle, @@ -372,6 +379,7 @@ function MenuItem( isAnonymousAction = false, shouldBlockSelection = false, shouldParseTitle = false, + shouldParseHelperText = false, shouldCheckActionAllowedOnPress = true, onSecondaryInteraction, titleWithTooltips, @@ -425,10 +433,16 @@ function MenuItem( if (!title || !shouldParseTitle) { return ''; } - const parser = new ExpensiMark(); - return parser.replace(title, {shouldEscapeText}); + return Parser.replace(title, {shouldEscapeText}); }, [title, shouldParseTitle, shouldEscapeText]); + const helperHtml = useMemo(() => { + if (!helperText || !shouldParseHelperText) { + return ''; + } + return Parser.replace(helperText, {shouldEscapeText}); + }, [helperText, shouldParseHelperText, shouldEscapeText]); + const processedTitle = useMemo(() => { let titleToWrap = ''; if (shouldRenderAsHTML) { @@ -442,6 +456,16 @@ function MenuItem( return titleToWrap ? `${titleToWrap}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, html]); + const processedHelperText = useMemo(() => { + let textToWrap = ''; + + if (shouldParseHelperText) { + textToWrap = helperHtml; + } + + return textToWrap ? `${textToWrap}` : ''; + }, [shouldParseHelperText, helperHtml]); + const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); const renderTitleContent = () => { @@ -680,6 +704,7 @@ function MenuItem( )} + {!!furtherDetailsComponent && {furtherDetailsComponent}} {titleComponent}
      @@ -767,7 +792,14 @@ function MenuItem( )} - {!!helperText && {helperText}} + {!!helperText && + (shouldParseHelperText ? ( + + + + ) : ( + {helperText} + ))} diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 2eb073bb39be..d88dde545f3b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -118,7 +118,7 @@ function BaseModal( } hideModal(true); }, - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps [], ); @@ -242,7 +242,9 @@ function BaseModal( deviceWidth={windowWidth} animationIn={animationIn ?? modalStyleAnimationIn} animationOut={animationOut ?? modalStyleAnimationOut} + // eslint-disable-next-line react-compiler/react-compiler useNativeDriver={useNativeDriverProp && useNativeDriver} + // eslint-disable-next-line react-compiler/react-compiler useNativeDriverForBackdrop={useNativeDriverForBackdrop && useNativeDriver} hideModalContentWhileAnimating={hideModalContentWhileAnimating} animationInTiming={animationInTiming} diff --git a/src/components/Modal/ModalContent.tsx b/src/components/Modal/ModalContent.tsx index 49d3b049220f..f71affe760ac 100644 --- a/src/components/Modal/ModalContent.tsx +++ b/src/components/Modal/ModalContent.tsx @@ -14,7 +14,7 @@ type ModalContentProps = { }; function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps React.useEffect(() => () => onDismiss?.(), []); return children; } diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index fa3454ed9e61..1acee187c67b 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -8,8 +8,8 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -22,7 +22,6 @@ import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import Button from './Button'; @@ -36,6 +35,7 @@ import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusB import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; +import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu'; import SettlementButton from './SettlementButton'; type MoneyReportHeaderProps = { @@ -86,29 +86,22 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); + const {isSmallScreenWidth} = useWindowDimensions(); const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); - const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); - const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isOnHold = TransactionUtils.isOnHold(transaction); - const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction); - const canHoldOrUnholdRequest = !isEmptyObject(transaction) && !isSettled && !isApproved && !isDeletedParentAction; // Only the requestor can delete the request, admins can only edit it. const isActionOwner = typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; - const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [paymentType, setPaymentType] = useState(); const [requestType, setRequestType] = useState(); const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy); const policyType = policy?.type; - const isPayer = ReportUtils.isPayer(session, moneyRequestReport); const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); - const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); const navigateBackToAfterDelete = useRef(); const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t)); @@ -117,29 +110,26 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea // allTransactions in TransactionUtils might have stale data const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID, transactions); - const cancelPayment = useCallback(() => { - if (!chatReport) { - return; - } - IOU.cancelPayment(moneyRequestReport, chatReport); - setIsConfirmModalVisible(false); - }, [moneyRequestReport, chatReport]); - const shouldShowPayButton = useMemo(() => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy), [moneyRequestReport, chatReport, policy]); const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, chatReport, policy), [moneyRequestReport, chatReport, policy]); const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation; - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation; + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin; + + const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation && !shouldShowExportIntegrationButton; + const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt; const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; - const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation; + const shouldShowAnyButton = + shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation || shouldShowExportIntegrationButton; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); @@ -197,22 +187,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea TransactionActions.markAsCash(iouTransactionID, reportID); }, [requestParentReportAction, transactionThreadReport?.reportID]); - const changeMoneyRequestStatus = () => { - if (!transactionThreadReport) { - return; - } - const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) - ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID ?? '-1' - : '-1'; - - if (isOnHold) { - IOU.unholdRequest(iouTransactionID, transactionThreadReport.reportID); - } else { - const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, iouTransactionID, transactionThreadReport.reportID, activeRoute)); - } - }; - const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( changeMoneyRequestStatus(), - }); - } - if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning && !isInvoiceReport) { - threeDotsMenuItems.push({ - icon: Expensicons.Stopwatch, - text: translate('iou.hold'), - onSelected: () => changeMoneyRequestStatus(), - }); - } - } - useEffect(() => { if (isLoadingHoldUseExplained) { return; @@ -290,23 +241,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea IOU.dismissHoldUseExplanation(); }; - if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) { - threeDotsMenuItems.push({ - icon: Expensicons.Trashcan, - text: translate('iou.cancelPayment'), - onSelected: () => setIsConfirmModalVisible(true), - }); - } - - // If the report supports adding transactions to it, then it also supports deleting transactions from it. - if (canDeleteRequest && !isEmptyObject(transactionThreadReport)) { - threeDotsMenuItems.push({ - icon: Expensicons.Trashcan, - text: translate('reportActionContextMenu.deleteAction', {action: requestParentReportAction}), - onSelected: () => setIsDeleteRequestModalVisible(true), - }); - } - useEffect(() => { if (canDeleteRequest) { return; @@ -327,9 +261,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea onBackButtonPress={onBackButtonPress} // Shows border if no buttons or banners are showing below the header shouldShowBorderBottom={!isMoreContentShown} - shouldShowThreeDotsButton - threeDotsMenuItems={threeDotsMenuItems} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} > {shouldShowSettlementButton && !shouldUseNarrowLayout && ( @@ -352,6 +283,15 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea /> )} + {shouldShowExportIntegrationButton && !shouldUseNarrowLayout && ( + + + + )} {shouldShowSubmitButton && !shouldUseNarrowLayout && (
      + ); } diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 8c44a316ce94..9a797d98e025 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -1,6 +1,133 @@ -import {addMonths, format, startOfMonth} from 'date-fns'; +import {addMonths, format, fromUnixTime, startOfMonth} from 'date-fns'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import DateUtils from '@libs/DateUtils'; +import type {Phrase, PhraseParameters} from '@libs/Localize'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type {AccountData} from '@src/types/onyx/Fund'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type BillingStatusResult = { + title: string; + subtitle: string; + isError: boolean; + isRetryAvailable?: boolean; + isAddButtonDark?: boolean; + isAuthenticationRequired?: boolean; + icon?: IconAsset; + rightIcon?: IconAsset; +}; + +function getBillingStatus( + translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, + accountData?: AccountData, +): BillingStatusResult | undefined { + const cardEnding = accountData?.cardNumber ?? ''; + + const amountOwed = SubscriptionUtils.getAmountOwed(); + + const subscriptionStatus = SubscriptionUtils.getSubscriptionStatus(); + + const endDate = SubscriptionUtils.getOverdueGracePeriodDate(); + + const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; + + const isCurrentCardExpired = DateUtils.isCardExpired(accountData?.cardMonth ?? 0, accountData?.cardYear ?? 0); + + switch (subscriptionStatus?.status) { + case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: + return { + title: translate('subscription.billingBanner.policyOwnerAmountOwed.title'), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwed.subtitle', {date: endDateFormatted}), + isError: true, + isRetryAvailable: true, + }; + + case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: + return { + title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle'), + isError: true, + }; + + case SubscriptionUtils.PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING: + return { + title: translate('subscription.billingBanner.policyOwnerUnderInvoicing.title'), + subtitle: translate('subscription.billingBanner.policyOwnerUnderInvoicing.subtitle', {date: endDateFormatted}), + isError: true, + isAddButtonDark: true, + }; + + case SubscriptionUtils.PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE: + return { + title: translate('subscription.billingBanner.policyOwnerUnderInvoicingOverdue.title'), + subtitle: translate('subscription.billingBanner.policyOwnerUnderInvoicingOverdue.subtitle'), + isError: true, + isAddButtonDark: true, + }; + + case SubscriptionUtils.PAYMENT_STATUS.BILLING_DISPUTE_PENDING: + return { + title: translate('subscription.billingBanner.billingDisputePending.title'), + subtitle: translate('subscription.billingBanner.billingDisputePending.subtitle', {amountOwed, cardEnding}), + isError: true, + isRetryAvailable: false, + }; + + case SubscriptionUtils.PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED: + return { + title: translate('subscription.billingBanner.cardAuthenticationRequired.title'), + subtitle: translate('subscription.billingBanner.cardAuthenticationRequired.subtitle', {cardEnding}), + isError: true, + isAuthenticationRequired: true, + }; + + case SubscriptionUtils.PAYMENT_STATUS.INSUFFICIENT_FUNDS: + return { + title: translate('subscription.billingBanner.insufficientFunds.title'), + subtitle: translate('subscription.billingBanner.insufficientFunds.subtitle', {amountOwed}), + isError: true, + isRetryAvailable: true, + }; + + case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRED: + return { + title: translate('subscription.billingBanner.cardExpired.title'), + subtitle: translate('subscription.billingBanner.cardExpired.subtitle', {amountOwed}), + isError: true, + isRetryAvailable: !isCurrentCardExpired, + }; + + case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRE_SOON: + return { + title: translate('subscription.billingBanner.cardExpireSoon.title'), + subtitle: translate('subscription.billingBanner.cardExpireSoon.subtitle'), + isError: false, + icon: Illustrations.CreditCardEyes, + }; + + case SubscriptionUtils.PAYMENT_STATUS.RETRY_BILLING_SUCCESS: + return { + title: translate('subscription.billingBanner.retryBillingSuccess.title'), + subtitle: translate('subscription.billingBanner.retryBillingSuccess.subtitle'), + isError: false, + rightIcon: Expensicons.Close, + }; + + case SubscriptionUtils.PAYMENT_STATUS.RETRY_BILLING_ERROR: + return { + title: translate('subscription.billingBanner.retryBillingError.title'), + subtitle: translate('subscription.billingBanner.retryBillingError.subtitle'), + isError: true, + isRetryAvailable: false, + }; + + default: + return undefined; + } +} /** * Get the next billing date. @@ -20,6 +147,8 @@ function shouldShowPreTrialBillingBanner(): boolean { } export default { + getBillingStatus, shouldShowPreTrialBillingBanner, getNextBillingDate, }; +export type {BillingStatusResult}; diff --git a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx index 357ce7eb4cc5..96aad062f9f9 100644 --- a/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx @@ -19,9 +19,9 @@ function ChangeBillingCurrency() { const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard), [fundList]); - const changeBillingCurrency = useCallback((currency?: ValueOf, values?: FormOnyxValues) => { + const changeBillingCurrency = useCallback((currency?: ValueOf, values?: FormOnyxValues) => { if (values?.securityCode) { - PaymentMethods.updateBillingCurrency(currency ?? CONST.CURRENCY.USD, values.securityCode); + PaymentMethods.updateBillingCurrency(currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, values.securityCode); } Navigation.goBack(); }, []); diff --git a/src/pages/settings/Subscription/PaymentCard/index.tsx b/src/pages/settings/Subscription/PaymentCard/index.tsx index 0e92194d8d8f..66e1012d954a 100644 --- a/src/pages/settings/Subscription/PaymentCard/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import PaymentCardForm from '@components/AddPaymentCard/PaymentCardForm'; @@ -11,6 +11,7 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePreferredCurrency from '@hooks/usePreferredCurrency'; +import usePrevious from '@hooks/usePrevious'; import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; import useSubscriptionPrice from '@hooks/useSubscriptionPrice'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,14 +21,11 @@ import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; function AddPaymentCard() { const styles = useThemeStyles(); const {translate} = useLocalize(); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); - const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); - const [shouldShowPaymentCardForm, setShouldShowPaymentCardForm] = useState(false); const subscriptionPlan = useSubscriptionPlan(); const subscriptionPrice = useSubscriptionPrice(); @@ -35,7 +33,6 @@ function AddPaymentCard() { const isCollect = subscriptionPlan === CONST.POLICY.TYPE.TEAM; const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL; - const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard), [fundList]); useEffect(() => { PaymentMethods.clearPaymentCardFormErrorAndSubmit(); @@ -45,34 +42,36 @@ function AddPaymentCard() { }; }, []); - useEffect(() => { - if (!defaultCard?.accountData || isEmptyObject(defaultCard?.accountData)) { - setShouldShowPaymentCardForm(true); - return; - } - PaymentMethods.setPaymentCardForm(defaultCard.accountData); - setShouldShowPaymentCardForm(true); - }, [defaultCard?.accountData]); - const addPaymentCard = useCallback((values: FormOnyxValues) => { const cardData = { - cardNumber: values.cardNumber, + cardNumber: CardUtils.getMCardNumberString(values.cardNumber), cardMonth: CardUtils.getMonthFromExpirationDateString(values.expirationDate), cardYear: CardUtils.getYearFromExpirationDateString(values.expirationDate), cardCVV: values.securityCode, addressName: values.nameOnCard, addressZip: values.addressZipCode, - currency: values.currency ?? CONST.CURRENCY.USD, + currency: values.currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, }; PaymentMethods.addSubscriptionPaymentCard(cardData); }, []); + const [formData] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); + const prevFormDataSetupComplete = usePrevious(!!formData?.setupComplete); + + useEffect(() => { + if (prevFormDataSetupComplete || !formData?.setupComplete) { + return; + } + + PaymentMethods.continueSetup(); + }, [prevFormDataSetupComplete, formData?.setupComplete]); + return ( {translate('subscription.subscriptionSettings.autoIncrease')} {translate('subscription.subscriptionSettings.saveUpTo', { - amountSaved: isCollect ? CONST.SUBSCRIPTION_POSSIBLE_COST_SAVINGS.COLLECT_PLAN : CONST.SUBSCRIPTION_POSSIBLE_COST_SAVINGS.CONTROL_PLAN, + amountWithCurrency: convertToShortDisplayString(possibleCostSavings, preferredCurrency), })} diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index f697590327a8..a3b914192284 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import Onyx, {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -13,7 +13,9 @@ import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import Switch from '@components/Switch'; import TestToolMenu from '@components/TestToolMenu'; +import TestToolRow from '@components/TestToolRow'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useEnvironment from '@hooks/useEnvironment'; @@ -21,6 +23,8 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {setShouldMaskOnyxState} from '@libs/actions/MaskOnyx'; +import ExportOnyxState from '@libs/ExportOnyxState'; import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; @@ -37,11 +41,12 @@ type BaseMenuItem = { type TroubleshootPageOnyxProps = { shouldStoreLogs: OnyxEntry; + shouldMaskOnyxState: boolean; }; type TroubleshootPageProps = TroubleshootPageOnyxProps; -function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { +function TroubleshootPage({shouldStoreLogs, shouldMaskOnyxState}: TroubleshootPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isProduction} = useEnvironment(); @@ -50,6 +55,17 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); + const exportOnyxState = useCallback(() => { + ExportOnyxState.readFromOnyxDatabase().then((value: Record) => { + let dataToShare = value; + if (shouldMaskOnyxState) { + dataToShare = ExportOnyxState.maskFragileData(value); + } + + ExportOnyxState.shareAsFile(JSON.stringify(dataToShare)); + }); + }, [shouldMaskOnyxState]); + const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { translationKey: 'initialSettingsPage.troubleshoot.viewConsole', @@ -63,6 +79,11 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { icon: Expensicons.RotateLeft, action: () => setIsConfirmationModalVisible(true), }, + { + translationKey: 'initialSettingsPage.troubleshoot.exportOnyxState', + icon: Expensicons.Download, + action: exportOnyxState, + }, ]; if (shouldStoreLogs) { @@ -78,7 +99,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { wrapperStyle: [styles.sectionMenuItemTopDescription], })) .reverse(); - }, [shouldStoreLogs, translate, waitForNavigate, styles.sectionMenuItemTopDescription]); + }, [waitForNavigate, exportOnyxState, shouldStoreLogs, translate, styles.sectionMenuItemTopDescription]); return ( + + + ({ shouldStoreLogs: { key: ONYXKEYS.SHOULD_STORE_LOGS, }, + shouldMaskOnyxState: { + key: ONYXKEYS.SHOULD_MASK_ONYX_STATE, + selector: (shouldMaskOnyxState) => shouldMaskOnyxState ?? true, + }, })(TroubleshootPage); diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 02bb5dd99687..81611d3defb0 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -84,14 +84,14 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const shouldDisplayCardDomain = !cardList?.[cardID]?.nameValuePairs?.issuedBy; + const shouldDisplayCardDomain = !cardList?.[cardID]?.nameValuePairs?.issuedBy || !cardList?.[cardID]?.nameValuePairs?.isVirtual; const domain = cardList?.[cardID]?.domainName ?? ''; const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardID]?.nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { if (shouldDisplayCardDomain) { - return CardUtils.getDomainCards(cardList)[domain]?.filter((card) => !card?.nameValuePairs?.issuedBy) ?? []; + return CardUtils.getDomainCards(cardList)[domain]?.filter((card) => !card?.nameValuePairs?.issuedBy || !card?.nameValuePairs?.isVirtual) ?? []; } return [cardList?.[cardID]]; }, [shouldDisplayCardDomain, cardList, cardID, domain]); @@ -122,7 +122,7 @@ function ExpensifyCardPage({ [revealedCardID]: '', })); }) - .catch((error) => { + .catch((error: string) => { setCardsDetailsErrors((prevState) => ({ ...prevState, [revealedCardID]: error, diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index f684b76a152e..a1fc79110235 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -226,7 +226,7 @@ function PaymentMethodList({ return; } - const isAdminIssuedVirtualCard = !!card?.nameValuePairs?.issuedBy; + const isAdminIssuedVirtualCard = !!card?.nameValuePairs?.issuedBy && !!card?.nameValuePairs?.isVirtual; // The card should be grouped to a specific domain and such domain already exists in a assignedCardsGrouped if (assignedCardsGrouped.some((item) => item.isGroupedCardDomain && item.description === card.domainName) && !isAdminIssuedVirtualCard) { @@ -242,7 +242,7 @@ function PaymentMethodList({ assignedCardsGrouped.push({ key: card.cardID.toString(), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - title: isAdminIssuedVirtualCard ? card?.nameValuePairs?.cardTitle || card.bank : card.bank, + title: card?.nameValuePairs?.cardTitle || card.bank, description: card.domainName, onPress: () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(String(card.cardID))), isGroupedCardDomain: !isAdminIssuedVirtualCard, diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx index b3ebbde3ae71..a324c91ecb85 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.tsx +++ b/src/pages/settings/Wallet/TransferBalancePage.tsx @@ -114,7 +114,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans } PaymentMethods.saveWalletTransferAccountTypeAndID(selectedAccount?.accountType ?? '', selectedAccount?.methodID?.toString() ?? '-1'); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want this effect to run on initial render + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we only want this effect to run on initial render }, []); if (walletTransfer?.shouldShowSuccess && !walletTransfer?.loading) { diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 738dced9e1b7..81b021550a76 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -273,10 +273,6 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi Navigation.navigate(source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET ? ROUTES.SETTINGS_WALLET : ROUTES.SETTINGS_WALLET_TRANSFER_BALANCE); }; - useEffect(() => { - PaymentMethods.openWalletPage(); - }, []); - useEffect(() => { // If the user was previously offline, skip debouncing showing the loader if (!network.isOffline) { diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index 1c8084eb12e2..f7c96004d7db 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -173,7 +173,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false input.current.focus(); } return () => clearTimeout(focusTimeout); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); useEffect(() => { @@ -267,7 +267,6 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false onSubmitEditing={validateAndSubmitForm} autoCapitalize="none" autoCorrect={false} - autoFocus inputMode={CONST.INPUT_MODE.EMAIL} errorText={formError} hasError={shouldShowServerError} diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index 0b4310cce337..024d5a7c5610 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -266,6 +266,7 @@ function SignInPage({credentials, account, activeClients = [], preferredLocale, diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx index b61136993488..3aab8a003bc9 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx @@ -19,6 +19,7 @@ function BackgroundImage({width, transitionDuration, isSmallScreen = false}: Bac const animatedStyle = useAnimatedStyle(() => ({opacity: opacity.value})); // This sets the opacity animation for the background image once it has loaded. function setOpacityAnimation() { + // eslint-disable-next-line react-compiler/react-compiler opacity.value = withTiming(1, { duration: CONST.MICROSECONDS_PER_MS, easing: Easing.ease, diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index 523c89874f9c..9a3584de7779 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; +import SafariFormWrapper from '@components/Form/SafariFormWrapper'; import FormHelpMessage from '@components/FormHelpMessage'; import type {MagicCodeInputHandle} from '@components/MagicCodeInput'; import MagicCodeInput from '@components/MagicCodeInput'; @@ -217,7 +218,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs clearLocalSignInData(); // `clearLocalSignInData` is not required as a dependency, and adding it // overcomplicates things requiring clearLocalSignInData function to use useCallback - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isLoadingResendValidationForm]); useEffect(() => { @@ -291,7 +292,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs }, [account, credentials, twoFactorAuthCode, validateCode, isUsingRecoveryCode, recoveryCode]); return ( - <> + {/* At this point, if we know the account requires 2FA we already successfully authenticated */} {account?.requiresTwoFactorAuth ? ( @@ -402,7 +403,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs - + ); } diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx index e9b632a0cee9..f5aaf9ea8ffd 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.tsx +++ b/src/pages/tasks/NewTaskDescriptionPage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import {ExpensiMark} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -16,7 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {NewTaskNavigatorParamList} from '@libs/Navigation/types'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; @@ -35,8 +34,6 @@ type NewTaskDescriptionPageOnyxProps = { type NewTaskDescriptionPageProps = NewTaskDescriptionPageOnyxProps & StackScreenProps; -const parser = new ExpensiMark(); - function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -80,7 +77,7 @@ function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) { ; -const parser = new ExpensiMark(); - function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -50,7 +47,7 @@ function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { useEffect(() => { setTaskTitle(task?.title ?? ''); - setTaskDescription(parseHtmlToMarkdown(parser.replace(task?.description ?? ''))); + setTaskDescription(Parser.htmlToMarkdown(Parser.replace(task?.description ?? ''))); }, [task]); const validate = (values: FormOnyxValues): FormInputErrors => { @@ -136,7 +133,7 @@ function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) { autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - defaultValue={parseHtmlToMarkdown(parser.replace(taskDescription))} + defaultValue={Parser.htmlToMarkdown(Parser.replace(taskDescription))} value={taskDescription} onValueChange={setTaskDescription} isMarkdownEnabled diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx index 66bd7d9e9a82..92f5b2394308 100644 --- a/src/pages/tasks/TaskDescriptionPage.tsx +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -15,7 +15,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; @@ -48,7 +48,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti const submit = useCallback( (values: FormOnyxValues) => { - if (values.description !== parseHtmlToMarkdown(report?.description ?? '') && !isEmptyObject(report)) { + if (values.description !== Parser.htmlToMarkdown(report?.description ?? '') && !isEmptyObject(report)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server Task.editTask(report, {description: values.description}); @@ -111,7 +111,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti name={INPUT_IDS.DESCRIPTION} label={translate('newTaskPage.descriptionOptional')} accessibilityLabel={translate('newTaskPage.descriptionOptional')} - defaultValue={parseHtmlToMarkdown(report?.description ?? '')} + defaultValue={Parser.htmlToMarkdown(report?.description ?? '')} ref={(element: AnimatedTextInputRef) => { if (!element) { return; diff --git a/src/pages/wallet/WalletStatementPage.tsx b/src/pages/wallet/WalletStatementPage.tsx index 40f69d809618..0db72877bd2e 100644 --- a/src/pages/wallet/WalletStatementPage.tsx +++ b/src/pages/wallet/WalletStatementPage.tsx @@ -42,7 +42,7 @@ function WalletStatementPage({walletStatement, route}: WalletStatementPageProps) if (!yearMonth || yearMonth.length !== 6 || yearMonth > currentYearMonth) { Navigation.dismissModal(); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- we want this effect to run only on mount + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we want this effect to run only on mount }, []); useEffect(() => { diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 02e5ab13fd86..09a4c3b49b3f 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -1,11 +1,12 @@ /* eslint-disable rulesdir/no-negated-variables */ -import React, {useEffect} from 'react'; +import React, {useEffect, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useNetwork from '@hooks/useNetwork'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -23,6 +24,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; const ACCESS_VARIANTS = { [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy), + [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => PolicyUtils.isControlPolicy(policy), [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login), [CONST.IOU.ACCESS_VARIANTS.CREATE]: ( policy: OnyxEntry, @@ -108,6 +110,7 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps const isPolicyIDInRoute = !!policyID?.length; const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType); const isFromGlobalCreate = isEmptyObject(report?.reportID); + const pendingField = featureName ? props.policy?.pendingFields?.[featureName] : undefined; useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(policy)) { @@ -116,20 +119,33 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps } Policy.openWorkspace(policyID, []); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isPolicyIDInRoute, policyID]); const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true; + const [isPolicyFeatureEnabled, setIsPolicyFeatureEnabled] = useState(isFeatureEnabled); + const {isOffline} = useNetwork(); + const isPageAccessible = accessVariants.reduce((acc, variant) => { const accessFunction = ACCESS_VARIANTS[variant]; return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType); }, true); const isPolicyNotAccessible = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id; - const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked; + const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isPolicyFeatureEnabled || shouldBeBlocked; + + // We only update the feature state if it isn't pending. + // This is because the feature state changes several times during the creation of a workspace, while we are waiting for a response from the backend. + // Without this, we can have unexpectedly have 'Not Found' be shown. + useEffect(() => { + if (pendingField && !isOffline && !isFeatureEnabled) { + return; + } + setIsPolicyFeatureEnabled(isFeatureEnabled); + }, [pendingField, isOffline, isFeatureEnabled]); if (shouldShowFullScreenLoadingIndicator) { return ; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 6715bf21c911..267a3ac38850 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -62,6 +62,7 @@ type WorkspaceMenuItem = { | typeof SCREENS.WORKSPACE.MORE_FEATURES | typeof SCREENS.WORKSPACE.PROFILE | typeof SCREENS.WORKSPACE.MEMBERS + | typeof SCREENS.WORKSPACE.EXPENSIFY_CARD | typeof SCREENS.WORKSPACE.REPORT_FIELDS; }; @@ -106,11 +107,27 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc [CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]: policy?.areTagsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]: policy?.tax?.trackingEnabled, [CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: policy?.areConnectionsEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled, }), [policy], ) as PolicyFeatureStates; + const fetchPolicyData = useCallback(() => { + if (policyDraft?.id) { + return; + } + Policy.openPolicyInitialPage(route.params.policyID); + }, [policyDraft?.id, route.params.policyID]); + + useNetwork({onReconnect: fetchPolicyData}); + + useFocusEffect( + useCallback(() => { + fetchPolicyData(); + }, [fetchPolicyData]), + ); + const policyID = policy?.id ?? '-1'; const policyName = policy?.name ?? ''; @@ -123,7 +140,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc App.savePolicyDraftByNewWorkspace(policyDraft.id, policyDraft.name, '', policyDraft.makeMeAdmin); // We only care when the component renders the first time - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -133,18 +150,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc setIsCurrencyModalOpen(false); }, [policy?.outputCurrency, isCurrencyModalOpen]); - const fetchPolicyData = useCallback(() => { - Policy.openPolicyInitialPage(route.params.policyID); - }, [route.params.policyID]); - - useNetwork({onReconnect: fetchPolicyData}); - - useFocusEffect( - useCallback(() => { - fetchPolicyData(); - }, [fetchPolicyData]), - ); - /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { Policy.updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD); @@ -206,7 +211,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = []; // We only update feature states if they aren't pending. - // These changes are made to synchronously change feature states along with FeatureEnabledAccessOrNotFoundComponent. + // These changes are made to synchronously change feature states along with AccessOrNotFoundWrapperComponent. useEffect(() => { setFeatureStates((currentFeatureStates) => { const newFeatureStates = {} as PolicyFeatureStates; @@ -231,6 +236,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]) { + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.expensifyCard', + icon: Expensicons.CreditCard, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.EXPENSIFY_CARD, + }); + } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.workflows', @@ -270,6 +284,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]) { + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.reportFields', + icon: Expensicons.Pencil, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.REPORT_FIELDS, + }); + } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.accounting', @@ -281,15 +304,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } - if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]) { - protectedCollectPolicyMenuItems.push({ - translationKey: 'workspace.common.reportFields', - icon: Expensicons.Pencil, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.REPORT_FIELDS, - }); - } - protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.moreFeatures', icon: Expensicons.Gear, diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 9bab92bc38b0..877a77f62d3f 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import {ExpensiMark} from 'expensify-common'; import lodashDebounce from 'lodash/debounce'; import React, {useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; @@ -22,8 +21,8 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Parser from '@libs/Parser'; import * as PolicyUtils from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -59,8 +58,6 @@ type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInviteMessagePageOnyxProps & StackScreenProps; -const parser = new ExpensiMark(); - function WorkspaceInviteMessagePage({ workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, @@ -87,20 +84,18 @@ function WorkspaceInviteMessagePage({ workspaceInviteMessageDraft || // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - policy?.description || - parser.replace( - translate('workspace.common.welcomeNote', { - workspaceName: policy?.name ?? '', - }), - ); + Parser.htmlToMarkdown(policy?.description ?? '') || + translate('workspace.common.welcomeNote', { + workspaceName: policy?.name ?? '', + }); useEffect(() => { if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { - setWelcomeNote(parseHtmlToMarkdown(getDefaultWelcomeNote())); + setWelcomeNote(getDefaultWelcomeNote()); return; } Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); const debouncedSaveDraft = lodashDebounce((newDraft: string | null) => { @@ -113,9 +108,7 @@ function WorkspaceInviteMessagePage({ Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID); debouncedSaveDraft(null); SearchInputManager.searchInput = ''; - // Pop the invite message page before navigating to the members page. - Navigation.goBack(); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); + Navigation.dismissModal(); }; /** Opens privacy url as an external link */ diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index a2e81a6da550..c29819b3c6f9 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -81,13 +81,13 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli return () => { Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [route.params.policyID]); useEffect(() => { Policy.clearErrors(route.params.policyID); openWorkspaceInvitePage(); - // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- policyID changes remount the component }, []); useNetwork({onReconnect: openWorkspaceInvitePage}); @@ -171,7 +171,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli setPersonalDetails(Object.values(newPersonalDetailsDict)); setSelectedOptions(Object.values(newSelectedOptionsDict)); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [options.personalDetails, policy?.employeeList, betas, debouncedSearchTerm, excludedUsers, areOptionsInitialized, inviteOptions.personalDetails, inviteOptions.userToInvite]); const sections: MembersSection[] = useMemo(() => { @@ -299,7 +299,6 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli message={policy?.alertMessage ?? ''} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]} enabledWhenOffline - disablePressOnEnter /> ), [inviteUser, policy?.alertMessage, selectedOptions.length, shouldShowAlertPrompt, styles, translate], diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 8f1bb6ee12ba..adbf5a664c82 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -43,7 +43,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx'; +import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployeeList, Session} from '@src/types/onyx'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -65,9 +65,8 @@ type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps & * Inverts an object, equivalent of _.invert */ function invertObject(object: Record): Record { - const invertedEntries = Object.entries(object).map(([key, value]) => [value, key]); - const inverted: Record = Object.fromEntries(invertedEntries); - return inverted; + const invertedEntries = Object.entries(object).map(([key, value]) => [value, key] as const); + return Object.fromEntries(invertedEntries); } type MemberOption = Omit & {accountID: number}; @@ -137,7 +136,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, newErrors[member] = translate('workspace.people.error.cannotRemove'); }); setErrors(newErrors); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [selectedEmployees, policy?.owner, session?.accountID]); // useFocus would make getWorkspaceMembers get called twice on fresh login because policyEmployee is a dependency of getWorkspaceMembers. @@ -147,7 +146,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, return; } getWorkspaceMembers(); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isFocused]); useEffect(() => { @@ -175,7 +174,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, // This is an equivalent of the lodash intersection function. The reduce method below is used to filter the items that exist in both arrays. return [prevSelectedElements, currentSelectedElements].reduce((prev, members) => prev.filter((item) => members.includes(item))); }); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [policy?.employeeList, policyMemberEmailsToAccountIDs]); useEffect(() => { @@ -305,14 +304,6 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, [route.params.policyID], ); - /** - * Check if the policy member is deleted from the workspace - */ - const isDeletedPolicyEmployee = useCallback( - (policyEmployee: PolicyEmployee): boolean => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors), - [isOffline], - ); - const policyOwner = policy?.owner; const currentUserLogin = currentUserPersonalDetails.login; const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {}); @@ -321,7 +312,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - if (isDeletedPolicyEmployee(policyEmployee)) { + if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { return; } @@ -355,7 +346,10 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, accountID, isSelected, isDisabledCheckbox: !(isPolicyAdmin && accountID !== policy?.ownerAccountID && accountID !== session?.accountID), - isDisabled: isPolicyAdmin && (policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors)), + isDisabled: + !!details.isOptimisticPersonalDetail || + (isPolicyAdmin && (policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyEmployee.errors))), + cursorStyle: details.isOptimisticPersonalDetail ? styles.cursorDefault : {}, text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: roleBadge, @@ -373,13 +367,13 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase())); + result = OptionsListUtils.sortItemsAlphabetically(result); return result; }, [ + isOffline, currentUserLogin, formatPhoneNumber, invitedPrimaryToSecondaryLogins, - isDeletedPolicyEmployee, isPolicyAdmin, personalDetails, policy?.owner, @@ -390,6 +384,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, selectedEmployees, session?.accountID, translate, + styles.cursorDefault, ]); const data = useMemo(() => getUsers(), [getUsers]); diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 1499f3a0c0b3..46d897bd0a0f 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -2,6 +2,7 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -16,12 +17,15 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import {isControlPolicy} from '@libs/PolicyUtils'; import * as Category from '@userActions/Policy/Category'; import * as DistanceRate from '@userActions/Policy/DistanceRate'; import * as Policy from '@userActions/Policy/Policy'; import * as Tag from '@userActions/Policy/Tag'; +import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -41,6 +45,7 @@ type Item = { isActive: boolean; disabled?: boolean; action: (isEnabled: boolean) => void; + disabledAction?: () => void; pendingAction: PendingAction | undefined; errors?: Errors; onCloseError?: () => void; @@ -52,18 +57,50 @@ type SectionObject = { items: Item[]; }; +// TODO: remove when Onyx data is available +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const mockedCardsList = { + test1: { + cardholder: {accountID: 1, lastName: 'Smith', firstName: 'Bob', displayName: 'Bob Smith', avatar: ''}, + name: 'Test 1', + limit: 1000, + lastFourPAN: '1234', + }, + test2: { + cardholder: {accountID: 2, lastName: 'Miller', firstName: 'Alex', displayName: 'Alex Miller', avatar: ''}, + name: 'Test 2', + limit: 2000, + lastFourPAN: '1234', + }, + test3: { + cardholder: {accountID: 3, lastName: 'Brown', firstName: 'Kevin', displayName: 'Kevin Brown', avatar: ''}, + name: 'Test 3', + limit: 3000, + lastFourPAN: '1234', + }, +}; + function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPageProps) { const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); - const {canUseReportFieldsFeature} = usePermissions(); + const {canUseReportFieldsFeature, canUseWorkspaceFeeds} = usePermissions(); const hasAccountingConnection = !!policy?.areConnectionsEnabled && !isEmptyObject(policy?.connections); - const isSyncTaxEnabled = !!policy?.connections?.quickbooksOnline?.config?.syncTax || !!policy?.connections?.xero?.config?.importTaxRates; + const isSyncTaxEnabled = + !!policy?.connections?.quickbooksOnline?.config?.syncTax || + !!policy?.connections?.xero?.config?.importTaxRates || + !!policy?.connections?.netsuite?.options?.config?.syncOptions?.syncTax; const policyID = policy?.id ?? ''; + // @ts-expect-error a new props will be added during feed api implementation + const workspaceAccountID = (policy?.workspaceAccountID as string) ?? ''; + const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); + // Uncomment this line for testing disabled toggle feature - for c+ + // const [cardsList = mockedCardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); const [isIntegrateWarningModalOpen, setIsIntegrateWarningModalOpen] = useState(false); const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); + const [isDisableExpensifyCardWarningModalOpen, setIsDisableExpensifyCardWarningModalOpen] = useState(false); const spendItems: Item[] = [ { @@ -88,6 +125,24 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }, ]; + // TODO remove this when feature will be fully done, and move spend item inside spendItems array + if (canUseWorkspaceFeeds) { + spendItems.splice(1, 0, { + icon: Illustrations.HandCard, + titleTranslationKey: 'workspace.moreFeatures.expensifyCard.title', + subtitleTranslationKey: 'workspace.moreFeatures.expensifyCard.subtitle', + isActive: policy?.areExpensifyCardsEnabled ?? false, + pendingAction: policy?.pendingFields?.areExpensifyCardsEnabled, + disabled: !isEmptyObject(cardsList), + action: (isEnabled: boolean) => { + Policy.enableExpensifyCard(policy?.id ?? '-1', isEnabled); + }, + disabledAction: () => { + setIsDisableExpensifyCardWarningModalOpen(true); + }, + }); + } + const organizeItems: Item[] = [ { icon: Illustrations.FolderOpen, @@ -150,6 +205,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro return; } if (isEnabled) { + if (!isControlPolicy(policy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.reportFields.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)), + ); + return; + } + Policy.enablePolicyReportFields(policyID, true); return; } @@ -204,6 +266,8 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro > + { + setIsDisableExpensifyCardWarningModalOpen(false); + Report.navigateToConciergeChat(true); + }} + onCancel={() => setIsDisableExpensifyCardWarningModalOpen(false)} + prompt={translate('workspace.moreFeatures.expensifyCard.disableCardPrompt')} + confirmText={translate('workspace.moreFeatures.expensifyCard.disableCardButton')} + cancelText={translate('common.cancel')} + /> ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 2fa291831de1..3f53f9e1cf6d 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -148,7 +148,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return; } Navigation.dismissModal(newRoomReportID); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we just want this to update on changing the form State }, [isLoading, errorFields]); useEffect(() => { diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 65b97b7c0387..d8f917eb4f20 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -152,7 +152,7 @@ function WorkspacePageWithSections({ (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)) ); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [policy, shouldShowNonAdmin]); return ( diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceProfileAddressPage.tsx index c7cf00efb798..47793f7fb810 100644 --- a/src/pages/workspace/WorkspaceProfileAddressPage.tsx +++ b/src/pages/workspace/WorkspaceProfileAddressPage.tsx @@ -1,21 +1,14 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import AddressForm from '@components/AddressForm'; +import React, {useMemo} from 'react'; import type {FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import AddressPage from '@pages/AddressPage'; import {updateAddress} from '@userActions/Policy/Policy'; -import type {Country} from '@src/CONST'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import type ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {CompanyAddress} from '@src/types/onyx/Policy'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import type {WithPolicyProps} from './withPolicy'; import withPolicy from './withPolicy'; @@ -23,18 +16,21 @@ type WorkspaceProfileAddressPagePolicyProps = WithPolicyProps; type WorkspaceProfileAddressPageProps = StackScreenProps & WorkspaceProfileAddressPagePolicyProps; -function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPageProps) { - const styles = useThemeStyles(); +function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) { const {translate} = useLocalize(); - const address = useMemo(() => policy?.address, [policy]); - const [currentCountry, setCurrentCountry] = useState(address?.country); - const [[street1, street2], setStreets] = useState((address?.addressStreet ?? '').split('\n')); - const [state, setState] = useState(address?.state); - const [city, setCity] = useState(address?.city); - const [zipcode, setZipcode] = useState(address?.zipCode); - - const countryFromUrlTemp = route?.params?.country; - const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; + const address: Address = useMemo(() => { + const tempAddress = policy?.address; + const [street1, street2] = (tempAddress?.addressStreet ?? '').split('\n'); + const result = { + street: street1?.trim() ?? '', + street2: street2?.trim() ?? '', + city: tempAddress?.city?.trim() ?? '', + state: tempAddress?.state?.trim() ?? '', + zip: tempAddress?.zipCode?.trim().toUpperCase() ?? '', + country: tempAddress?.country ?? '', + }; + return result; + }, [policy]); const updatePolicyAddress = (values: FormOnyxValues) => { if (!policy) { @@ -50,78 +46,13 @@ function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPag Navigation.goBack(); }; - const handleAddressChange = useCallback((value: unknown, key: unknown) => { - const countryValue = value as Country | ''; - const addressKey = key as keyof CompanyAddress; - - if (addressKey !== 'country' && addressKey !== 'state' && addressKey !== 'city' && addressKey !== 'zipCode') { - return; - } - if (addressKey === 'country') { - setCurrentCountry(countryValue); - setState(''); - setCity(''); - setZipcode(''); - return; - } - if (addressKey === 'state') { - setState(countryValue); - setCity(''); - setZipcode(''); - return; - } - if (addressKey === 'city') { - setCity(countryValue); - setZipcode(''); - return; - } - setZipcode(countryValue); - }, []); - - useEffect(() => { - if (!address) { - return; - } - setStreets((address?.addressStreet ?? '').split('\n')); - setState(address.state); - setCurrentCountry(address.country); - setCity(address.city); - setZipcode(address.zipCode); - }, [address]); - - useEffect(() => { - if (!countryFromUrl) { - return; - } - handleAddressChange(countryFromUrl, 'country'); - }, [countryFromUrl, handleAddressChange]); - return ( - - Navigation.goBack()} - /> - - {translate('workspace.editor.addressContext')} - - - + ); } diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index 54dcfd62ac44..527fdf28f8d1 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import {Keyboard, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; @@ -12,7 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; +import Parser from '@libs/Parser'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; import * as Policy from '@userActions/Policy/Policy'; @@ -24,17 +23,15 @@ import type {WithPolicyProps} from './withPolicy'; type Props = WithPolicyProps; -const parser = new ExpensiMark(); - function WorkspaceProfileDescriptionPage({policy}: Props) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [description, setDescription] = useState(() => - parseHtmlToMarkdown( + Parser.htmlToMarkdown( // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( + Parser.replace( translate('workspace.common.welcomeNote', { workspaceName: policy?.name ?? '', }), diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 6f2097688b5c..96911233bbb2 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,6 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; import {Image, StyleSheet, View} from 'react-native'; @@ -25,6 +24,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import Parser from '@libs/Parser'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -47,9 +47,7 @@ type WorkspaceProfilePageOnyxProps = { type WorkspaceProfilePageProps = WithPolicyProps & WorkspaceProfilePageOnyxProps & StackScreenProps; -const parser = new ExpensiMark(); - -function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfilePageProps) { +function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {}, route}: WorkspaceProfilePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -57,6 +55,8 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfi const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); const {canUseSpotnanaTravel} = usePermissions(); + // When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx. + const policy = policyDraft?.id ? policyDraft : policyProp; const outputCurrency = policy?.outputCurrency ?? ''; const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? ''; const formattedCurrency = !isEmptyObject(policy) && !isEmptyObject(currencyList) ? `${outputCurrency} - ${currencySymbol}` : ''; @@ -78,7 +78,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfi // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( + Parser.replace( translate('workspace.common.welcomeNote', { workspaceName: policy?.name ?? '', }), @@ -88,8 +88,11 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfi const shouldShowAddress = !readOnly || formattedAddress; const fetchPolicyData = useCallback(() => { + if (policyDraft?.id) { + return; + } Policy.openPolicyProfilePage(route.params.policyID); - }, [route.params.policyID]); + }, [policyDraft?.id, route.params.policyID]); useNetwork({onReconnect: fetchPolicyData}); @@ -135,11 +138,6 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkspaceProfi } }, [policy?.id, policyName, activeWorkspaceID, setActiveWorkspaceID]); - // When we create a new workspaces, the policy prop will not be set on the first render. Therefore, we have to delay rendering until it has been set in Onyx. - if (policy === undefined) { - return null; - } - return ( {(hasVBA?: boolean) => ( diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index a025033edda8..7e996c761035 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -1,7 +1,6 @@ -import React, {useRef} from 'react'; +import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {ImageSourcePropType} from 'react-native'; -import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; import ContextMenuItem from '@components/ContextMenuItem'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -10,12 +9,16 @@ import QRShare from '@components/QRShare'; import type {QRShareHandle} from '@components/QRShare/types'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Clipboard from '@libs/Clipboard'; import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; import * as Url from '@libs/Url'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -25,6 +28,7 @@ import type {WithPolicyProps} from './withPolicy'; function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const themeStyles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); const qrCodeRef = useRef(null); @@ -32,15 +36,32 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const session = useSession(); const policyName = policy?.name ?? ''; - const id = policy?.id ?? '-1'; + const policyID = policy?.id ?? '-1'; const adminEmail = session?.email ?? ''; const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL); - const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(id, adminEmail)}`; + const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(policyID, adminEmail)}`; + + const hasAvatar = !!policy?.avatarURL; + const logo = hasAvatar ? (policy?.avatarURL as ImageSourcePropType) : undefined; + + const defaultWorkspaceAvatar = ReportUtils.getDefaultWorkspaceAvatar(policyName) || Expensicons.FallbackAvatar; + const defaultWorkspaceAvatarColors = StyleUtils.getDefaultWorkspaceAvatarColor(policyID); + + const svgLogo = !hasAvatar ? defaultWorkspaceAvatar : undefined; + const logoBackgroundColor = !hasAvatar ? defaultWorkspaceAvatarColors.backgroundColor?.toString() : undefined; + const svgLogoFillColor = !hasAvatar ? defaultWorkspaceAvatarColors.fill : undefined; + + const adminRoom = useMemo(() => { + if (!policy?.id) { + return undefined; + } + return ReportUtils.getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policy?.id); + }, [policy?.id]); return ( - + + + {translate('workspace.common.shareNote.header')} + + + + {translate('workspace.common.shareNote.content.firstPart')}{' '} + { + if (!adminRoom?.reportID) { + return; + } + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminRoom.reportID)); + }} + > + {CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} + {' '} + {translate('workspace.common.shareNote.content.secondPart')} + + + - {/* + {/* Right now QR code download button is not shown anymore This is a temporary measure because right now it's broken because of the Fabric update. We need to wait for react-native v0.74 to be released so react-native-view-shot gets fixed. - + Please see https://github.com/Expensify/App/issues/40110 to see if it can be re-enabled. */} - + {isJoinRequestPending && ( - + )} {!isJoinRequestPending && ( - + diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 2813dedf5008..28f94fbfbe04 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -1,7 +1,7 @@ import {differenceInMinutes, isValid, parseISO} from 'date-fns'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import CollapsibleSection from '@components/CollapsibleSection'; import ConfirmModal from '@components/ConfirmModal'; @@ -30,7 +30,8 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {hasSynchronizationError, removePolicyConnection, syncConnection} from '@libs/actions/connections'; -import {findCurrentXeroOrganization, getCurrentXeroOrganizationName, getIntegrationLastSuccessfulDate, getXeroTenants} from '@libs/PolicyUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import {findCurrentXeroOrganization, getCurrentSageIntacctEntityName, getCurrentXeroOrganizationName, getIntegrationLastSuccessfulDate, getXeroTenants} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; @@ -63,6 +64,7 @@ type AccountingIntegration = { onImportPagePress: () => void; onExportPagePress: () => void; onAdvancedPagePress: () => void; + onCardReconciliationPagePress: () => void; }; function accountingIntegrationData( connectionName: PolicyConnectionName, @@ -85,6 +87,7 @@ function accountingIntegrationData( ), onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.getRoute(policyID)), onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT.getRoute(policyID)), + onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO)), onAdvancedPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_ADVANCED.getRoute(policyID)), }; case CONST.POLICY.CONNECTIONS.NAME.XERO: @@ -100,6 +103,7 @@ function accountingIntegrationData( ), onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.getRoute(policyID)), onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID)), + onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO)), onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.getRoute(policyID)), }; case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: @@ -113,9 +117,10 @@ function accountingIntegrationData( integrationToDisconnect={integrationToDisconnect} /> ), - onImportPagePress: () => {}, - onExportPagePress: () => {}, - onAdvancedPagePress: () => {}, + onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT.getRoute(policyID)), + onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)), + onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.NETSUITE)), + onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)), }; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return { @@ -128,21 +133,23 @@ function accountingIntegrationData( integrationToDisconnect={integrationToDisconnect} /> ), - onImportPagePress: () => {}, - onExportPagePress: () => {}, - onAdvancedPagePress: () => {}, + onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID)), + onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)), + onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT)), + onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID)), }; default: return undefined; } } -function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccountingPageProps) { +function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { + const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy.id}`); const theme = useTheme(); const styles = useThemeStyles(); const {translate, datetimeToRelative: getDatetimeToRelative} = useLocalize(); const {isOffline} = useNetwork(); - const {canUseNetSuiteIntegration} = usePermissions(); + const {canUseNetSuiteIntegration, canUseSageIntacctIntegration} = usePermissions(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0}); const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); @@ -152,24 +159,22 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting const lastSyncProgressDate = parseISO(connectionSyncProgress?.timestamp ?? ''); const isSyncInProgress = !!connectionSyncProgress?.stageInProgress && - connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE && + (connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE || !policy.connections?.[connectionSyncProgress.connectionName]) && isValid(lastSyncProgressDate) && differenceInMinutes(new Date(), lastSyncProgressDate) < CONST.POLICY.CONNECTIONS.SYNC_STAGE_TIMEOUT_MINUTES; - const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter((name) => !(name === CONST.POLICY.CONNECTIONS.NAME.NETSUITE && !canUseNetSuiteIntegration)); - const connectedIntegration = accountingIntegrations.find((integration) => !!policy?.connections?.[integration]) ?? connectionSyncProgress?.connectionName; + const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter( + (name) => + !((name === CONST.POLICY.CONNECTIONS.NAME.NETSUITE && !canUseNetSuiteIntegration) || (name === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT && !canUseSageIntacctIntegration)), + ); + + const connectedIntegration = PolicyUtils.getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName; + const policyID = policy?.id ?? '-1'; const successfulDate = getIntegrationLastSuccessfulDate(connectedIntegration ? policy?.connections?.[connectedIntegration] : undefined); - const policyConnectedToXero = connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.XERO; - const policyConnectedToNetSuite = connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.NETSUITE; - const tenants = useMemo(() => getXeroTenants(policy), [policy]); const currentXeroOrganization = findCurrentXeroOrganization(tenants, policy?.connections?.xero?.config?.tenantID); - const currentXeroOrganizationName = useMemo(() => getCurrentXeroOrganizationName(policy), [policy]); - - const netSuiteSubsidiaryList = policy?.connections?.netsuite?.options?.data?.subsidiaryList ?? []; - const netSuiteSelectedSubsidiary = policy?.connections?.netsuite?.options?.config?.subsidiary ?? ''; const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo( () => [ @@ -196,6 +201,69 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting setDateTimeToRelative(''); }, [getDatetimeToRelative, successfulDate]); + const integrationSpecificMenuItems = useMemo(() => { + const sageIntacctEntityListLength = policy?.connections?.intacct?.data?.entities?.length; + const netSuiteSubsidiaryList = policy?.connections?.netsuite?.options?.data?.subsidiaryList ?? []; + switch (connectedIntegration) { + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return { + description: translate('workspace.xero.organization'), + iconRight: Expensicons.ArrowRight, + title: getCurrentXeroOrganizationName(policy), + wrapperStyle: [styles.sectionMenuItemTopDescription], + titleStyle: styles.fontWeightNormal, + shouldShowRightIcon: tenants.length > 1, + shouldShowDescriptionOnTop: true, + onPress: () => { + if (!(tenants.length > 1)) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id ?? '-1')); + }, + pendingAction: policy?.connections?.xero?.config?.pendingFields?.tenantID, + brickRoadIndicator: policy?.connections?.xero?.config?.errorFields?.tenantID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + }; + case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: + return { + description: translate('workspace.netsuite.subsidiary'), + iconRight: Expensicons.ArrowRight, + title: policy?.connections?.netsuite?.options?.config?.subsidiary ?? '', + wrapperStyle: [styles.sectionMenuItemTopDescription], + titleStyle: styles.fontWeightNormal, + shouldShowRightIcon: netSuiteSubsidiaryList?.length > 1, + shouldShowDescriptionOnTop: true, + pendingAction: policy?.connections?.netsuite?.options?.config?.pendingFields?.subsidiary, + brickRoadIndicator: policy?.connections?.netsuite?.options?.config?.errorFields?.subsidiary ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + onPress: () => { + if (!(netSuiteSubsidiaryList?.length > 1)) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR.getRoute(policyID)); + }, + }; + case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: + return { + description: translate('workspace.intacct.entity'), + iconRight: Expensicons.ArrowRight, + title: getCurrentSageIntacctEntityName(policy), + wrapperStyle: [styles.sectionMenuItemTopDescription], + titleStyle: styles.fontWeightNormal, + shouldShowRightIcon: !!sageIntacctEntityListLength, + shouldShowDescriptionOnTop: true, + pendingAction: policy?.connections?.intacct?.config?.pendingFields?.entity, + brickRoadIndicator: policy?.connections?.intacct?.config?.errorFields?.entity ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + onPress: () => { + if (!sageIntacctEntityListLength) { + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTITY.getRoute(policyID)); + }, + }; + default: + return undefined; + } + }, [connectedIntegration, currentXeroOrganization?.id, policy, policyID, styles.fontWeightNormal, styles.sectionMenuItemTopDescription, tenants.length, translate]); + const connectionsMenuItems: MenuItemData[] = useMemo(() => { if (isEmptyObject(policy?.connections) && !isSyncInProgress) { return accountingIntegrations.map((integration) => { @@ -228,7 +296,9 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting errorText: shouldShowSynchronizationError ? translate('workspace.accounting.syncError', connectedIntegration) : undefined, errorTextStyle: [styles.mt5], shouldShowRedDotIndicator: true, - description: isSyncInProgress ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) : datetimeToRelative, + description: isSyncInProgress + ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) + : translate('workspace.accounting.lastSync', datetimeToRelative), rightComponent: isSyncInProgress ? ( ), }, - ...(policyConnectedToXero && !shouldShowSynchronizationError - ? [ - { - description: translate('workspace.xero.organization'), - iconRight: Expensicons.ArrowRight, - title: currentXeroOrganizationName, - wrapperStyle: [styles.sectionMenuItemTopDescription], - titleStyle: styles.fontWeightNormal, - shouldShowRightIcon: tenants.length > 1, - shouldShowDescriptionOnTop: true, - onPress: () => { - if (!(tenants.length > 1)) { - return; - } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id ?? '-1')); - }, - pendingAction: policy?.connections?.xero?.config?.pendingFields?.tenantID, - brickRoadIndicator: policy?.connections?.xero?.config?.errorFields?.tenantID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - }, - ] - : []), - ...(policyConnectedToNetSuite && !shouldShowSynchronizationError - ? [ - { - description: translate('workspace.netsuite.subsidiary'), - iconRight: Expensicons.ArrowRight, - title: netSuiteSelectedSubsidiary, - wrapperStyle: [styles.sectionMenuItemTopDescription], - titleStyle: styles.fontWeightNormal, - shouldShowRightIcon: netSuiteSubsidiaryList.length > 1, - shouldShowDescriptionOnTop: true, - pendingAction: policy?.connections?.netsuite?.options?.config?.pendingFields?.subsidiary, - brickRoadIndicator: policy?.connections?.netsuite?.options?.config?.errorFields?.subsidiary ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - onPress: () => { - if (!(netSuiteSubsidiaryList.length > 1)) { - return; - } - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR.getRoute(policyID)); - }, - }, - ] - : []), + ...(isEmptyObject(integrationSpecificMenuItems) || shouldShowSynchronizationError || isEmptyObject(policy?.connections) ? [] : [integrationSpecificMenuItems]), ...(isEmptyObject(policy?.connections) || shouldShowSynchronizationError ? [] : [ @@ -313,6 +342,15 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting wrapperStyle: [styles.sectionMenuItemTopDescription], onPress: integrationData?.onExportPagePress, }, + { + icon: Expensicons.ExpensifyCard, + iconRight: Expensicons.ArrowRight, + shouldShowRightIcon: true, + title: translate('workspace.accounting.cardReconciliation'), + wrapperStyle: [styles.sectionMenuItemTopDescription], + onPress: integrationData?.onCardReconciliationPagePress, + }, + { icon: Expensicons.Gear, iconRight: Expensicons.ArrowRight, @@ -333,20 +371,13 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting styles.pb0, styles.mt5, styles.popoverMenuIcon, - styles.fontWeightNormal, connectionSyncProgress?.stageInProgress, datetimeToRelative, theme.spinner, overflowMenu, threeDotsMenuPosition, - policyConnectedToXero, - currentXeroOrganizationName, - tenants.length, - policyConnectedToNetSuite, - netSuiteSelectedSubsidiary, - netSuiteSubsidiaryList.length, + integrationSpecificMenuItems, accountingIntegrations, - currentXeroOrganization?.id, ]); const otherIntegrationsItems = useMemo(() => { @@ -457,11 +488,4 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting PolicyAccountingPage.displayName = 'PolicyAccountingPage'; -export default withPolicyConnections( - withOnyx({ - connectionSyncProgress: { - key: (props) => `${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${props.route.params.policyID}`, - }, - })(PolicyAccountingPage), - false, -); +export default withPolicyConnections(PolicyAccountingPage); diff --git a/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx b/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx index 75f4cab3783d..71728d523a37 100644 --- a/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx +++ b/src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx @@ -10,7 +10,7 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import connectToSageIntacct from '@libs/actions/connections/SageIntacct'; +import {connectToSageIntacct} from '@libs/actions/connections/SageIntacct'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -20,9 +20,9 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/SageIntactCredentialsForm'; -type IntacctPrerequisitesPageProps = StackScreenProps; +type SageIntacctPrerequisitesPageProps = StackScreenProps; -function EnterSageIntacctCredentialsPage({route}: IntacctPrerequisitesPageProps) { +function EnterSageIntacctCredentialsPage({route}: SageIntacctPrerequisitesPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const policyID: string = route.params.policyID; @@ -30,8 +30,7 @@ function EnterSageIntacctCredentialsPage({route}: IntacctPrerequisitesPageProps) const confirmCredentials = useCallback( (values: FormOnyxValues) => { connectToSageIntacct(policyID, values); - Navigation.goBack(); - Navigation.goBack(); + Navigation.dismissModal(); }, [policyID], ); @@ -93,6 +92,6 @@ function EnterSageIntacctCredentialsPage({route}: IntacctPrerequisitesPageProps) ); } -EnterSageIntacctCredentialsPage.displayName = 'PolicyEnterSageIntacctCredentialsPage'; +EnterSageIntacctCredentialsPage.displayName = 'EnterSageIntacctCredentialsPage'; export default EnterSageIntacctCredentialsPage; diff --git a/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx b/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx index 7e867d88285d..c55e46083470 100644 --- a/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx +++ b/src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx @@ -6,12 +6,12 @@ import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getPoliciesConnectedToSageIntacct} from '@libs/actions/Policy/Policy'; +import {copyExistingPolicyConnection} from '@libs/actions/connections'; +import {getAdminPoliciesConnectedToSageIntacct} from '@libs/actions/Policy/Policy'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; type ExistingConnectionsPageProps = StackScreenProps; @@ -19,7 +19,7 @@ type ExistingConnectionsPageProps = StackScreenProps { @@ -30,10 +30,10 @@ function ExistingConnectionsPage({route}: ExistingConnectionsPageProps) { key: policy.id, icon: policy.avatarURL ? policy.avatarURL : ReportUtils.getDefaultWorkspaceAvatar(policy.name), iconType: policy.avatarURL ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_WORKSPACE, - description: date ? translate('workspace.intacct.sageIntacctLastSync', date) : translate('workspace.accounting.intacct'), + description: date ? translate('workspace.common.lastSyncDate', CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.intacct, date) : translate('workspace.accounting.intacct'), onPress: () => { - // waiting for backend for reusing existing connections - Navigation.goBack(ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID)); + copyExistingPolicyConnection(policy.id, policyID, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT); + Navigation.dismissModal(); }, }; }); @@ -45,9 +45,9 @@ function ExistingConnectionsPage({route}: ExistingConnectionsPageProps) { testID={ExistingConnectionsPage.displayName} > Navigation.goBack(ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack()} /> ({ + text: entity.name, + value: entity.name, + keyForList: entity.id, + isSelected: entity.id === entityID, + })) ?? []; + + const saveSelection = ({keyForList}: ListItem) => { + if (!keyForList) { + return; + } + + updateSageIntacctEntity(policyID, keyForList ?? ''); + Navigation.goBack(); + }; + + return ( + mode.isSelected)?.keyForList} + onBackButtonPress={() => Navigation.dismissModal()} + title="workspace.intacct.entity" + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.entity} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.ENTITY)} + errorRowStyles={[styles.ph5, styles.mv2]} + onClose={() => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.ENTITY)} + /> + ); +} + +SageIntacctEntityPage.displayName = 'SageIntacctEntityPage'; + +export default withPolicy(SageIntacctEntityPage); diff --git a/src/pages/workspace/accounting/intacct/IntacctPrerequisitesPage.tsx b/src/pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage.tsx similarity index 89% rename from src/pages/workspace/accounting/intacct/IntacctPrerequisitesPage.tsx rename to src/pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage.tsx index eaf46392fb04..de0e0c6b3c90 100644 --- a/src/pages/workspace/accounting/intacct/IntacctPrerequisitesPage.tsx +++ b/src/pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage.tsx @@ -23,9 +23,9 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type IntacctPrerequisitesPageProps = StackScreenProps; +type SageIntacctPrerequisitesPageProps = StackScreenProps; -function IntacctPrerequisitesPage({route}: IntacctPrerequisitesPageProps) { +function SageIntacctPrerequisitesPage({route}: SageIntacctPrerequisitesPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); @@ -67,12 +67,12 @@ function IntacctPrerequisitesPage({route}: IntacctPrerequisitesPageProps) { Navigation.goBack()} + onBackButtonPress={() => Navigation.dismissModal()} /> @@ -99,6 +99,6 @@ function IntacctPrerequisitesPage({route}: IntacctPrerequisitesPageProps) { ); } -IntacctPrerequisitesPage.displayName = 'PolicyIntacctPrerequisitesPage'; +SageIntacctPrerequisitesPage.displayName = 'SageIntacctPrerequisitesPage'; -export default IntacctPrerequisitesPage; +export default SageIntacctPrerequisitesPage; diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx new file mode 100644 index 000000000000..2458b8579539 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -0,0 +1,148 @@ +import React, {useMemo} from 'react'; +import ConnectionLayout from '@components/ConnectionLayout'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import {getCurrentSageIntacctEntityName} from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicy from '@pages/workspace/withPolicy'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import { + updateSageIntacctApprovalMode, + updateSageIntacctAutoSync, + updateSageIntacctImportEmployees, + updateSageIntacctSyncReimbursedReports, + updateSageIntacctSyncReimbursementAccountID, +} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {SageIntacctDataElement} from '@src/types/onyx/Policy'; + +function getReimbursedAccountName(bankAccounts: SageIntacctDataElement[], reimbursementAccountID?: string): string | undefined { + return bankAccounts.find((bankAccount) => bankAccount.id === reimbursementAccountID)?.name ?? reimbursementAccountID; +} + +function SageIntacctAdvancedPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + + const {importEmployees, autoSync, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {}; + const {data, config} = policy?.connections?.intacct ?? {}; + + const toggleSections = useMemo( + () => [ + { + label: translate('workspace.sageIntacct.autoSync'), + description: translate('workspace.sageIntacct.autoSyncDescription'), + isActive: !!autoSync?.enabled, + onToggle: (enabled: boolean) => updateSageIntacctAutoSync(policyID, enabled), + pendingAction: pendingFields?.enabled, + error: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED), + onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED), + }, + { + label: translate('workspace.sageIntacct.inviteEmployees'), + description: translate('workspace.sageIntacct.inviteEmployeesDescription'), + isActive: !!importEmployees, + onToggle: (enabled: boolean) => { + updateSageIntacctImportEmployees(policyID, enabled); + updateSageIntacctApprovalMode(policyID, enabled); + }, + pendingAction: pendingFields?.importEmployees, + error: + ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES) ?? + ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE), + onCloseError: () => { + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES); + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE); + }, + }, + { + label: translate('workspace.sageIntacct.syncReimbursedReports'), + description: translate('workspace.sageIntacct.syncReimbursedReportsDescription'), + isActive: !!sync?.syncReimbursedReports, + onToggle: (enabled: boolean) => { + updateSageIntacctSyncReimbursedReports(policyID, enabled); + + if (enabled && !sync?.reimbursementAccountID) { + const reimbursementAccountID = data?.bankAccounts[0]?.id ?? ''; + updateSageIntacctSyncReimbursementAccountID(policyID, reimbursementAccountID); + } + }, + pendingAction: pendingFields?.syncReimbursedReports, + error: ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS), + onCloseError: () => { + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS); + }, + }, + ], + [ + translate, + autoSync?.enabled, + pendingFields?.enabled, + pendingFields?.importEmployees, + pendingFields?.syncReimbursedReports, + config, + importEmployees, + sync?.syncReimbursedReports, + sync?.reimbursementAccountID, + policyID, + data?.bankAccounts, + ], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))} + > + {toggleSections.map((section) => ( + + ))} + + {!!sync?.syncReimbursedReports && ( + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.getRoute(policyID))} + brickRoadIndicator={errorFields?.reimbursementAccountID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + )} + + ); +} + +SageIntacctAdvancedPage.displayName = 'SageIntacctAdvancedPage'; + +export default withPolicy(SageIntacctAdvancedPage); diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx new file mode 100644 index 000000000000..3a9bd67b1e48 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx @@ -0,0 +1,76 @@ +import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getSageIntacctBankAccounts} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import {updateSageIntacctSyncReimbursementAccountID} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function SageIntacctPaymentAccountPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const {config} = policy?.connections?.intacct ?? {}; + + const vendorSelectorOptions = useMemo(() => getSageIntacctBankAccounts(policy, config?.sync?.reimbursementAccountID), [policy, config?.sync?.reimbursementAccountID]); + + const updateDefaultVendor = useCallback( + ({value}: SelectorType) => { + if (value !== config?.sync?.reimbursementAccountID) { + updateSageIntacctSyncReimbursementAccountID(policyID, value); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID)); + }, + [policyID, config?.sync?.reimbursementAccountID], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate], + ); + + return ( + mode.isSelected)?.keyForList} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID))} + title="workspace.sageIntacct.paymentAccount" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.reimbursementAccountID} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID)} + /> + ); +} + +SageIntacctPaymentAccountPage.displayName = 'SageIntacctPaymentAccountPage'; + +export default withPolicyConnections(SageIntacctPaymentAccountPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx new file mode 100644 index 000000000000..a0761a4e334b --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx @@ -0,0 +1,81 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import {updateSageIntacctExportDate} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function SageIntacctDatePage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const {config} = policy?.connections?.intacct ?? {}; + const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; + const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_EXPORT_DATE).map((dateType) => ({ + value: dateType, + text: translate(`workspace.sageIntacct.exportDate.values.${dateType}.label`), + alternateText: translate(`workspace.sageIntacct.exportDate.values.${dateType}.description`), + keyForList: dateType, + isSelected: exportConfig?.exportDate === dateType, + })); + + const headerContent = useMemo( + () => ( + + {translate('workspace.sageIntacct.exportDate.description')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + const selectExportDate = useCallback( + (row: MenuListItem) => { + if (row.value !== exportConfig?.exportDate) { + updateSageIntacctExportDate(policyID, row.value); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)); + }, + [exportConfig?.exportDate, policyID], + ); + + return ( + selectExportDate(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.exportDate} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.EXPORT_DATE)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.EXPORT_DATE)} + /> + ); +} + +SageIntacctDatePage.displayName = 'SageIntacctDatePage'; + +export default withPolicyConnections(SageIntacctDatePage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx new file mode 100644 index 000000000000..43f2e289409c --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx @@ -0,0 +1,120 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getSageIntacctNonReimbursableActiveDefaultVendor, getSageIntacctVendors} from '@libs/PolicyUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import variables from '@styles/variables'; +import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Connections} from '@src/types/onyx/Policy'; + +type SageIntacctDefaultVendorPageProps = StackScreenProps; + +function SageIntacctDefaultVendorPage({route}: SageIntacctDefaultVendorPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = route.params.policyID ?? '-1'; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const {config} = policy?.connections?.intacct ?? {}; + const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; + + const isReimbursable = route.params.reimbursable === CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE; + + let defaultVendor; + let settingName: keyof Connections['intacct']['config']['export']; + if (!isReimbursable) { + const {nonReimbursable} = exportConfig ?? {}; + defaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy); + settingName = + nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE + ? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR + : CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR; + } else { + const {reimbursableExpenseReportDefaultVendor} = exportConfig ?? {}; + defaultVendor = reimbursableExpenseReportDefaultVendor; + settingName = CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR; + } + + const vendorSelectorOptions = useMemo(() => getSageIntacctVendors(policy, defaultVendor), [defaultVendor, policy]); + + const listHeaderComponent = useMemo( + () => ( + + {translate('workspace.sageIntacct.defaultVendorDescription', isReimbursable)} + + ), + [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal, isReimbursable], + ); + + const updateDefaultVendor = useCallback( + ({value}: SelectorType) => { + if (value !== defaultVendor) { + updateSageIntacctDefaultVendor(policyID, settingName, value); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID)); + }, + [defaultVendor, policyID, settingName], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + mode.isSelected)?.keyForList} + headerContent={listHeaderComponent} + onBackButtonPress={() => + Navigation.goBack( + isReimbursable + ? ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES.getRoute(policyID) + : ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID), + ) + } + title="workspace.sageIntacct.defaultVendor" + listEmptyContent={listEmptyContent} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.[settingName]} + errors={ErrorUtils.getLatestErrorField(config, settingName)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, settingName)} + /> + ); +} + +SageIntacctDefaultVendorPage.displayName = 'SageIntacctDefaultVendorPage'; + +export default SageIntacctDefaultVendorPage; diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx new file mode 100644 index 000000000000..5c3c977a93a5 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx @@ -0,0 +1,103 @@ +import React, {useMemo} from 'react'; +import ConnectionLayout from '@components/ConnectionLayout'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {getCurrentSageIntacctEntityName} from '@libs/PolicyUtils'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function SageIntacctExportPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + + const {config} = policy?.connections?.intacct ?? {}; + const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; + + const sections = useMemo( + () => [ + { + description: translate('workspace.sageIntacct.preferredExporter'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER.getRoute(policyID)), + title: exportConfig?.exporter ?? translate('workspace.sageIntacct.notConfigured'), + hasError: !!config?.errorFields?.exporter, + pendingAction: config?.pendingFields?.exporter, + }, + { + description: translate('workspace.sageIntacct.exportDate.label'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE.getRoute(policyID)), + title: exportConfig?.exportDate ? translate(`workspace.sageIntacct.exportDate.values.${exportConfig.exportDate}.label`) : translate(`workspace.sageIntacct.notConfigured`), + hasError: !!config?.errorFields?.exportDate, + pendingAction: config?.pendingFields?.exportDate, + }, + { + description: translate('workspace.sageIntacct.reimbursableExpenses.label'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES.getRoute(policyID)), + title: exportConfig?.reimbursable + ? translate(`workspace.sageIntacct.reimbursableExpenses.values.${exportConfig.reimbursable}`) + : translate('workspace.sageIntacct.notConfigured'), + hasError: !!config?.errorFields?.reimbursable || !!config?.errorFields?.reimbursableExpenseReportDefaultVendor, + pendingAction: config?.pendingFields?.reimbursable ?? config?.pendingFields?.reimbursableExpenseReportDefaultVendor, + }, + { + description: translate('workspace.sageIntacct.nonReimbursableExpenses.label'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID)), + title: exportConfig?.nonReimbursable + ? translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${exportConfig.nonReimbursable}`) + : translate('workspace.sageIntacct.notConfigured'), + hasError: + !!config?.errorFields?.nonReimbursable ?? + !!config?.errorFields?.nonReimbursableAccount ?? + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? !!config?.errorFields?.nonReimbursableVendor + : !!config?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, + pendingAction: + config?.pendingFields?.nonReimbursable ?? + config?.errorFields?.nonReimbursableAccount ?? + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? config?.pendingFields?.nonReimbursableVendor + : config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, + }, + ], + [config, exportConfig, policyID, translate], + ); + + return ( + + {sections.map((section) => ( + + + + ))} + + ); +} + +SageIntacctExportPage.displayName = 'SageIntacctExportPage'; + +export default withPolicyConnections(SageIntacctExportPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx new file mode 100644 index 000000000000..1b191cc53627 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx @@ -0,0 +1,78 @@ +import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getSageIntacctCreditCards} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import {updateSageIntacctNonreimbursableExpensesExportAccount} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function SageIntacctNonReimbursableCreditCardAccountPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + const {config} = policy?.connections?.intacct ?? {}; + const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; + + const creditCardSelectorOptions = useMemo(() => getSageIntacctCreditCards(policy, exportConfig?.nonReimbursableAccount), [exportConfig?.nonReimbursableAccount, policy]); + + const updateCreditCardAccount = useCallback( + ({value}: SelectorType) => { + if (value !== exportConfig?.nonReimbursableAccount) { + updateSageIntacctNonreimbursableExpensesExportAccount(policyID, value); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID)); + }, + [policyID, exportConfig?.nonReimbursableAccount], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + mode.isSelected)?.keyForList} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES.getRoute(policyID))} + title="workspace.sageIntacct.creditCardAccount" + listEmptyContent={listEmptyContent} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.nonReimbursableAccount} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT)} + /> + ); +} + +SageIntacctNonReimbursableCreditCardAccountPage.displayName = 'SageIntacctNonReimbursableCreditCardAccountPage'; + +export default withPolicyConnections(SageIntacctNonReimbursableCreditCardAccountPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx new file mode 100644 index 000000000000..c87d33857513 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx @@ -0,0 +1,186 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import ConnectionLayout from '@components/ConnectionLayout'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import {getSageIntacctNonReimbursableActiveDefaultVendor} from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import {updateSageIntacctDefaultVendor, updateSageIntacctNonreimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function getDefaultVendorName(defaultVendor?: string, vendors?: SageIntacctDataElementWithValue[]): string | undefined { + return (vendors ?? []).find((vendor) => vendor.id === defaultVendor)?.value ?? defaultVendor; +} + +function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const {data: intacctData, config} = policy?.connections?.intacct ?? {}; + + const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({ + value: expenseType, + text: translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${expenseType}`), + keyForList: expenseType, + isSelected: config?.export.nonReimbursable === expenseType, + })); + + const selectNonReimbursableExpense = useCallback( + (row: MenuListItem) => { + if (row.value === config?.export.nonReimbursable) { + return; + } + updateSageIntacctNonreimbursableExpensesExportDestination(policyID, row.value); + }, + [config?.export.nonReimbursable, policyID], + ); + + const defaultVendor = useMemo(() => { + const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy); + const defaultVendorName = getDefaultVendorName(activeDefaultVendor, intacctData?.vendors); + + const defaultVendorSection = { + description: translate('workspace.sageIntacct.defaultVendor'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase())), + title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), + hasError: + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? !!config?.errorFields?.nonReimbursableVendor + : !!config?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, + pendingAction: + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? config?.pendingFields?.nonReimbursableVendor + : config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, + }; + + return ( + + + + ); + }, [ + config?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, + config?.errorFields?.nonReimbursableVendor, + config?.export.nonReimbursable, + config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, + config?.pendingFields?.nonReimbursableVendor, + intacctData?.vendors, + policy, + policyID, + translate, + ]); + + const creditCardAccount = useMemo(() => { + const creditCardAccountSection = { + description: translate('workspace.sageIntacct.creditCardAccount'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.getRoute(policyID)), + title: config?.export.nonReimbursableAccount ? config.export.nonReimbursableAccount : translate('workspace.sageIntacct.notConfigured'), + hasError: !!config?.errorFields?.nonReimbursableAccount, + pendingAction: config?.pendingFields?.nonReimbursableAccount, + }; + + return ( + + + + ); + }, [config?.errorFields?.nonReimbursableAccount, config?.export.nonReimbursableAccount, config?.pendingFields?.nonReimbursableAccount, policyID, translate]); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + displayName={SageIntacctNonReimbursableExpensesPage.displayName} + policyID={policyID} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + shouldUseScrollView={false} + shouldIncludeSafeAreaPaddingBottom + > + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE)} + style={[styles.flexGrow1, styles.flexShrink1]} + contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]} + > + selectNonReimbursableExpense(selection as MenuListItem)} + sections={[{data}]} + ListItem={RadioListItem} + showScrollIndicator + shouldShowTooltips={false} + containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]} + /> + + + {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL && defaultVendor} + {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && ( + + {creditCardAccount} + { + const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; + updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor); + }} + wrapperStyle={[styles.ph5, styles.pv3]} + pendingAction={config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR)} + onCloseError={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR)} + /> + {!!config?.export.nonReimbursableCreditCardChargeDefaultVendor && defaultVendor} + + )} + + + ); +} + +SageIntacctNonReimbursableExpensesPage.displayName = 'SageIntacctNonReimbursableExpensesPage'; + +export default withPolicyConnections(SageIntacctNonReimbursableExpensesPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx new file mode 100644 index 000000000000..5d248aec382d --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx @@ -0,0 +1,111 @@ +import {isEmpty} from 'lodash'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import {getAdminEmployees, isExpensifyTeam} from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import {updateSageIntacctExporter} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type CardListItem = ListItem & { + value: string; +}; + +function SageIntacctPreferredExporterPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyOwner = policy?.owner ?? ''; + const {config} = policy?.connections?.intacct ?? {}; + const {export: exportConfiguration} = policy?.connections?.intacct?.config ?? {}; + const exporters = getAdminEmployees(policy); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + + const policyID = policy?.id ?? '-1'; + const data: CardListItem[] = useMemo(() => { + if (!isEmpty(policyOwner) && isEmpty(exporters)) { + return [ + { + value: policyOwner, + text: policyOwner, + keyForList: policyOwner, + isSelected: exportConfiguration?.exporter === policyOwner, + }, + ]; + } + + return exporters?.reduce((options, exporter) => { + if (!exporter.email) { + return options; + } + + // Don't show guides if the current user is not a guide themselves or an Expensify employee + if (isExpensifyTeam(exporter.email) && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) { + return options; + } + + options.push({ + value: exporter.email, + text: exporter.email, + keyForList: exporter.email, + isSelected: exportConfiguration?.exporter === exporter.email, + }); + return options; + }, []); + }, [exportConfiguration, exporters, policyOwner, currentUserLogin]); + + const selectExporter = useCallback( + (row: CardListItem) => { + if (row.value !== exportConfiguration?.exporter) { + updateSageIntacctExporter(policyID, row.value); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)); + }, + [policyID, exportConfiguration], + ); + + const headerContent = useMemo( + () => ( + + {translate('workspace.sageIntacct.exportPreferredExporterNote')} + {translate('workspace.sageIntacct.exportPreferredExporterSubNote')} + + ), + [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal], + ); + + return ( + mode.isSelected)?.keyForList} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} + title="workspace.sageIntacct.preferredExporter" + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.exporter} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.EXPORTER)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.EXPORTER)} + /> + ); +} + +SageIntacctPreferredExporterPage.displayName = 'SageIntacctPreferredExporterPage'; + +export default withPolicyConnections(SageIntacctPreferredExporterPage); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx new file mode 100644 index 000000000000..f03094393667 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -0,0 +1,148 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import ConnectionLayout from '@components/ConnectionLayout'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import {updateSageIntacctDefaultVendor, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function getDefaultVendorName(defaultVendor?: string, vendors?: SageIntacctDataElementWithValue[]): string | undefined { + return (vendors ?? []).find((vendor) => vendor.id === defaultVendor)?.value ?? defaultVendor; +} + +function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const {data: intacctData, config} = policy?.connections?.intacct ?? {}; + const {reimbursable, reimbursableExpenseReportDefaultVendor} = policy?.connections?.intacct?.config?.export ?? {}; + + const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE).map((expenseType) => ({ + value: expenseType, + text: translate(`workspace.sageIntacct.reimbursableExpenses.values.${expenseType}`), + keyForList: expenseType, + isSelected: reimbursable === expenseType, + })); + + const selectReimbursableDestination = useCallback( + (row: MenuListItem) => { + if (row.value !== reimbursable) { + updateSageIntacctReimbursableExpensesExportDestination(policyID, row.value); + } + if (row.value === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL) { + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)); + } + }, + [reimbursable, policyID], + ); + + const defaultVendor = useMemo(() => { + const defaultVendorName = getDefaultVendorName(reimbursableExpenseReportDefaultVendor, intacctData?.vendors); + const defaultVendorSection = { + description: translate('workspace.sageIntacct.defaultVendor'), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)), + title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), + hasError: !!config?.errorFields?.reimbursableExpenseReportDefaultVendor, + pendingAction: config?.pendingFields?.reimbursableExpenseReportDefaultVendor, + }; + + return ( + + + + ); + }, [ + config?.errorFields?.reimbursableExpenseReportDefaultVendor, + config?.pendingFields?.reimbursableExpenseReportDefaultVendor, + intacctData?.vendors, + policyID, + reimbursableExpenseReportDefaultVendor, + translate, + ]); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + displayName={SageIntacctReimbursableExpensesPage.displayName} + policyID={policyID} + connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + shouldUseScrollView={false} + shouldIncludeSafeAreaPaddingBottom + > + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)} + style={[styles.flexGrow1, styles.flexShrink1]} + contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]} + > + selectReimbursableDestination(selection as MenuListItem)} + sections={[{data}]} + ListItem={RadioListItem} + showScrollIndicator + shouldShowTooltips={false} + containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]} + /> + + {reimbursable === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT && ( + + { + const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; + updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor); + }} + pendingAction={config?.pendingFields?.reimbursableExpenseReportDefaultVendor} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR)} + wrapperStyle={[styles.ph5, styles.pv3]} + onCloseError={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR)} + /> + {!!reimbursableExpenseReportDefaultVendor && defaultVendor} + + )} + + ); +} + +SageIntacctReimbursableExpensesPage.displayName = 'SageIntacctReimbursableExpensesPage'; + +export default withPolicyConnections(SageIntacctReimbursableExpensesPage); diff --git a/src/pages/workspace/accounting/intacct/import/DimensionTypeSelector.tsx b/src/pages/workspace/accounting/intacct/import/DimensionTypeSelector.tsx new file mode 100644 index 000000000000..05d0c0438c8f --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/DimensionTypeSelector.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import {View} from 'react-native'; +import FormHelpMessage from '@components/FormHelpMessage'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; + +type DimensionTypeSelectorProps = { + /** Error text to display */ + errorText?: string; + + /** Business type to display */ + value?: string; + + /** Callback to call when the input changes */ + onInputChange?: (value: string) => void; +}; + +function DimensionTypeSelector({errorText = '', value = '', onInputChange}: DimensionTypeSelectorProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const selectionOptions = [ + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + text: translate('common.tag'), + alternateText: translate('workspace.common.lineItemLevel'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + isSelected: value === CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + }, + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + text: translate('workspace.common.reportField'), + alternateText: translate('workspace.common.reportLevel'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + isSelected: value === CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + }, + ]; + + const onDimensionTypeSelected = (dimensionType: SelectorType) => { + if (!onInputChange || dimensionType.value === value) { + return; + } + onInputChange(dimensionType.value); + }; + + return ( + + {translate('workspace.common.displayedAs')} + + {selectionOptions.map((option) => ( + + ))} + {!!errorText && ( + + )} + + + ); +} + +export default DimensionTypeSelector; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx new file mode 100644 index 000000000000..3dc7a325650b --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx @@ -0,0 +1,101 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import ConnectionLayout from '@components/ConnectionLayout'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {addSageIntacctUserDimensions} from '@libs/actions/connections/SageIntacct'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import withPolicy from '@pages/workspace/withPolicy'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/SageIntacctDimensionsForm'; +import DimensionTypeSelector from './DimensionTypeSelector'; + +function SageIntacctAddUserDimensionPage({policy}: WithPolicyProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions; + const {inputCallbackRef} = useAutoFocusInput(); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + + if (!values[INPUT_IDS.INTEGRATION_NAME]) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('common.error.fieldRequired')); + } + + if (userDimensions?.some((userDimension) => userDimension.dimension === values[INPUT_IDS.INTEGRATION_NAME])) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('workspace.intacct.dimensionExists')); + } + + if (!values[INPUT_IDS.DIMENSION_TYPE]) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.DIMENSION_TYPE, translate('common.error.fieldRequired')); + } + return errors; + }, + [translate, userDimensions], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID))} + > + { + addSageIntacctUserDimensions(policyID, value[INPUT_IDS.INTEGRATION_NAME], value[INPUT_IDS.DIMENSION_TYPE], userDimensions ?? []); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID)); + }} + submitButtonText={translate('common.confirm')} + enabledWhenOffline + shouldValidateOnBlur + shouldValidateOnChange + > + + + + + + + + + ); +} + +SageIntacctAddUserDimensionPage.displayName = 'SageIntacctAddUserDimensionPage'; + +export default withPolicy(SageIntacctAddUserDimensionPage); diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx new file mode 100644 index 000000000000..d0d9578a72b2 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctEditUserDimensionsPage.tsx @@ -0,0 +1,141 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; +import ConnectionLayout from '@components/ConnectionLayout'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearSageIntacctErrorField, editSageIntacctUserDimensions, removeSageIntacctUserDimensions} from '@libs/actions/connections/SageIntacct'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/SageIntacctDimensionsForm'; +import DimensionTypeSelector from './DimensionTypeSelector'; + +type SageIntacctEditUserDimensionsPageProps = StackScreenProps; + +function SageIntacctEditUserDimensionsPage({route}: SageIntacctEditUserDimensionsPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const editedUserDimensionName: string = route.params.dimensionName; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); + const policyID: string = policy?.id ?? '-1'; + const config = policy?.connections?.intacct?.config; + const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions; + const editedUserDimension = userDimensions?.find((userDimension) => userDimension.dimension === editedUserDimensionName); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + + if (!values[INPUT_IDS.INTEGRATION_NAME]) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('common.error.fieldRequired')); + } + + if (userDimensions?.some((userDimension) => userDimension.dimension === values[INPUT_IDS.INTEGRATION_NAME] && editedUserDimensionName !== values[INPUT_IDS.INTEGRATION_NAME])) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.INTEGRATION_NAME, translate('workspace.intacct.dimensionExists')); + } + + if (!values[INPUT_IDS.DIMENSION_TYPE]) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.DIMENSION_TYPE, translate('common.error.fieldRequired')); + } + return errors; + }, + [editedUserDimensionName, translate, userDimensions], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID))} + > + { + editSageIntacctUserDimensions(policyID, editedUserDimensionName, value[INPUT_IDS.INTEGRATION_NAME], value[INPUT_IDS.DIMENSION_TYPE], userDimensions ?? []); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID)); + }} + submitButtonText={translate('common.save')} + enabledWhenOffline + shouldValidateOnBlur + shouldValidateOnChange + > + clearSageIntacctErrorField(policyID, `dimension_${editedUserDimensionName}`)} + > + + + + + + + + setIsDeleteModalOpen(true)} + /> + + + { + setIsDeleteModalOpen(false); + removeSageIntacctUserDimensions(policyID, editedUserDimensionName, userDimensions ?? []); + Navigation.goBack(); + }} + onCancel={() => setIsDeleteModalOpen(false)} + prompt={translate('workspace.intacct.removeDimensionPrompt')} + confirmText={translate('common.remove')} + cancelText={translate('common.cancel')} + danger + shouldEnableNewFocusManagement + /> + + + ); +} + +SageIntacctEditUserDimensionsPage.displayName = 'SageIntacctEditUserDimensionsPage'; + +export default SageIntacctEditUserDimensionsPage; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx new file mode 100644 index 000000000000..543d3f4d0154 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctImportPage.tsx @@ -0,0 +1,146 @@ +import {Str} from 'expensify-common'; +import React, {useMemo} from 'react'; +import ConnectionLayout from '@components/ConnectionLayout'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearSageIntacctErrorField, updateSageIntacctBillable, updateSageIntacctSyncTaxConfiguration} from '@libs/actions/connections/SageIntacct'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getCurrentSageIntacctEntityName} from '@libs/PolicyUtils'; +import withPolicy from '@pages/workspace/withPolicy'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ROUTES from '@src/ROUTES'; +import type {SageIntacctConnectionsConfig, SageIntacctMappingValue} from '@src/types/onyx/Policy'; + +function getDisplayTypeTranslationKey(displayType?: SageIntacctMappingValue): TranslationPaths | undefined { + switch (displayType) { + case CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT: { + return 'workspace.intacct.employeeDefault'; + } + case CONST.SAGE_INTACCT_MAPPING_VALUE.TAG: { + return 'workspace.accounting.importTypes.TAG'; + } + case CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD: { + return 'workspace.accounting.importTypes.REPORT_FIELD'; + } + default: { + return 'workspace.accounting.notImported'; + } + } +} + +const checkForUserDimensionWithError = (config?: SageIntacctConnectionsConfig) => + config?.mappings?.dimensions?.some((dimension) => !!config?.errorFields?.[`dimension_${dimension.dimension}`]); + +const checkForUserDimensionWithPendingAction = (config?: SageIntacctConnectionsConfig) => + config?.mappings?.dimensions?.some((dimension) => !!config?.pendingFields?.[`dimension_${dimension.dimension}`]); + +function SageIntacctImportPage({policy}: WithPolicyProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const policyID: string = policy?.id ?? '-1'; + const sageIntacctConfig = policy?.connections?.intacct?.config; + + const mapingItems = useMemo( + () => + Object.values(CONST.SAGE_INTACCT_CONFIG.MAPPINGS).map((mapping) => { + const menuItemTitleKey = getDisplayTypeTranslationKey(sageIntacctConfig?.mappings?.[mapping]); + return { + description: Str.recapitalize(translate('workspace.intacct.mappingTitle', mapping)), + action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.getRoute(policyID, mapping)), + title: menuItemTitleKey ? translate(menuItemTitleKey) : undefined, + hasError: !!sageIntacctConfig?.errorFields?.[mapping], + pendingAction: sageIntacctConfig?.pendingFields?.[mapping], + }; + }), + [policyID, sageIntacctConfig?.errorFields, sageIntacctConfig?.mappings, sageIntacctConfig?.pendingFields, translate], + ); + + return ( + + {}} + disabled + /> + updateSageIntacctBillable(policyID, !sageIntacctConfig?.mappings?.syncItems)} + pendingAction={sageIntacctConfig?.pendingFields?.syncItems} + errors={ErrorUtils.getLatestErrorField(sageIntacctConfig ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_ITEMS)} + onCloseError={() => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_ITEMS)} + /> + + {mapingItems.map((section) => ( + + + + ))} + + updateSageIntacctSyncTaxConfiguration(policyID, !sageIntacctConfig?.tax?.syncTax)} + pendingAction={sageIntacctConfig?.pendingFields?.tax} + errors={ErrorUtils.getLatestErrorField(sageIntacctConfig ?? {}, CONST.SAGE_INTACCT_CONFIG.TAX)} + onCloseError={() => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.TAX)} + /> + + + 0 + ? translate('workspace.intacct.userDimensionsAdded', sageIntacctConfig?.mappings?.dimensions?.length) + : undefined + } + description={translate('workspace.intacct.userDefinedDimensions')} + shouldShowRightIcon + onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS.getRoute(policyID))} + brickRoadIndicator={checkForUserDimensionWithError(sageIntacctConfig) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + + ); +} + +SageIntacctImportPage.displayName = 'SageIntacctImportPage'; + +export default withPolicy(SageIntacctImportPage); diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx new file mode 100644 index 000000000000..57ebac617393 --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx @@ -0,0 +1,81 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import {updateSageIntacctMappingValue} from '@libs/actions/connections/SageIntacct'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {SageIntacctMappingName, SageIntacctMappingValue} from '@src/types/onyx/Policy'; + +type SageIntacctMappingsTypePageProps = StackScreenProps; + +function SageIntacctMappingsTypePage({route}: SageIntacctMappingsTypePageProps) { + const {translate} = useLocalize(); + + const mappingName: SageIntacctMappingName = route.params.mapping; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); + const policyID = policy?.id ?? '-1'; + const mappings = policy?.connections?.intacct?.config?.mappings; + + const selectionOptions = useMemo( + () => [ + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT, + text: translate('workspace.intacct.employeeDefault'), + alternateText: translate('workspace.common.appliedOnExport'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT, + isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT, + }, + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + text: translate('workspace.common.tags'), + alternateText: translate('workspace.common.lineItemLevel'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.TAG, + }, + { + value: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + text: translate('workspace.common.reportFields'), + alternateText: translate('workspace.common.reportLevel'), + keyForList: CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + isSelected: mappings?.[mappingName] === CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD, + }, + ], + [mappingName, mappings, translate], + ); + + const updateMapping = useCallback( + ({value}: SelectorType) => { + updateSageIntacctMappingValue(policyID, mappingName, value as SageIntacctMappingValue); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.getRoute(policyID, mappingName)); + }, + [mappingName, policyID], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS.getRoute(policyID, mappingName))} + title="workspace.common.displayedAs" + /> + ); +} + +SageIntacctMappingsTypePage.displayName = 'SageIntacctMappingsTypePage'; + +export default SageIntacctMappingsTypePage; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx new file mode 100644 index 000000000000..c76a9f0e26bc --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx @@ -0,0 +1,122 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import {Str} from 'expensify-common'; +import React, {useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import ConnectionLayout from '@components/ConnectionLayout'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearSageIntacctErrorField, updateSageIntacctMappingValue} from '@libs/actions/connections/SageIntacct'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {SageIntacctMappingName, SageIntacctMappingValue} from '@src/types/onyx/Policy'; + +type SageIntacctToggleMappingsPageProps = StackScreenProps; + +type DisplayTypeTranslationKeys = { + titleKey: TranslationPaths; + descriptionKey: TranslationPaths; +}; + +function getDisplayTypeTranslationKeys(displayType?: SageIntacctMappingValue): DisplayTypeTranslationKeys | undefined { + switch (displayType) { + case CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT: { + return {titleKey: 'workspace.intacct.employeeDefault', descriptionKey: 'workspace.intacct.employeeDefaultDescription'}; + } + case CONST.SAGE_INTACCT_MAPPING_VALUE.TAG: { + return {titleKey: 'workspace.common.tags', descriptionKey: 'workspace.intacct.displayedAsTagDescription'}; + } + case CONST.SAGE_INTACCT_MAPPING_VALUE.REPORT_FIELD: { + return {titleKey: 'workspace.common.reportFields', descriptionKey: 'workspace.intacct.displayedAsReportFieldDescription'}; + } + default: { + return undefined; + } + } +} + +function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID ?? '-1'}`); + const mappingName: SageIntacctMappingName = route.params.mapping; + const policyID: string = policy?.id ?? '-1'; + + const config = policy?.connections?.intacct?.config; + const translationKeys = getDisplayTypeTranslationKeys(config?.mappings?.[mappingName]); + const [importMapping, setImportMapping] = useState(config?.mappings?.[mappingName] && config?.mappings?.[mappingName] !== CONST.SAGE_INTACCT_MAPPING_VALUE.NONE); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID))} + > + + {translate('workspace.intacct.toggleImportTitleFirstPart')} + {translate('workspace.intacct.mappingTitle', mappingName)} + {translate('workspace.intacct.toggleImportTitleSecondPart')} + + clearSageIntacctErrorField(policyID, mappingName)} + > + { + if (importMapping) { + setImportMapping(false); + updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.NONE); + } else { + setImportMapping(true); + updateSageIntacctMappingValue(policyID, mappingName, CONST.SAGE_INTACCT_MAPPING_VALUE.DEFAULT); + } + }} + /> + {importMapping && ( + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE.getRoute(policyID, mappingName))} + /> + + {translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined} + + + )} + + + ); +} + +SageIntacctToggleMappingsPage.displayName = 'SageIntacctToggleMappingsPage'; + +export default SageIntacctToggleMappingsPage; diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage.tsx new file mode 100644 index 000000000000..4a6d5b7bd21c --- /dev/null +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctUserDimensionsPage.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import ConnectionLayout from '@components/ConnectionLayout'; +import FixedFooter from '@components/FixedFooter'; +import Icon from '@components/Icon'; +import * as Illustrations from '@components/Icon/Illustrations'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import withPolicy from '@pages/workspace/withPolicy'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function SageIntacctUserDimensionsPage({policy}: WithPolicyProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + const config = policy?.connections?.intacct?.config; + const userDimensions = policy?.connections?.intacct?.config?.mappings?.dimensions ?? []; + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID))} + > + {userDimensions?.length === 0 ? ( + + + + + + {translate('workspace.intacct.addAUserDefinedDimension')} + + + + + { + Link.openExternalLink(CONST.SAGE_INTACCT_INSTRUCTIONS); + }} + > + {translate('workspace.intacct.detailedInstructionsLink')} + + {translate('workspace.intacct.detailedInstructionsRestOfSentence')} + + + + + ) : ( + <> + + + { + Link.openExternalLink(CONST.SAGE_INTACCT_INSTRUCTIONS); + }} + > + {translate('workspace.intacct.detailedInstructionsLink')} + + {translate('workspace.intacct.detailedInstructionsRestOfSentence')} + + + + {userDimensions.map((userDimension) => ( + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EDIT_USER_DIMENSION.getRoute(policyID, userDimension.dimension))} + brickRoadIndicator={config?.errorFields?.[`dimension_${userDimension.dimension}`] ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + ))} + + + )} + +