diff --git a/.github/actions/composite/setupGitForOSBotify/action.yml b/.github/actions/composite/setupGitForOSBotify/action.yml index 456cef93676a..9495ba07b0ac 100644 --- a/.github/actions/composite/setupGitForOSBotify/action.yml +++ b/.github/actions/composite/setupGitForOSBotify/action.yml @@ -2,20 +2,25 @@ name: 'Setup Git for OSBotify' description: 'Setup Git for OSBotify' inputs: - GPG_PASSPHRASE: - description: 'Passphrase used to decrypt GPG key' + OP_SERVICE_ACCOUNT_TOKEN: + description: "1Password service account token" required: true runs: using: composite steps: - - name: Decrypt OSBotify GPG key - run: cd .github/workflows && gpg --quiet --batch --yes --decrypt --passphrase=${{ inputs.GPG_PASSPHRASE }} --output OSBotify-private-key.asc OSBotify-private-key.asc.gpg + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password shell: bash + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.OP_SERVICE_ACCOUNT_TOKEN }} + run: op read "op://Mobile-Deploy-CI/OSBotify-private-key.asc/OSBotify-private-key.asc" --force --out-file ./OSBotify-private-key.asc - name: Import OSBotify GPG Key shell: bash - run: cd .github/workflows && gpg --import OSBotify-private-key.asc + run: gpg --import OSBotify-private-key.asc - name: Set up git for OSBotify shell: bash @@ -24,8 +29,3 @@ runs: git config --global commit.gpgsign true git config --global user.name OSBotify git config --global user.email infra+osbotify@expensify.com - - - name: Enable debug logs for git - shell: bash - if: runner.debug == '1' - run: echo "GIT_TRACE=true" >> "$GITHUB_ENV" diff --git a/.github/actions/composite/setupGitForOSBotifyApp/action.yml b/.github/actions/composite/setupGitForOSBotifyApp/action.yml index 404ddc55e954..45d85663bfa6 100644 --- a/.github/actions/composite/setupGitForOSBotifyApp/action.yml +++ b/.github/actions/composite/setupGitForOSBotifyApp/action.yml @@ -5,8 +5,8 @@ name: "Setup Git for OSBotify" description: "Setup Git for OSBotify" inputs: - GPG_PASSPHRASE: - description: "Passphrase used to decrypt GPG key" + OP_SERVICE_ACCOUNT_TOKEN: + description: "1Password service account token" required: true OS_BOTIFY_APP_ID: description: "Application ID for OS Botify" @@ -39,13 +39,18 @@ runs: sparse-checkout: | .github - - name: Decrypt OSBotify GPG key - run: cd .github/workflows && gpg --quiet --batch --yes --decrypt --passphrase=${{ inputs.GPG_PASSPHRASE }} --output OSBotify-private-key.asc OSBotify-private-key.asc.gpg + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password shell: bash + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.OP_SERVICE_ACCOUNT_TOKEN }} + run: op read "op://Mobile-Deploy-CI/OSBotify-private-key.asc/OSBotify-private-key.asc" --force --out-file ./OSBotify-private-key.asc - name: Import OSBotify GPG Key shell: bash - run: cd .github/workflows && gpg --import OSBotify-private-key.asc + run: gpg --import OSBotify-private-key.asc - name: Set up git for OSBotify shell: bash @@ -55,11 +60,6 @@ runs: git config user.name OSBotify git config user.email infra+osbotify@expensify.com - - name: Enable debug logs for git - shell: bash - if: runner.debug == '1' - run: echo "GIT_TRACE=true" >> "$GITHUB_ENV" - - name: Sync clock shell: bash run: sudo sntp -sS time.windows.com diff --git a/.github/workflows/OSBotify-private-key.asc.gpg b/.github/workflows/OSBotify-private-key.asc.gpg deleted file mode 100644 index 03f06222d0fe..000000000000 Binary files a/.github/workflows/OSBotify-private-key.asc.gpg and /dev/null differ diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 45dacacd0b16..73b62556ffd4 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -80,14 +80,6 @@ git fetch origin tag 1.0.1-0 --no-tags --shallow-exclude=1.0.0-0 # This will fet ## Secrets The GitHub workflows require a large list of secrets to deploy, notify and test the code: -1. `LARGE_SECRET_PASSPHRASE` - decrypts secrets stored in various encrypted files stored in GitHub repository. To create updated versions of these encrypted files, refer to steps 1-4 of [this encrypted secrets help page](https://docs.github.com/en/actions/reference/encrypted-secrets#limits-for-secrets) using the `LARGE_SECRET_PASSPHRASE`. - 1. `android/app/my-upload-key.keystore.gpg` - 1. `android/app/android-fastlane-json-key.json.gpg` - 1. `ios/NewApp_AdHoc.mobileprovision` - 1. `ios/NewApp_AdHoc_Notification_Service.mobileprovision` - 1. `ios/NewApp_AppStore.mobileprovision.gpg` - 1. `ios/NewApp_AppStore_Notification_Service.mobileprovision.gpg` - 1. `ios/Certificates.p12.gpg` 1. `SLACK_WEBHOOK` - Sends Slack notifications via Slack WebHook https://expensify.slack.com/services/B01AX48D7MM 1. `OS_BOTIFY_TOKEN` - Personal access token for @OSBotify user in GitHub 1. `CLA_BOTIFY_TOKEN` - Personal access token for @CLABotify user in GitHub @@ -105,6 +97,11 @@ The GitHub workflows require a large list of secrets to deploy, notify and test 1. `APPLE_DEMO_PASSWORD` - Demo account password used for https://appstoreconnect.apple.com/ 1. `BROWSERSTACK` - Used to access Browserstack's API +We use 1Password for many secrets and in general use two different actions from 1Password to fetch secrets: + +1. `1password/install-cli-action` - This action is used to install 1Password cli `op` and is used to grab **files** using the `op read` command. +1. `1password/load-secrets-action` - This action is used to fetch **strings** from 1Password. + ### Important note about Secrets Secrets are available by default in most workflows. The exception to the rule is callable workflows. If a workflow is triggered by the `workflow_call` event, it will only have access to repo secrets if the workflow that called it passed in the secrets explicitly (for example, using `secrets: inherit`). diff --git a/.github/workflows/androidBump.yml b/.github/workflows/androidBump.yml index e10304d1d922..5ea71c028e15 100644 --- a/.github/workflows/androidBump.yml +++ b/.github/workflows/androidBump.yml @@ -21,9 +21,14 @@ jobs: with: bundler-cache: true - - name: Decrypt json Google Play credentials - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password working-directory: android/app + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: op read "op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - name: Get status from Google Play and generate next rollout percentage id: checkAndroidStatus diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index d7784e2f610b..c4d33b00bef4 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -85,9 +85,14 @@ jobs: with: bundler-cache: true - - name: Decrypt keystore to sign the APK/AAB - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output my-upload-key.keystore my-upload-key.keystore.gpg + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password working-directory: android/app + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: op read "op://Mobile-Deploy-CI/New Expensify my-upload-key.keystore/my-upload-key.keystore" --force --out-file ./my-upload-key.keystore - name: Get package version id: getPackageVersion diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index 5cb0a99730c9..a4ec14ff6825 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -43,9 +43,9 @@ jobs: - name: Set up git for OSBotify id: setupGitForOSBotify - uses: ./.github/actions/composite/setupGitForOSBotifyApp + uses: Expensify/App/.github/actions/composite/setupGitForOSBotifyApp@main with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} diff --git a/.github/workflows/compareNDandODbuilds.yml b/.github/workflows/compareNDandODbuilds.yml index 51ba44a192e9..99a5de896501 100644 --- a/.github/workflows/compareNDandODbuilds.yml +++ b/.github/workflows/compareNDandODbuilds.yml @@ -53,11 +53,13 @@ jobs: uses: 1password/install-cli-action@v1 - name: Load files from 1Password + working-directory: android/app env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op document get --output ./upload-key.keystore upload-key.keystore - op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json + op read "op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json + op read "op://Mobile-Deploy-CI/New Expensify my-upload-key.keystore/my-upload-key.keystore" --force --out-file ./my-upload-key.keystore + # Copy the keystore to the Android directory for Fullstory cp ./upload-key.keystore Mobile-Expensify/Android @@ -104,9 +106,14 @@ jobs: with: IS_HYBRID_BUILD: 'false' - - name: Decrypt keystore to sign the APK/AAB - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output my-upload-key.keystore my-upload-key.keystore.gpg + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password working-directory: android/app + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: op read "op://Mobile-Deploy-CI/New Expensify my-upload-key.keystore/my-upload-key.keystore" --force --out-file ./my-upload-key.keystore - name: Build Android Release working-directory: android diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index 85c928707c6c..a933c2f1686f 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -23,15 +23,15 @@ on: value: ${{ jobs.createNewVersion.outputs.NEW_VERSION }} secrets: - LARGE_SECRET_PASSPHRASE: - description: Passphrase used to decrypt GPG key - required: true SLACK_WEBHOOK: description: Webhook used to comment in slack required: true OS_BOTIFY_COMMIT_TOKEN: description: OSBotify personal access token, used to workaround committing to protected branch required: true + OP_SERVICE_ACCOUNT_TOKEN: + description: 1Password service account token + required: true jobs: validateActor: @@ -74,7 +74,7 @@ jobs: uses: ./.github/actions/composite/setupGitForOSBotify id: setupGitForOSBotify with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - name: Generate new E/App version id: bumpVersion diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ca2f0f8a698..4b4ea2413ae5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,7 +45,7 @@ jobs: uses: ./.github/actions/composite/setupGitForOSBotifyApp id: setupGitForOSBotify with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} @@ -97,12 +97,14 @@ jobs: pattern: android-*-artifact merge-multiple: true - - name: Log downloaded artifact paths - run: ls -R /tmp/artifacts + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 - - name: Decrypt json w/ Google Play credentials - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg + - name: Load files from 1Password working-directory: android/app + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: op read "op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json - name: Upload Android app to Google Play run: bundle exec fastlane android upload_google_play_internal @@ -166,9 +168,10 @@ jobs: env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op read op://Mobile-Deploy-CI/firebase.json/firebase.json --force --out-file ./firebase.json - op read op://Mobile-Deploy-CI/upload-key.keystore/upload-key.keystore --force --out-file ./upload-key.keystore - op read op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json --force --out-file ./android-fastlane-json-key.json + op read "op://Mobile-Deploy-CI/firebase.json/firebase.json" --force --out-file ./firebase.json + op read "op://Mobile-Deploy-CI/upload-key.keystore/upload-key.keystore" --force --out-file ./upload-key.keystore + op read "op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json + # Copy the keystore to the Android directory for Fullstory cp ./upload-key.keystore Mobile-Expensify/Android @@ -298,10 +301,15 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode - - name: Decrypt Developer ID Certificate - run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg + - name: Load Desktop credentials from 1Password + id: load-credentials + uses: 1password/load-secrets-action@v2 + with: + export-env: false env: - DEVELOPER_ID_SECRET_PASSPHRASE: ${{ secrets.DEVELOPER_ID_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + DESKTOP_CERTIFICATE_BASE64: "op://Mobile-Deploy-CI/Desktop Certificates.p12/CSC_LINK" + DESKTOP_CERTIFICATE_PASSWORD: "op://Mobile-Deploy-CI/Desktop Certificates.p12/CSC_KEY_PASSWORD" - name: Build desktop app run: | @@ -311,8 +319,8 @@ jobs: npm run desktop-build-staging fi env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + CSC_LINK: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_BASE64 }} + CSC_KEY_PASSWORD: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} @@ -373,25 +381,17 @@ jobs: max_attempts: 5 command: scripts/pod-install.sh - - name: Decrypt AppStore profile - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt AppStore Notification Service profile - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt certificate - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 - - name: Decrypt App Store Connect API key - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg + - name: Load files from 1Password env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://Mobile-Deploy-CI/NewApp_AppStore/NewApp_AppStore.mobileprovision" --force --out-file ./NewApp_AppStore.mobileprovision + op read "op://Mobile-Deploy-CI/NewApp_AppStore_Notification_Service/NewApp_AppStore_Notification_Service.mobileprovision" --force --out-file ./NewApp_AppStore_Notification_Service.mobileprovision + op read "op://Mobile-Deploy-CI/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 + op read "op://Mobile-Deploy-CI/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json - name: Get iOS native version id: getIOSVersion @@ -511,30 +511,12 @@ jobs: env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op read op://Mobile-Deploy-CI/firebase.json/firebase.json --force --out-file ./firebase.json - op read op://Mobile-Deploy-CI/OldApp_AppStore/OldApp_AppStore.mobileprovision --force --out-file ./OldApp_AppStore.mobileprovision - op read op://Mobile-Deploy-CI/OldApp_AppStore_Share_Extension/OldApp_AppStore_Share_Extension.mobileprovision --force --out-file ./OldApp_AppStore_Share_Extension.mobileprovision - op read op://Mobile-Deploy-CI/OldApp_AppStore_Notification_Service/OldApp_AppStore_Notification_Service.mobileprovision --force --out-file ./OldApp_AppStore_Notification_Service.mobileprovision - - - name: Decrypt AppStore profile - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt AppStore Notification Service profile - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt certificate - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt App Store Connect API key - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + op read "op://Mobile-Deploy-CI/firebase.json/firebase.json" --force --out-file ./firebase.json + op read "op://Mobile-Deploy-CI/OldApp_AppStore/OldApp_AppStore.mobileprovision" --force --out-file ./OldApp_AppStore.mobileprovision + op read "op://Mobile-Deploy-CI/OldApp_AppStore_Share_Extension/OldApp_AppStore_Share_Extension.mobileprovision" --force --out-file ./OldApp_AppStore_Share_Extension.mobileprovision + op read "op://Mobile-Deploy-CI/OldApp_AppStore_Notification_Service/OldApp_AppStore_Notification_Service.mobileprovision" --force --out-file ./OldApp_AppStore_Notification_Service.mobileprovision + op read "op://Mobile-Deploy-CI/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json + op read "op://Mobile-Deploy-CI/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - name: Set current App version in Env run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 39dfbe8e84a7..134ac0eff19f 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -25,7 +25,8 @@ jobs: repo: context.repo.repo, run_id: runId, }); - return jobsData.data; + const jobNamesToIgnore = ['confirmPassingBuild']; + return jobsData.data.filter(job => !jobNamesToIgnore.includes(job.name)); - name: Fetch Previous Workflow Run id: previous-workflow-run diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml index 2285eec56065..ca030e95de1d 100644 --- a/.github/workflows/finishReleaseCycle.yml +++ b/.github/workflows/finishReleaseCycle.yml @@ -22,7 +22,7 @@ jobs: uses: ./.github/actions/composite/setupGitForOSBotifyApp id: setupGitForOSBotify with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} @@ -87,7 +87,7 @@ jobs: id: setupGitForOSBotify uses: ./.github/actions/composite/setupGitForOSBotifyApp with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} @@ -128,7 +128,7 @@ jobs: - name: Setup git for OSBotify uses: ./.github/actions/composite/setupGitForOSBotifyApp with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index bfe860e60224..10ca10882464 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -104,7 +104,7 @@ jobs: - name: Setup Git for OSBotify uses: ./.github/actions/composite/setupGitForOSBotifyApp with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }} OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }} diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 869db3d04be7..80918d65462c 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -111,9 +111,6 @@ jobs: pattern: android-*-artifact merge-multiple: true - - name: Log downloaded artifact paths - run: ls -R /tmp/artifacts - - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -189,20 +186,17 @@ jobs: max_attempts: 5 command: scripts/pod-install.sh - - name: Decrypt AdHoc profile - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AdHoc.mobileprovision NewApp_AdHoc.mobileprovision.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Decrypt AdHoc Notification Service profile - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AdHoc_Notification_Service.mobileprovision NewApp_AdHoc_Notification_Service.mobileprovision.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 - - name: Decrypt certificate - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg + - name: Load files from 1Password env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op read "op://Mobile-Deploy-CI/NewApp_AdHoc/NewApp_AdHoc.mobileprovision" --force --out-file ./NewApp_AdHoc.mobileprovision + op read "op://Mobile-Deploy-CI/NewApp_AdHoc_Notification_Service/NewApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./NewApp_AdHoc_Notification_Service.mobileprovision + op read "op://Mobile-Deploy-CI/NewApp_AdHoc_Share_Extension.mobileprovision/NewApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./NewApp_AdHoc_Share_Extension.mobileprovision + op read "op://Mobile-Deploy-CI/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 @@ -248,10 +242,15 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode - - name: Decrypt Developer ID Certificate - run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg + - name: Load Desktop credentials from 1Password + id: load-credentials + uses: 1password/load-secrets-action@v2 + with: + export-env: false env: - DEVELOPER_ID_SECRET_PASSPHRASE: ${{ secrets.DEVELOPER_ID_SECRET_PASSPHRASE }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + DESKTOP_CERTIFICATE_BASE64: "op://Mobile-Deploy-CI/Desktop Certificates.p12/CSC_LINK" + DESKTOP_CERTIFICATE_PASSWORD: "op://Mobile-Deploy-CI/Desktop Certificates.p12/CSC_KEY_PASSWORD" - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 @@ -263,8 +262,8 @@ jobs: - name: Build desktop app for testing run: npm run desktop-build-adhoc env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + CSC_LINK: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_BASE64 }} + CSC_KEY_PASSWORD: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index 6a8a0d5884bf..9bd1b3b0f541 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -59,7 +59,7 @@ jobs: echo "REF=$(gh pr view ${{ github.event.inputs.PULL_REQUEST_NUMBER }} --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + getOldDotPR: runs-on: ubuntu-latest needs: validateActor @@ -106,7 +106,7 @@ jobs: fi env: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - + postGitHubCommentBuildStarted: runs-on: ubuntu-latest @@ -153,16 +153,16 @@ jobs: cd Mobile-Expensify git fetch origin ${{ needs.getOldDotBranchRef.outputs.OLD_DOT_REF }} git checkout ${{ needs.getOldDotBranchRef.outputs.OLD_DOT_REF }} - + - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - name: Setup Node id: setup-node uses: ./.github/actions/composite/setupNode - with: + with: IS_HYBRID_BUILD: 'true' - + - name: Run grunt build run: | cd Mobile-Expensify @@ -192,10 +192,11 @@ jobs: env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op document get --output ./upload-key.keystore upload-key.keystore - op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json + op read "op://Mobile-Deploy-CI/upload-key.keystore/upload-key.keystore" --force --out-file ./upload-key.keystore + op read "op://Mobile-Deploy-CI/android-fastlane-json-key.json/android-fastlane-json-key.json" --force --out-file ./android-fastlane-json-key.json + # Copy the keystore to the Android directory for Fullstory - cp ./upload-key.keystore Mobile-Expensify/Android + cp ./upload-key.keystore Mobile-Expensify/Android - name: Load Android upload keystore credentials from 1Password id: load-credentials @@ -215,28 +216,28 @@ jobs: ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} run: bundle exec fastlane android build_adhoc_hybrid - + - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 - + - name: Upload Android AdHoc build to S3 run: bundle exec fastlane android upload_s3 env: S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} S3_BUCKET: ad-hoc-expensify-cash - S3_REGION: us-east-1 + S3_REGION: us-east-1 - name: Export S3 path id: exportAndroidS3Path run: | # $s3APKPath is set from within the Fastfile, android upload_s3 lane echo "S3_APK_PATH=$s3APKPath" >> "$GITHUB_OUTPUT" - + iosHybrid: name: Build and deploy iOS for testing needs: [validateActor, getBranchRef, getOldDotBranchRef] @@ -271,9 +272,9 @@ jobs: - name: Setup Node id: setup-node uses: ./.github/actions/composite/setupNode - with: + with: IS_HYBRID_BUILD: 'true' - + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it run: | cp .env.staging .env.adhoc @@ -284,7 +285,7 @@ jobs: uses: ruby/setup-ruby@v1.204.0 with: bundler-cache: true - + - name: Install New Expensify Gems run: bundle install @@ -314,14 +315,10 @@ jobs: env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: | - op read op://Mobile-Deploy-CI/OldApp_AdHoc/OldApp_AdHoc.mobileprovision --force --out-file ./OldApp_AdHoc.mobileprovision - op read op://Mobile-Deploy-CI/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision - op read op://Mobile-Deploy-CI/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision - - - name: Decrypt certificate - run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg - env: - LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + op read "op://Mobile-Deploy-CI/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision + op read "op://Mobile-Deploy-CI/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision + op read "op://Mobile-Deploy-CI/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision + op read "op://Mobile-Deploy-CI/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12 - name: Build AdHoc app run: bundle exec fastlane ios build_adhoc_hybrid @@ -347,8 +344,6 @@ jobs: name: ios path: ./ios_paths.json - - postGithubComment: runs-on: ubuntu-latest name: Post a GitHub comment with app download links for testing diff --git a/Mobile-Expensify b/Mobile-Expensify index 7d33c85d7554..1feafc32db9c 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 7d33c85d75549f3bc95621538ce0ca47b06074a9 +Subproject commit 1feafc32db9c2596b02598228a56be57fd8d39d6 diff --git a/README.md b/README.md index de5c746a964d..1263b1e66b3a 100644 --- a/README.md +++ b/README.md @@ -532,6 +532,15 @@ The primary difference is that the native code, which runs React Native, is loca The `Mobile-Expensify` directory is a **Git submodule**. This means it points to a specific commit on the `Mobile-Expensify` repository. +If you'd like to fetch the submodule while executing the `git pull` command in `Expensify/App` instead of updating it manually you can run this command in the root of the project: + +``` +git config submodule.recurse true +``` + +> [!WARNING] +> Please, remember that the submodule will get updated automatically only after executing the `git pull` command - if you switch between branches it is still recommended to execute `git submodule update` to make sure you're working on a compatible submodule version! + If you'd like to download the most recent changes from the `main` branch, please use the following command: ```bash git submodule update --remote diff --git a/android/app/android-fastlane-json-key.json.gpg b/android/app/android-fastlane-json-key.json.gpg deleted file mode 100644 index 386ee2b45f44..000000000000 Binary files a/android/app/android-fastlane-json-key.json.gpg and /dev/null differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 98a560cbbed6..156718f4c3c2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009009401 - versionName "9.0.94-1" + versionCode 1009009419 + versionName "9.0.94-19" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/android/app/my-upload-key.keystore.gpg b/android/app/my-upload-key.keystore.gpg deleted file mode 100644 index e7ff57a171db..000000000000 Binary files a/android/app/my-upload-key.keystore.gpg and /dev/null differ diff --git a/android/app/src/main/res/font/expensify_mono.xml b/android/app/src/main/res/font/expensify_mono.xml index 346a34ba22b6..fe966e831e31 100644 --- a/android/app/src/main/res/font/expensify_mono.xml +++ b/android/app/src/main/res/font/expensify_mono.xml @@ -1,5 +1,7 @@ + + diff --git a/android/app/src/main/res/font/expensifymono_bolditalic.otf b/android/app/src/main/res/font/expensifymono_bolditalic.otf new file mode 100644 index 000000000000..4f3ac80d9119 Binary files /dev/null and b/android/app/src/main/res/font/expensifymono_bolditalic.otf differ diff --git a/android/app/src/main/res/font/expensifymono_italic.otf b/android/app/src/main/res/font/expensifymono_italic.otf new file mode 100644 index 000000000000..d2a7b8124b82 Binary files /dev/null and b/android/app/src/main/res/font/expensifymono_italic.otf differ diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json index 1c59d667fca1..9cf517a7fab4 100644 --- a/android/link-assets-manifest.json +++ b/android/link-assets-manifest.json @@ -5,6 +5,14 @@ "path": "assets/fonts/native/ExpensifyMono-Bold.otf", "sha1": "d70e12540200613e9e6ac9068bed57e4bf477bfe" }, + { + "path": "assets/fonts/native/ExpensifyMono-BoldItalic.otf", + "sha1": "ef9f92ba902942e232301c64bb55e79165435bcc" + }, + { + "path": "assets/fonts/native/ExpensifyMono-Italic.otf", + "sha1": "874301891972b3a2a6a8ece69b978e74ac9d10a2" + }, { "path": "assets/fonts/native/ExpensifyMono-Regular.otf", "sha1": "9bbd3795afea1b1136c5b6a8ecd7d470fd5ea1b2" diff --git a/assets/css/fonts.css b/assets/css/fonts.css index 7705be0b1140..7d24eb353189 100644 --- a/assets/css/fonts.css +++ b/assets/css/fonts.css @@ -40,6 +40,20 @@ src: url('/fonts/ExpensifyMono-Bold.woff2') format('woff2'), url('/fonts/ExpensifyMono-Bold.woff') format('woff'); } +@font-face { + font-family: Expensify Mono; + font-weight: 400; + font-style: italic; + src: url('/fonts/ExpensifyMono-Italic.woff2') format('woff2'), url('/fonts/ExpensifyMono-Italic.woff') format('woff'); +} + +@font-face { + font-family: Expensify Mono; + font-weight: 700; + font-style: italic; + src: url('/fonts/ExpensifyMono-BoldItalic.woff2') format('woff2'), url('/fonts/ExpensifyMono-BoldItalic.woff') format('woff'); +} + @font-face { font-family: Expensify New Kansas; font-weight: 500; diff --git a/assets/fonts/native/ExpensifyMono-BoldItalic.otf b/assets/fonts/native/ExpensifyMono-BoldItalic.otf new file mode 100644 index 000000000000..4f3ac80d9119 Binary files /dev/null and b/assets/fonts/native/ExpensifyMono-BoldItalic.otf differ diff --git a/assets/fonts/native/ExpensifyMono-Italic.otf b/assets/fonts/native/ExpensifyMono-Italic.otf new file mode 100644 index 000000000000..d2a7b8124b82 Binary files /dev/null and b/assets/fonts/native/ExpensifyMono-Italic.otf differ diff --git a/assets/fonts/web/ExpensifyMono-BoldItalic.woff b/assets/fonts/web/ExpensifyMono-BoldItalic.woff new file mode 100644 index 000000000000..898a2c908d02 Binary files /dev/null and b/assets/fonts/web/ExpensifyMono-BoldItalic.woff differ diff --git a/assets/fonts/web/ExpensifyMono-BoldItalic.woff2 b/assets/fonts/web/ExpensifyMono-BoldItalic.woff2 new file mode 100644 index 000000000000..27e67d312f3d Binary files /dev/null and b/assets/fonts/web/ExpensifyMono-BoldItalic.woff2 differ diff --git a/assets/fonts/web/ExpensifyMono-Italic.woff b/assets/fonts/web/ExpensifyMono-Italic.woff new file mode 100644 index 000000000000..8b823f33a4a6 Binary files /dev/null and b/assets/fonts/web/ExpensifyMono-Italic.woff differ diff --git a/assets/fonts/web/ExpensifyMono-Italic.woff2 b/assets/fonts/web/ExpensifyMono-Italic.woff2 new file mode 100644 index 000000000000..b7b7df6f5131 Binary files /dev/null and b/assets/fonts/web/ExpensifyMono-Italic.woff2 differ diff --git a/assets/images/customEmoji/global-create.svg b/assets/images/customEmoji/global-create.svg deleted file mode 100644 index 60b46eb97aed..000000000000 --- a/assets/images/customEmoji/global-create.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/assets/images/export.svg b/assets/images/export.svg new file mode 100644 index 000000000000..ed6ae9897368 --- /dev/null +++ b/assets/images/export.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/images/integrationicons/netsuite-quickstart-icon-square.svg b/assets/images/integrationicons/netsuite-quickstart-icon-square.svg new file mode 100644 index 000000000000..5b8ddb542cf7 --- /dev/null +++ b/assets/images/integrationicons/netsuite-quickstart-icon-square.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/desktop/developer_id.p12.gpg b/desktop/developer_id.p12.gpg deleted file mode 100644 index ad166e3f8334..000000000000 Binary files a/desktop/developer_id.p12.gpg and /dev/null differ diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index 02baa7f30570..089b06750053 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -59,7 +59,9 @@ This dictates when reimbursable expenses will export, according to your preferre **Journal Entries:** Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your workspace's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite. -- 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 +- When [automatic reconciliation](https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation) is enabled, Expensify Card expenses will always export as individual, itemized Journal Entries, regardless of Expense Reports or Vendor Bills settings configured for non-reimbursable expenses on the Export tab. +- Without automatic reconciliation, Expensify Card expenses will export using the export type configured for non-reimbursable expenses on the Export tab. +- Expensify Card expenses exported as Journal Entries will always export as individual, itemized Journal Entries, regardless of whether the "one journal entry for all items on report" setting is enabled. - 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 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 38686462a1c2..ee03a18033ea 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 @@ -54,8 +54,6 @@ To add your Expensify Card to a digital wallet, follow the steps below: - **Restricted Country**: Transactions from restricted countries will be declined. {% include faq-begin.md %} -## Can I use Smart Limits with a free Expensify account? -If you're on the Free plan, you won't have the option to use Smart Limits. Your card limit will simply reset at the end of each calendar month. ## I still haven't received my Expensify Card. What should I do? For more information on why your card hasn't arrived, you can check out this resource on [Requesting a Card](https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Card#what-if-i-havent-received-my-card-after-multiple-weeks). diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md b/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md index ee181706d70d..59314be96584 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md @@ -39,7 +39,7 @@ Yes! Customers can pay in AUD, GBP, or NZD in addition to USD. - **Control Plan:** A$30, £14, or NZ$32 per user/month (Annual subscription + Expensify Cards) ## Is Expensify free for individuals? -Yes! Individuals can use Expensify for free to track expenses. +Yes! Individuals can use Expensify for free to track expenses. The steps in this [help article](https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself) will walk you through creating a personal workspace to track your expenses. ## How do I get more info about pricing? For customized information or help choosing the right plan, reach out to Expensify Concierge or email **concierge@expensify.com**. diff --git a/docs/articles/new-expensify/connect-credit-cards/Personal-Cards.md b/docs/articles/new-expensify/connect-credit-cards/Personal-Cards.md new file mode 100644 index 000000000000..70fc5cdffb52 --- /dev/null +++ b/docs/articles/new-expensify/connect-credit-cards/Personal-Cards.md @@ -0,0 +1,12 @@ +--- +title: Personal Cards +description: Learn how to track and manage your personal credit card expenses in Expensify through automatic imports or manual uploads. +--- + +# Overview + +Expensify makes it easy to track expenses and get reimbursed by linking your personal credit card. Once connected, transactions can be imported automatically, or you can upload a CSV file for manual entry. These transactions will be merged with SmartScanned receipts and, if enabled, can generate IRS-compliant eReceipts. + +--- + +We are currently developing the personal card connection feature for New Expensify. Once available, we will update this article with step-by-step instructions on how to connect your card. Stay tuned! diff --git a/docs/articles/new-expensify/connect-credit-cards/company-cards/Commercial-feeds.md b/docs/articles/new-expensify/connect-credit-cards/company-cards/Commercial-feeds.md index 2dbe47d3b178..bd94e2ccff54 100644 --- a/docs/articles/new-expensify/connect-credit-cards/company-cards/Commercial-feeds.md +++ b/docs/articles/new-expensify/connect-credit-cards/company-cards/Commercial-feeds.md @@ -2,115 +2,111 @@ title: Commercial-feeds.md description: Commercial feeds --- + # Overview Commercial feeds are the most reliable way to import company card expenses. They remain unaffected by changes to bank login credentials or UI updates, making them highly recommended for those eligible. + The easiest way to confirm your eligibility for a commercial feed is to ask your bank directly. -# Prerequisites for enabling a commercial feed -If you haven't already, you need to create a workspace before setting up a commercial feed. Go to Settings > Workspaces > New workspace to create one. -Additionally, you’ll need to enable company cards on your workspace by navigating to Settings > Workspaces > [your workspace] > More features, and toggling on Company cards. Note that upgrading to the Control plan is required to access this feature. -# How to set up a Mastercard commercial feed + +# Prerequisites for Enabling a Commercial Feed +If you haven't already, you need to create a workspace before setting up a commercial feed. Go to **Settings > Workspaces > New workspace** to create one. + +Additionally, you’ll need to enable company cards on your workspace by navigating to **Settings > Workspaces > [your workspace] > More features**, and toggling on **Company cards**. Note that upgrading to the Control plan is required to access this feature. + +# How to Set Up a Mastercard Commercial Feed Your bank must access Mastercard's SmartData portal to complete the process. Expensify is a registered vendor in the portal, so no additional Mastercard forms are required. Your bank may, however, have its own forms. -## Steps to add a Mastercard commercial feed: -Contact your banking relationship manager and request that your CDF (Common Data File) feed be sent directly to Expensify in the Mastercard SmartData Portal (file type: CDF version 3 Release 11.01). Specify the earliest transaction date you need in the feed. -The bank will initiate feed delivery by selecting Expensify in Mastercard's portal and will email you the distribution ID. -While waiting for your bank, ensure your Control plan workspace in Expensify is set up. -Submit the distribution ID in Expensify by navigating to Settings > Workspaces > [your workspace] > Company cards > Add cards, selecting your bank (choose "Other" if not listed), and then selecting Mastercard Commercial Cards. -Once submitted, Expensify will connect the feed and notify you when it’s enabled. -# How to set up a Visa commercial feed -## Steps to add a Visa commercial feed: -Contact your banking relationship manager and request that your VCF (Variant Call Format) feed be sent directly to Expensify. Share this with your bank: "There’s a checkbox in your Visa Subscription Management portal that can be selected to enable the feed, eliminating the need for a test file." -Request the feed filename or raw file information, including the Processor ID, Financial Institution (bank) ID, and Company ID. -While waiting for your bank, ensure your Control plan workspace in Expensify is set up. -Submit the required IDs in Expensify by navigating to Settings > Workspaces > [your workspace] > Company cards > Add cards, selecting your bank (choose "Other" if not listed), and then selecting Visa Commercial Cards. -Once submitted, Expensify will connect the feed and notify you when it’s enabled. - -# How to set up an American Express corporate feed + +## Steps to Add a Mastercard Commercial Feed: +1. Contact your banking relationship manager and request that your CDF (Common Data File) feed be sent directly to Expensify in the Mastercard SmartData Portal (file type: CDF version 3 Release 11.01). Specify the earliest transaction date you need in the feed. +2. The bank will initiate feed delivery by selecting Expensify in Mastercard's portal and will email you the distribution ID. +3. While waiting for your bank, ensure your Control workspace in Expensify is set up. +4. Submit the distribution ID in Expensify by navigating to **Settings > Workspaces > [your workspace] > Company cards > Add cards**, selecting your bank (choose "Other" if not listed), and then selecting **Mastercard Commercial Cards**. +5. Once submitted, Expensify will connect the feed and notify you when it’s enabled. + +# How to Set Up a Visa Commercial Feed +## Steps to Add a Visa Commercial Feed: +1. Contact your banking relationship manager and request that your VCF (Variant Call Format) feed be sent directly to Expensify. Share this with your bank: "There’s a checkbox in your Visa Subscription Management portal that can be selected to enable the feed, eliminating the need for a test file." +2. Request the feed filename or raw file information, including the Processor ID, Financial Institution (bank) ID, and Company ID. +3. While waiting for your bank, ensure your Control workspace in Expensify is set up. +4. Submit the required IDs in Expensify by navigating to **Settings > Workspaces > [your workspace] > Company cards > Add cards**, selecting your bank (choose "Other" if not listed), and then selecting **Visa Commercial Cards**. +5. Once submitted, Expensify will connect the feed and notify you when it’s enabled. + +# How to Set Up an American Express Corporate Feed To begin, fill out Amex's required forms and send them to Amex for processing. Download the forms [here](https://drive.google.com/file/d/1zqDA_MCk06jk_fWjzx2y0r4gOyAMqKJe/view?usp=sharing). -## Instructions for filling out the Amex forms: -PAGE 1 -Corporation Name: The legal name of your company on file with American Express -Corporation Address: The legal address of your company -Requested Feed Start Date: The earliest transaction date you want in Expensify (use international date format: DD/MM/YY or spelled out, e.g., January 1, 1900). -Requestor Contact: Name of the person completing the request -Email Address: Email of the person completing the request -Control Account Number: The master or basic control account number for the cards you’d like to add (not a credit card number). Contact Amex if you need assistance identifying the correct number. -PAGE 2 +## Instructions for Filling Out the Amex Forms: +**PAGE 1** +- **Corporation Name:** The legal name of your company on file with American Express +- **Corporation Address:** The legal address of your company +- **Requested Feed Start Date:** The earliest transaction date you want in Expensify (use international date format: DD/MM/YY or spelled out, e.g., January 1, 1900). +- **Requestor Contact:** Name of the person completing the request +- **Email Address:** Email of the person completing the request +- **Control Account Number:** The master or basic control account number for the cards you’d like to add (not a credit card number). Contact Amex if you need assistance identifying the correct number. + +**PAGE 2** No information required -PAGE 3 -Client Registered Name: The legal name of your company on file with American Express -Master Control Account or Basic Control Account: Same as the control account number on page 1 -PAGE 4 -Country List: The country where the account originates -Client Authorization: Complete your full name, job title, and date (use international date format i.e., DD/MM/YY). Sign where indicated. +**PAGE 3** +- **Client Registered Name:** The legal name of your company on file with American Express +- **Master Control Account or Basic Control Account:** Same as the control account number on page 1 + +**PAGE 4** +- **Country List:** The country where the account originates +- **Client Authorization:** Complete your full name, job title, and date (use international date format i.e., DD/MM/YY). Sign where indicated. -## Steps to add an American Express corporate feed: -Send the completed forms to electronictransmissionsteam@aexp.com and request they send your corporate card feed to Expensify. You should receive a confirmation email within a few days. -While waiting, ensure your Control plan workspace in Expensify is set up. -Amex will send a Production Letter with delivery file name information (e.g., R123456_B123456789_GL1025_001_$DATE$$TIME$_$SEQ$). -Submit the delivery file name in Expensify by navigating to Settings > Workspaces > [your workspace] > Company cards > Add cards > American Express > American Express Corporate Cards. -Once submitted, Expensify will connect the feed and notify you when it’s enabled. +## Steps to Add an American Express Corporate Feed: +1. Send the completed forms to **electronictransmissionsteam@aexp.com** and request they send your corporate card feed to Expensify. You should receive a confirmation email within a few days. +2. While waiting, ensure your Control workspace in Expensify is set up. +3. Amex will send a Production Letter with delivery file name information (e.g., `R123456_B123456789_GL1025_001_$DATE$$TIME$_$SEQ$`). +4. Submit the delivery file name in Expensify by navigating to **Settings > Workspaces > [your workspace] > Company cards > Add cards > American Express > American Express Corporate Cards**. +5. Once submitted, Expensify will connect the feed and notify you when it’s enabled. -# How to assign company cards -Once your feed is connected, you can assign cards to employees. To do this, navigate to Settings > Workspaces > [your workspace] > Company cards. +# How to Assign Company Cards +Once your feed is connected, you can assign cards to employees. To do this, navigate to **Settings > Workspaces > [your workspace] > Company cards**. -![Click the feed name to view the feed selector]({{site.url}}/assets/images/commfeed/commfeed-01.png){:width="100%"} +![Click the feed name to view the feed selector]({{site.url}}/assets/images/commfeed/commfeed-01-updated.png){:width="100%"} If you have multiple feeds, click the feed name at the top left to select the appropriate one. -![Select a feed from the feed selector to view it]({{site.url}}/assets/images/commfeed/commfeed-02.png){:width="100%"} +![Select a feed from the feed selector to view it]({{site.url}}/assets/images/commfeed/commfeed-02-updated.png){:width="100%"} -Click Assign card to select an employee. All workspace members appear in the list. +Click **Assign card** to select an employee. All workspace members appear in the list. -![Click assign card and select an employee from the list]({{site.url}}/assets/images/commfeed-03.png){:width="100%"} +![Click assign card and select an employee from the list]({{site.url}}/assets/images/commfeed/commfeed-03-updated.png){:width="100%"} Select the card you want to assign. Cards only appear if they have recent transactions. -![Select a card from the list]({{site.url}}/assets/images/commfeed/commfeed-04.png){:width="100%"} +![Select a card from the list]({{site.url}}/assets/images/commfeed/commfeed-04-updated.png){:width="100%"} Choose a start date: -From the beginning: Imports all available transactions (typically 30-90 days). -Custom start date: Allows you to specify a date. -![Select your transaction start date]({{site.url}}/assets/images/commfeed/commfeed-05.png){:width="100%"} -Review the details and click Assign card. Transactions will import immediately. -![Double check the selections and assign the card]({{site.url}}/assets/images/commfeed/commfeed-06.png){:width="100%"} - -# Managing cards -Clicking an assigned card opens the Card details page, where you can: -Change the card name. -Select a card-specific export account (if connected to accounting software like QuickBooks, NetSuite, Xero, etc.). -Update the card to pull recent transactions. -Unassign the card (note: unassigning deletes unsubmitted expenses on draft reports in the cardholder’s account). -![Manage the card on the card details page]({{site.url}}/assets/images/commfeed/commfeed-07.png){:width="100%"} - -{% include faq-begin.md %} +- **From the beginning:** Imports all available transactions (typically 30-90 days). +- **Custom start date:** Allows you to specify a date. + +![Select your transaction start date]({{site.url}}/assets/images/commfeed/commfeed-05-updated.png){:width="100%"} -## My commercial feed is connected. Why is a specific card not appearing for assignment? -Cards appear for assignment if they’re active and have at least one recent transaction. If a card meeting these criteria doesn’t appear, contact your account manager or message concierge@expensify.com. +Review the details and click **Assign card**. Transactions will import immediately. -## Is there an extra fee for using commercial feeds? -No, commercial feed setup is included in the Control plan. +![Double check the selections and assign the card]({{site.url}}/assets/images/commfeed/commfeed-06-updated.png){:width="100%"} -## What’s the difference between a direct feed and commercial feed? -Direct feeds use login credentials for quick setup, but can require re-authenticating from time to time. Commercial feeds require bank involvement for setup but offer the most reliable connection. +# Managing Cards +Once a card is assigned, you can manage its settings by navigating to **Settings > Workspaces > [your workspace] > Company cards** and selecting the assigned card. -## I have a Small Business Amex account. Am I eligible to set up a commercial feed? -Small Business or Triumph Amex accounts may not be eligible for a commercial feed and might need to use an Amex direct feed. +## Available Card Management Actions: +- **Rename the Card**: Change the card name for easier identification. +- **Set a Specific Export Account**: If connected to accounting software like QuickBooks, NetSuite, or Xero, you can assign a unique export account for this card. +- **Update Transactions**: Manually refresh the card feed to pull in the latest transactions. +- **Unassign the Card**: Removing a card unassigns it from the employee and deletes unsubmitted expenses from draft reports in their account. -## Are commercial feeds the best option if my bank isn’t one where Expensify supports direct feeds? -Yes. If direct feeds are not available for your bank, commercial feeds are the best option for importing company card transactions. Currently, Expensify supports direct feeds for: -American Express -Bank of America -Brex -Capital One -Chase -Citibank -Stripe -Wells Fargo +![Manage the card on the card details page]({{site.url}}/assets/images/commfeed/commfeed-07-updated.png){:width="100%"} +# FAQ -{% include faq-end.md %} +## My commercial feed is connected. Why is a specific card not appearing for assignment? +Cards appear for assignment if they’re active and have at least one recent transaction. If a card meeting these criteria doesn’t appear, contact your account manager or message concierge@expensify.com. +## Is there an extra fee for using commercial feeds? +No, commercial feed setup is included in the Control plan. +## What’s the difference between a direct feed and commercial feed? +Direct feeds use login credentials for quick setup, but can require re-authenticating from time to time. Commercial feeds require bank involvement for setup but offer the most reliable connection. diff --git a/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md b/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md new file mode 100644 index 000000000000..99a67a577500 --- /dev/null +++ b/docs/articles/new-expensify/connections/netsuite/Connect-To-NetSuite.md @@ -0,0 +1,162 @@ +--- +title: Connect To NetSuite +description: Connect NetSuite to New Expensify for streamlined expense reporting and accounting integration. +order: 1 +--- + +{% include info.html %} +To use the NetSuite connection, you must have a NetSuite account and an Expensify Control plan. +{% include end-info.html %} + +Expensify’s integration with NetSuite supports syncing data between the two systems. Before you start connecting Expensify with NetSuite, there are a few things to note: + +- You must be able to login to NetSuite as an administrator 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 to who you would like to add the Expensify Integration role. 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 a NetSuite admin. +2. Click _Edit > Access_, then find the Expensify Integration role in the dropdown and add it to the user. +3. Click **Save**. + + +{% include info.html %} +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. +{% include end-info.html %} + + +# 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 + +{% include info.html %} +Expense Reports must be enabled in order to use Expensify’s integration with NetSuite. +{% include end-info.html %} + + +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 + +{% include info.html %} +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. +{% include end-info.html %} + +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) + +{% include info.html %} +**Things to note about tax.** +Expensify imports NetSuite Tax Groups (not Tax Codes). To ensure Tax Groups can be applied to expenses go to _Setup > Accounting > Set Up Taxes_ and set 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. +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. +{% include end-info.html %} + +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. + +# Step 12: Connect Expensify to NetSuite + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to connect to NetSuite. +4. Click **More features** in the left menu. +5. Scroll down to the Integrate section and enable the **Accounting** toggle. +6. Click **Accounting** in the left menu. +7. Click **Connect** next to NetSuite. +8. Click **Next** until you reach setup step 5 (If you followed the instructions above, then the first four setup steps will already be complete.) +9. 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_.) +10. Click **Confirm** to complete the setup. + + +![The New Expensify workspace setting is open and the More Features tab is selected and visible. The toggle to enable Accounting is highlighted with an orange call out and is currently in the grey disabled position.]({{site.url}}/assets/images/ExpensifyHelp-Xero-1.png) + +![The New Expensify workspace settings > More features tab is open with the toggle to enable Accounting enabled and green. The Accounting tab is now visible in the left-hand menu and is highlighted with an orange call out.]({{site.url}}/assets/images/ExpensifyHelp-Xero-2.png){:width="100%"} + +After completing the setup, the NetSuite connection will sync. It can take 1-2 minutes to sync with NetSuite. + +Once connected, all newly approved and paid reports exported from Expensify will be generated in NetSuite using SOAP Web Services (the term NetSuite employs when records are created through the integration). You can then move forward with [configuring the NetSuite settings](https://help.expensify.com/articles/new-expensify/connections/netsuite/Configure-Netsuite) in Expensify. + +{% include faq-begin.md %} + +## If I have a lot of customer and vendor data in NetSuite, how can I help ensure that importing them all is seamless? + +For importing your customers and vendors, make sure your page size is set to 1000 in NetSuite. + +Go to **Setup > Integration > Web Services Preferences** and search **Page Size** to determine your page size. + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md b/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md deleted file mode 100644 index 990217523743..000000000000 --- a/docs/articles/new-expensify/connections/netsuite/Connect-to-NetSuite.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Connect to NetSuite -description: Integrate NetSuite with Expensify -order: 1 ---- - -{% include info.html %} -To use the NetSuite connection, you must have a NetSuite account and an Expensify Control plan. -{% include end-info.html %} - -Expensify’s integration with NetSuite supports syncing 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 - -While logged into NetSuite as an administrator, go to **Customization > SuiteBundler > Search & Install Bundles**, then search for “Expensify”. -Click on the Expensify Connect bundle (Bundle ID 283395). -Click **Install**. -If you already have the Expensify Connect bundle installed, head to **Customization > SuiteBundler > Search & Install Bundles > List**, and update it to the latest version. -Select "Show on Existing Custom Forms" for all available fields. - -# Step 2: Enable Token-Based Authentication - -In NetSuite, go to **Setup > Company > Enable Features > SuiteCloud > Manage Authentication**. -Make sure “Token Based Authentication” is enabled. -Click **Save**. - -# Step 3: Add Expensify Integration Role to a User - -In NetSuite, head to **Lists > Employees**, and find the user to who you would like to add the Expensify Integration role. 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. -Click **Edit > Access**, then find the Expensify Integration role in the dropdown and add it to the user. -Click **Save**. - - -{% include info.html %} -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. -{% include end-info.html %} - -# Step 4: Create Access Tokens - - -In NetSuite, enter “page: tokens” in the Global Search. -Click **New Access Token**. -Select Expensify as the application (this must be the original Expensify integration from the bundle). -Select the role Expensify Integration. -Click **Save**. -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 - -{% include info.html %} -Expense Reports must be enabled in order to use Expensify’s integration with NetSuite. -{% include end-info.html %} - - -In NetSuite, go to **Setup > Company > Enable Features > Employees**. -Confirm the checkbox next to "Expense Reports" is checked. -If not, click the checkbox and then click **Save** to enable Expense Reports. - - -# Step 6: Confirm Expense Categories are set up in NetSuite - -{% include info.html %} -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. -{% include end-info.html %} - - -In NetSuite, go to **Setup > Accounting > Expense Categories** (a list of Expense Categories should show.) -If no Expense Categories are visible, click **New** to create new ones. - -# Step 7: Confirm Journal Entry Transaction Forms are Configured Properly - -In NetSuite, go to **Customization > Forms > Transaction Forms.** -Click **Customize** or **Edit** next to the Standard Journal Entry form. -Click **Screen Fields > Main**. Please verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal." -Click the sub-header **Lines** and verify that the “Show” column for “Receipt URL” is checked. -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 - - -In NetSuite, go to **Customization > Forms > Transaction Forms.** -Click **Customize** or **Edit** next to the Standard Expense Report form, then click **Screen Fields > Main.** -Verify the “Created From” label has “Show” checked and the "Display Type" is set to "Normal." -Click the second sub-header, **Expenses**, and verify that the "Show" column for "Receipt URL" is checked. -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 - - -In NetSuite, go to **Customization > Forms > Transaction Forms.** -Click **Customize** or **Edit** next to your preferred Vendor Bill form. -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. -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. -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 - - -In NetSuite, go to **Customization > Forms > Transaction Forms**. -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. -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. -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) - -{% include info.html %} -**Things to note about tax.** -Expensify imports NetSuite Tax Groups (not Tax Codes). To ensure Tax Groups can be applied to expenses go to **Setup > Accounting > Set Up Taxes** and set 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. -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. -{% include end-info.html %} - -Go to **Setup > Accounting > Tax Groups**. -Click **New**. -Select the country for your Tax Group. -Enter the Tax Name (this is what employees will see in Expensify.) -Select the subsidiary for this Tax Group. -Select the Tax Code from the table you wish to include in this Tax Group. -Click **Add**. -Click **Save**. -Create one NetSuite Tax Group for each tax rate you want to show in Expensify. - -# Step 12: Connect Expensify to NetSuite - -Click your profile image or icon in the bottom left menu. -Scroll down and click **Workspaces** in the left menu. -Select the workspace you want to connect to NetSuite. -Click **More features** in the left menu. -Click **More features** in the left menu. -Scroll down to the Integrate section and enable the Accounting toggle. -Click **Accounting** in the left menu. -Click **Connect** next to NetSuite. -Click **Next** until you reach setup step 5 (If you followed the instructions above, then the first four setup steps will already be complete.) -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**.) -Click **Confirm** to complete the setup. - - -![The New Expensify workspace setting is open and the More Features tab is selected and visible. The toggle to enable Accounting is highlighted with an orange call out and is currently in the grey disabled position.]({{site.url}}/assets/images/ExpensifyHelp-Xero-1.png) - -![The New Expensify workspace settings > More features tab is open with the toggle to enable Accounting enabled and green. The Accounting tab is now visible in the left-hand menu and is highlighted with an orange call out.]({{site.url}}/assets/images/ExpensifyHelp-Xero-2.png){:width="100%"} - -After completing the setup, the NetSuite connection will sync. It can take 1-2 minutes to sync with NetSuite. - -Once connected, all newly approved and paid 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 %} - -## If I have a lot of customer and vendor data in NetSuite, how can I help ensure that importing them all is seamless? - -For importing your customers and vendors, make sure your page size is set to 1000 in NetSuite. - -Go to **Setup > Integration > Web Services Preferences** and search **Page Size** to determine your page size. - -{% include faq-end.md %} diff --git a/docs/redirects.csv b/docs/redirects.csv index 2ddd13d5fc8b..f4970373fb7a 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -620,6 +620,7 @@ https://help.expensify.com/articles/expensify-classic/connect-credit-cards/compa https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards/ https://help.expensify.com/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa,https://help.expensify.com/new-expensify/hubs/expensify-card/ https://help.expensify.com/articles/new-expensify/expensify-card/Dispute-Expensify-Card-transaction,https://help.expensify.com/articles/new-expensify/expensify-card/Disputing-Expensify-Card-Transactions +https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-To-NetSuite https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Card,https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Expensify-Card https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address,https://help.expensify.com/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify https://help.expensify.com/articles/expensify-classic/domains/SAML-SSO,https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify diff --git a/fastlane/Fastfile b/fastlane/Fastfile index be90ce55ffaa..b9930ca92324 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -332,7 +332,7 @@ def setupIOSSigningCertificate() ) import_certificate( - certificate_path: "./ios/Certificates.p12", + certificate_path: "./Certificates.p12", keychain_name: "ios-build.keychain", keychain_password: keychain_password ) @@ -346,11 +346,11 @@ platform :ios do setupIOSSigningCertificate() install_provisioning_profile( - path: "./ios/NewApp_AppStore.mobileprovision" + path: "./NewApp_AppStore.mobileprovision" ) install_provisioning_profile( - path: "./ios/NewApp_AppStore_Notification_Service.mobileprovision" + path: "./NewApp_AppStore_Notification_Service.mobileprovision" ) build_app( @@ -478,11 +478,11 @@ platform :ios do setupIOSSigningCertificate() install_provisioning_profile( - path: "./ios/NewApp_AdHoc.mobileprovision" + path: "./NewApp_AdHoc.mobileprovision" ) install_provisioning_profile( - path: "./ios/NewApp_AdHoc_Notification_Service.mobileprovision" + path: "./NewApp_AdHoc_Notification_Service.mobileprovision" ) build_app( @@ -520,7 +520,7 @@ platform :ios do lane :upload_testflight do upload_to_testflight( app_identifier: "com.chat.expensify.chat", - api_key_path: "./ios/ios-fastlane-json-key.json", + api_key_path: "./ios-fastlane-json-key.json", distribute_external: true, notify_external_testers: true, changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.", @@ -554,7 +554,7 @@ platform :ios do lane :upload_testflight_hybrid do upload_to_testflight( app_identifier: "com.expensify.expensifylite", - api_key_path: "./ios/ios-fastlane-json-key.json", + api_key_path: "./ios-fastlane-json-key.json", distribute_external: true, notify_external_testers: true, reject_build_waiting_for_review: true, @@ -590,7 +590,7 @@ platform :ios do lane :submit_for_review do deliver( app_identifier: "com.chat.expensify.chat", - api_key_path: "./ios/ios-fastlane-json-key.json", + api_key_path: "./ios-fastlane-json-key.json", # Skip HTMl report verification force: true, @@ -674,7 +674,7 @@ platform :ios do lane :submit_hybrid_for_rollout do deliver( app_identifier: "com.expensify.expensifylite", - api_key_path: "./ios/ios-fastlane-json-key.json", + api_key_path: "./ios-fastlane-json-key.json", # Skip HTML report verification force: true, diff --git a/ios/Certificates.p12.gpg b/ios/Certificates.p12.gpg deleted file mode 100644 index 91f827416367..000000000000 Binary files a/ios/Certificates.p12.gpg and /dev/null differ diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg deleted file mode 100644 index 567a867981e6..000000000000 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and /dev/null differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg deleted file mode 100644 index 6437d0a3f096..000000000000 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and /dev/null differ diff --git a/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg b/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg deleted file mode 100644 index c9b3eb213f79..000000000000 Binary files a/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg and /dev/null differ diff --git a/ios/NewApp_AppStore.mobileprovision.gpg b/ios/NewApp_AppStore.mobileprovision.gpg deleted file mode 100644 index 22624c6f41d6..000000000000 Binary files a/ios/NewApp_AppStore.mobileprovision.gpg and /dev/null differ diff --git a/ios/NewApp_AppStore_Notification_Service.mobileprovision.gpg b/ios/NewApp_AppStore_Notification_Service.mobileprovision.gpg deleted file mode 100644 index 503a096f1726..000000000000 Binary files a/ios/NewApp_AppStore_Notification_Service.mobileprovision.gpg and /dev/null differ diff --git a/ios/NewApp_Development.mobileprovision.gpg b/ios/NewApp_Development.mobileprovision.gpg deleted file mode 100644 index 34f034752b7f..000000000000 Binary files a/ios/NewApp_Development.mobileprovision.gpg and /dev/null differ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index c8d825589bfb..14866da313d5 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 0DFC45952C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; }; 0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; }; 0F5E5351263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; }; + 0F749C2B3B8F4562B816DEAB /* ExpensifyMono-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = ED64768FC6254E4D8FCD12BC /* ExpensifyMono-Italic.otf */; }; 1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 18D050E0262400AF000D658B /* BridgingFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D050DF262400AF000D658B /* BridgingFile.swift */; }; @@ -28,6 +29,7 @@ 30581EA8AAFD4FCE88C5D191 /* ExpensifyNeue-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */; }; 374FB8D728A133FE000D84EF /* OriginImageRequestHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 374FB8D628A133FE000D84EF /* OriginImageRequestHandler.mm */; }; 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 383643672B6D4AE2005BB9AE /* DeviceCheck.framework */; }; + 524F95D57E75496EBD14B0AA /* ExpensifyMono-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = A96F65C6624044318D21DAB1 /* ExpensifyMono-BoldItalic.otf */; }; 7041848526A8E47D00E09F4D /* RCTStartupTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */; }; 7041848626A8E47D00E09F4D /* RCTStartupTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7041848426A8E47D00E09F4D /* RCTStartupTimer.m */; }; 70CF6E82262E297300711ADC /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 70CF6E81262E297300711ADC /* BootSplash.storyboard */; }; @@ -142,6 +144,7 @@ 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; 8EFE0319D586C1078DB926FD /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + A96F65C6624044318D21DAB1 /* ExpensifyMono-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyMono-BoldItalic.otf"; sourceTree = ""; }; AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = BackgroundTasks.framework; path = System/Library/Frameworks/BackgroundTasks.framework; sourceTree = SDKROOT; }; BBE493797E97F2995E627244 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify-NewExpensifyTests/ExpoModulesProvider.swift"; sourceTree = ""; }; @@ -159,6 +162,7 @@ E9DF872C2525201700607FDC /* AirshipConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AirshipConfig.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; + ED64768FC6254E4D8FCD12BC /* ExpensifyMono-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Italic.otf"; path = "../assets/fonts/native/ExpensifyMono-Italic.otf"; sourceTree = ""; }; EF33B19FC6A7FE676839430D /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; EFA5CA89CC675CA3370CF89E /* Pods-NewExpensify.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugproduction.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugproduction.xcconfig"; sourceTree = ""; }; F0C450E92705020500FD2970 /* colors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = colors.json; path = ../colors.json; sourceTree = ""; }; @@ -345,6 +349,8 @@ 44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */, D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */, DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */, + A96F65C6624044318D21DAB1 /* ExpensifyMono-BoldItalic.otf */, + ED64768FC6254E4D8FCD12BC /* ExpensifyMono-Italic.otf */, E704648954784DDFBAADF568 /* ExpensifyMono-Regular.otf */, 52796131E6554494B2DDB056 /* ExpensifyNeue-Bold.otf */, 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */, @@ -532,6 +538,8 @@ 30581EA8AAFD4FCE88C5D191 /* ExpensifyNeue-Italic.otf in Resources */, 1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */, D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */, + 524F95D57E75496EBD14B0AA /* ExpensifyMono-BoldItalic.otf in Resources */, + 0F749C2B3B8F4562B816DEAB /* ExpensifyMono-Italic.otf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 25deb5ad9f83..2043ee4a2cb6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.94.1 + 9.0.94.19 FullStory OrgId @@ -92,6 +92,8 @@ ExpensifyNewKansas-Medium.otf ExpensifyNewKansas-MediumItalic.otf ExpensifyMono-Bold.otf + ExpensifyMono-BoldItalic.otf + ExpensifyMono-Italic.otf ExpensifyMono-Regular.otf ExpensifyNeue-Bold.otf ExpensifyNeue-BoldItalic.otf diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index acfce5c2e675..6c71db15b8f1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.94.1 + 9.0.94.19 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2e661cccef7f..73074d2a200e 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.94 CFBundleVersion - 9.0.94.1 + 9.0.94.19 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 395e6b1c618a..a7347a4c5097 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2514,7 +2514,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.223): + - RNLiveMarkdown (0.1.230): - DoubleConversion - glog - hermes-engine @@ -2534,10 +2534,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.223) + - RNLiveMarkdown/newarch (= 0.1.230) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.223): + - RNLiveMarkdown/newarch (0.1.230): - DoubleConversion - glog - hermes-engine @@ -3412,7 +3412,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 364e6862a112045bb5c5d35601f0bdb0304af979 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 5c76c659b125006ff525a095b65184ecb72392f3 + RNLiveMarkdown: 3887f27df3e002a4d008488df0ef62d7bcc526eb RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: d184c8d3213acf4c97ec71fbbb6f9d4954552d80 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/ios/ios-fastlane-json-key.json.gpg b/ios/ios-fastlane-json-key.json.gpg deleted file mode 100644 index 06d2109da080..000000000000 --- a/ios/ios-fastlane-json-key.json.gpg +++ /dev/null @@ -1,2 +0,0 @@ -  H46 )E$R `LIu0؄<;\ՠFIι{[3EįL?ʼ-V6vW6}뾆ck)>O##E:AAx|FQe"6Q Є7YQ+pc8~ǓbDYRA!\T `-yGy>IGU ejYC< H}?9J`2 T -,a>"J(,}v;F>i9ѠaQrN;mMM_D3L͖sVDa1nh9ɍX9 \ No newline at end of file diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json index 1c59d667fca1..9cf517a7fab4 100644 --- a/ios/link-assets-manifest.json +++ b/ios/link-assets-manifest.json @@ -5,6 +5,14 @@ "path": "assets/fonts/native/ExpensifyMono-Bold.otf", "sha1": "d70e12540200613e9e6ac9068bed57e4bf477bfe" }, + { + "path": "assets/fonts/native/ExpensifyMono-BoldItalic.otf", + "sha1": "ef9f92ba902942e232301c64bb55e79165435bcc" + }, + { + "path": "assets/fonts/native/ExpensifyMono-Italic.otf", + "sha1": "874301891972b3a2a6a8ece69b978e74ac9d10a2" + }, { "path": "assets/fonts/native/ExpensifyMono-Regular.otf", "sha1": "9bbd3795afea1b1136c5b6a8ecd7d470fd5ea1b2" diff --git a/package-lock.json b/package-lock.json index ef23310e7f26..30002a164a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "new.expensify", - "version": "9.0.94-1", + "version": "9.0.94-19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.94-1", + "version": "9.0.94-19", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-background-task": "file:./modules/background-task", - "@expensify/react-native-live-markdown": "0.1.223", + "@expensify/react-native-live-markdown": "0.1.230", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -99,7 +99,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.89", + "react-native-onyx": "2.0.92", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -3642,9 +3642,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.223", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.223.tgz", - "integrity": "sha512-rE5cQ9lBDP2tqtR4Tta3PNx2i5K83sdht1meYMvmLPqFVy7C9A743wzZe6oudVnhSDem8MbU4NMJStadp9xn6Q==", + "version": "0.1.230", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.230.tgz", + "integrity": "sha512-1b+sVJRvHPpnSFhGtS4IHD03vzMEz79NUyokVd+coWNnLOgb4/3ZlC9U58Dymc9+/eqq191lhALSDJMTm2DWow==", "license": "MIT", "workspaces": [ "./example", @@ -3654,7 +3654,7 @@ "node": ">= 18.0.0" }, "peerDependencies": { - "expensify-common": ">=2.0.108", + "expensify-common": ">=2.0.115", "react": "*", "react-native": "*", "react-native-reanimated": ">=3.16.4" @@ -32336,9 +32336,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.89", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.89.tgz", - "integrity": "sha512-JzXjas0UNnYqTH4XD2Qfs64kBJBvHQ7HIIglieL1+Gto7eANyFRUpr0uRM3BlONinSPD/1xWZIurYAJtHuM5dg==", + "version": "2.0.92", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.92.tgz", + "integrity": "sha512-6StFOp3j4DC3gsY5Cl1qcbZ8mXL1RUMyzDf4l4im/4QlF6+bSpOHdYDZZjrUddbO/i1PA5ktUnAK4NM/JQ+BZg==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index ae6c407494fe..42e75636878a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.94-1", + "version": "9.0.94-19", "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.", @@ -79,7 +79,7 @@ "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-background-task": "file:./modules/background-task", - "@expensify/react-native-live-markdown": "0.1.223", + "@expensify/react-native-live-markdown": "0.1.230", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -166,7 +166,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.89", + "react-native-onyx": "2.0.92", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", diff --git a/src/CONST.ts b/src/CONST.ts index 1250092cb910..b8af68ddb934 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {add as dateAdd} from 'date-fns'; import {sub as dateSubtract} from 'date-fns/sub'; +// eslint-disable-next-line lodash/import-scope +import type {Dictionary} from 'lodash'; +import invertBy from 'lodash/invertBy'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; import type {ValueOf} from 'type-fest'; @@ -16,6 +19,8 @@ import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; const EMPTY_ARRAY = Object.freeze([]); const EMPTY_OBJECT = Object.freeze({}); +const DEFAULT_NUMBER_ID = 0; + const CLOUDFRONT_DOMAIN = 'cloudfront.net'; const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`; const ACTIVE_EXPENSIFY_URL = addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com'); @@ -159,7 +164,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Press the button.\n' + + '1. Click the green *+* button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -182,7 +187,7 @@ const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Press the button\n' + + '1. Click the green *+* button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -206,7 +211,7 @@ const onboardingPersonalSpendMessage: OnboardingMessage = { '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Press the button.\n' + + '1. Click the green *+* button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click "Just track it (don\'t submit it)".\n' + @@ -229,7 +234,7 @@ const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessage = { '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Press the button.\n' + + '1. Click the green *+* button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click "Just track it (don\'t submit it)".\n' + @@ -943,7 +948,7 @@ const CONST = { CLOUDFRONT_URL, EMPTY_ARRAY, EMPTY_OBJECT, - DEFAULT_NUMBER_ID: 0, + DEFAULT_NUMBER_ID, USE_EXPENSIFY_URL, EXPENSIFY_URL, GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', @@ -2171,6 +2176,31 @@ const CONST = { '_vietNam', ] as string[], + NSQS_EXPORT_DATE: { + LAST_EXPENSE: 'LAST_EXPENSE', + EXPORTED: 'EXPORTED', + SUBMITTED: 'SUBMITTED', + }, + + NSQS_INTEGRATION_ENTITY_MAP_TYPES: { + NETSUITE_DEFAULT: 'NETSUITE_DEFAULT', + REPORT_FIELD: 'REPORT_FIELD', + TAG: 'TAG', + }, + + NSQS_CONFIG: { + AUTO_SYNC: 'autoSync', + SYNC_OPTIONS: { + MAPPING: { + CUSTOMERS: 'syncOptions.mapping.customers', + PROJECTS: 'syncOptions.mapping.projects', + }, + }, + EXPORTER: 'exporter', + EXPORT_DATE: 'exportDate', + APPROVAL_ACCOUNT: 'approvalAccount', + }, + QUICKBOOKS_EXPORT_DATE: { LAST_EXPENSE: 'LAST_EXPENSE', REPORT_EXPORTED: 'REPORT_EXPORTED', @@ -2657,17 +2687,20 @@ const CONST = { QBD: 'quickbooksDesktop', XERO: 'xero', NETSUITE: 'netsuite', + NSQS: 'netsuiteQuickStart', SAGE_INTACCT: 'intacct', }, ROUTE: { QBO: 'quickbooks-online', XERO: 'xero', NETSUITE: 'netsuite', + NSQS: 'nsqs', SAGE_INTACCT: 'sage-intacct', QBD: 'quickbooks-desktop', }, NAME_USER_FRIENDLY: { netsuite: 'NetSuite', + netsuiteQuickStart: 'NSQS', quickbooksOnline: 'QuickBooks Online', quickbooksDesktop: 'QuickBooks Desktop', xero: 'Xero', @@ -2745,6 +2778,12 @@ const CONST = { NETSUITE_SYNC_EXPENSIFY_REIMBURSED_REPORTS: 'netSuiteSyncExpensifyReimbursedReports', NETSUITE_SYNC_IMPORT_VENDORS_TITLE: 'netSuiteImportVendorsTitle', NETSUITE_SYNC_IMPORT_CUSTOM_LISTS_TITLE: 'netSuiteImportCustomListsTitle', + NSQS_SYNC_CONNECTION: 'nsqsSyncConnection', + NSQS_SYNC_ACCOUNTS: 'nsqsSyncAccounts', + NSQS_SYNC_EMPLOYEES: 'nsqsSyncEmployees', + NSQS_SYNC_CUSTOMERS: 'nsqsSyncCustomers', + NSQS_SYNC_PROJECTS: 'nsqsSyncProjects', + NSQS_SYNC_CURRENCY: 'nsqsSyncCurrency', SAGE_INTACCT_SYNC_CHECK_CONNECTION: 'intacctCheckConnection', SAGE_INTACCT_SYNC_IMPORT_TITLE: 'intacctImportTitle', SAGE_INTACCT_SYNC_IMPORT_DATA: 'intacctImportData', @@ -2753,6 +2792,19 @@ const CONST = { SAGE_INTACCT_SYNC_IMPORT_SYNC_REIMBURSED_REPORTS: 'intacctImportSyncBillPayments', }, SYNC_STAGE_TIMEOUT_MINUTES: 20, + + // Map each connection to its designated display connection + get MULTI_CONNECTIONS_MAPPING() { + return { + [this.NAME.NETSUITE]: this.NAME.NETSUITE, + [this.NAME.NSQS]: this.NAME.NETSUITE, + } as Record, ValueOf | undefined>; + }, + + // Get linked connections by the designated display connection + get MULTI_CONNECTIONS_MAPPING_INVERTED() { + return invertBy(this.MULTI_CONNECTIONS_MAPPING) as Dictionary> | undefined>; + }, }, ACCESS_VARIANTS: { PAID: 'paid', @@ -2778,7 +2830,7 @@ const CONST = { NAME_PER_DIEM_INTERNATIONAL: 'Per Diem International', DISTANCE_UNIT_MILES: 'mi', DISTANCE_UNIT_KILOMETERS: 'km', - MILEAGE_IRS_RATE: 0.67, + MILEAGE_IRS_RATE: 0.7, DEFAULT_RATE: 'Default Rate', RATE_DECIMALS: 3, FAKE_P2P_ID: '_FAKE_P2P_ID_', @@ -5043,6 +5095,7 @@ const CONST = { quickbooksOnline: 'QuickBooks Online', xero: 'Xero', netsuite: 'NetSuite', + netsuiteQuickStart: 'NSQS', intacct: 'Sage Intacct', quickbooksDesktop: 'QuickBooks Desktop', }, @@ -5192,7 +5245,7 @@ const CONST = { '\n' + 'Here’s how to start a chat:\n' + '\n' + - '1. Press the button.\n' + + '1. Click the green *+* button.\n' + '2. Choose *Start chat*.\n' + '3. Enter emails or phone numbers.\n' + '\n' + @@ -5209,7 +5262,7 @@ const CONST = { '\n' + 'Here’s how to request money:\n' + '\n' + - '1. Press the button\n' + + '1. Click the green *+* button.\n' + '2. Choose *Start chat*.\n' + '3. Enter any email, SMS, or name of who you want to split with.\n' + '4. From within the chat, click the *+* button on the message bar, and click *Split expense*.\n' + @@ -5244,7 +5297,7 @@ const CONST = { '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Press the button.\n' + + '1. Click the green *+* button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -5326,183 +5379,183 @@ const CONST = { }, CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{ "AED": { - "rate": 396, + "rate": 414, "unit": "km" }, "AFN": { - "rate": 8369, + "rate": 8851, "unit": "km" }, "ALL": { - "rate": 11104, + "rate": 10783, "unit": "km" }, "AMD": { - "rate": 56842, + "rate": 45116, "unit": "km" }, "ANG": { - "rate": 193, + "rate": 203, "unit": "km" }, "AOA": { - "rate": 67518, + "rate": 102929, "unit": "km" }, "ARS": { - "rate": 9873, + "rate": 118428, "unit": "km" }, "AUD": { - "rate": 85, + "rate": 88, "unit": "km" }, "AWG": { - "rate": 195, + "rate": 203, "unit": "km" }, "AZN": { - "rate": 183, + "rate": 192, "unit": "km" }, "BAM": { - "rate": 177, + "rate": 212, "unit": "km" }, "BBD": { - "rate": 216, + "rate": 225, "unit": "km" }, "BDT": { - "rate": 9130, + "rate": 13697, "unit": "km" }, "BGN": { - "rate": 177, + "rate": 211, "unit": "km" }, "BHD": { - "rate": 40, + "rate": 42, "unit": "km" }, "BIF": { - "rate": 210824, + "rate": 331847, "unit": "km" }, "BMD": { - "rate": 108, + "rate": 113, "unit": "km" }, "BND": { - "rate": 145, + "rate": 153, "unit": "km" }, "BOB": { - "rate": 745, + "rate": 779, "unit": "km" }, "BRL": { - "rate": 594, + "rate": 660, "unit": "km" }, "BSD": { - "rate": 108, + "rate": 113, "unit": "km" }, "BTN": { - "rate": 7796, + "rate": 9761, "unit": "km" }, "BWP": { - "rate": 1180, + "rate": 1569, "unit": "km" }, "BYN": { - "rate": 280, + "rate": 369, "unit": "km" }, "BYR": { - "rate": 2159418, + "rate": 2255979, "unit": "km" }, "BZD": { - "rate": 217, + "rate": 227, "unit": "km" }, "CAD": { - "rate": 70, + "rate": 72, "unit": "km" }, "CDF": { - "rate": 213674, + "rate": 321167, "unit": "km" }, "CHF": { - "rate": 70, + "rate": 76, "unit": "km" }, "CLP": { - "rate": 77249, + "rate": 111689, "unit": "km" }, "CNY": { - "rate": 702, + "rate": 808, "unit": "km" }, "COP": { - "rate": 383668, + "rate": 473791, "unit": "km" }, "CRC": { - "rate": 65899, + "rate": 57190, "unit": "km" }, "CUC": { - "rate": 108, + "rate": 113, "unit": "km" }, "CUP": { - "rate": 2776, + "rate": 2902, "unit": "km" }, "CVE": { - "rate": 6112, + "rate": 11961, "unit": "km" }, "CZK": { - "rate": 2356, + "rate": 2715, "unit": "km" }, "DJF": { - "rate": 19151, + "rate": 19956, "unit": "km" }, "DKK": { - "rate": 379, + "rate": 381, "unit": "km" }, "DOP": { - "rate": 6144, + "rate": 6948, "unit": "km" }, "DZD": { - "rate": 14375, + "rate": 15226, "unit": "km" }, "EEK": { - "rate": 1576, + "rate": 1646, "unit": "km" }, "EGP": { - "rate": 1696, + "rate": 5657, "unit": "km" }, "ERN": { - "rate": 1617, + "rate": 1690, "unit": "km" }, "ETB": { - "rate": 4382, + "rate": 14326, "unit": "km" }, "EUR": { @@ -5510,11 +5563,11 @@ const CONST = { "unit": "km" }, "FJD": { - "rate": 220, + "rate": 264, "unit": "km" }, "FKP": { - "rate": 77, + "rate": 90, "unit": "km" }, "GBP": { @@ -5522,55 +5575,55 @@ const CONST = { "unit": "mi" }, "GEL": { - "rate": 359, + "rate": 323, "unit": "km" }, "GHS": { - "rate": 620, + "rate": 1724, "unit": "km" }, "GIP": { - "rate": 77, + "rate": 90, "unit": "km" }, "GMD": { - "rate": 5526, + "rate": 8111, "unit": "km" }, "GNF": { - "rate": 1081319, + "rate": 974619, "unit": "km" }, "GTQ": { - "rate": 832, + "rate": 872, "unit": "km" }, "GYD": { - "rate": 22537, + "rate": 23585, "unit": "km" }, "HKD": { - "rate": 837, + "rate": 877, "unit": "km" }, "HNL": { - "rate": 2606, + "rate": 2881, "unit": "km" }, "HRK": { - "rate": 684, + "rate": 814, "unit": "km" }, "HTG": { - "rate": 8563, + "rate": 14734, "unit": "km" }, "HUF": { - "rate": 33091, + "rate": 44127, "unit": "km" }, "IDR": { - "rate": 1555279, + "rate": 1830066, "unit": "km" }, "ILS": { @@ -5578,147 +5631,147 @@ const CONST = { "unit": "km" }, "INR": { - "rate": 7805, + "rate": 9761, "unit": "km" }, "IQD": { - "rate": 157394, + "rate": 147577, "unit": "km" }, "IRR": { - "rate": 4539961, + "rate": 4741290, "unit": "km" }, "ISK": { - "rate": 13518, + "rate": 15772, "unit": "km" }, "JMD": { - "rate": 15794, + "rate": 17738, "unit": "km" }, "JOD": { - "rate": 77, + "rate": 80, "unit": "km" }, "JPY": { - "rate": 11748, + "rate": 17542, "unit": "km" }, "KES": { - "rate": 11845, + "rate": 14589, "unit": "km" }, "KGS": { - "rate": 9144, + "rate": 9852, "unit": "km" }, "KHR": { - "rate": 437658, + "rate": 453066, "unit": "km" }, "KMF": { - "rate": 44418, + "rate": 53269, "unit": "km" }, "KPW": { - "rate": 97043, + "rate": 101389, "unit": "km" }, "KRW": { - "rate": 121345, + "rate": 162705, "unit": "km" }, "KWD": { - "rate": 32, + "rate": 35, "unit": "km" }, "KYD": { - "rate": 90, + "rate": 93, "unit": "km" }, "KZT": { - "rate": 45396, + "rate": 58319, "unit": "km" }, "LAK": { - "rate": 1010829, + "rate": 2452802, "unit": "km" }, "LBP": { - "rate": 164153, + "rate": 10093809, "unit": "km" }, "LKR": { - "rate": 21377, + "rate": 33423, "unit": "km" }, "LRD": { - "rate": 18709, + "rate": 22185, "unit": "km" }, "LSL": { - "rate": 1587, + "rate": 2099, "unit": "km" }, "LTL": { - "rate": 348, + "rate": 364, "unit": "km" }, "LVL": { - "rate": 71, + "rate": 74, "unit": "km" }, "LYD": { - "rate": 486, + "rate": 554, "unit": "km" }, "MAD": { - "rate": 967, + "rate": 1127, "unit": "km" }, "MDL": { - "rate": 1910, + "rate": 2084, "unit": "km" }, "MGA": { - "rate": 406520, + "rate": 529635, "unit": "km" }, "MKD": { - "rate": 5570, + "rate": 6650, "unit": "km" }, "MMK": { - "rate": 152083, + "rate": 236413, "unit": "km" }, "MNT": { - "rate": 306788, + "rate": 382799, "unit": "km" }, "MOP": { - "rate": 863, + "rate": 904, "unit": "km" }, "MRO": { - "rate": 38463, + "rate": 40234, "unit": "km" }, "MRU": { - "rate": 3862, + "rate": 4506, "unit": "km" }, "MUR": { - "rate": 4340, + "rate": 5226, "unit": "km" }, "MVR": { - "rate": 1667, + "rate": 1735, "unit": "km" }, "MWK": { - "rate": 84643, + "rate": 195485, "unit": "km" }, "MXN": { @@ -5726,23 +5779,23 @@ const CONST = { "unit": "km" }, "MYR": { - "rate": 444, + "rate": 494, "unit": "km" }, "MZN": { - "rate": 7772, + "rate": 7199, "unit": "km" }, "NAD": { - "rate": 1587, + "rate": 2099, "unit": "km" }, "NGN": { - "rate": 42688, + "rate": 174979, "unit": "km" }, "NIO": { - "rate": 3772, + "rate": 4147, "unit": "km" }, "NOK": { @@ -5750,35 +5803,35 @@ const CONST = { "unit": "km" }, "NPR": { - "rate": 12474, + "rate": 15617, "unit": "km" }, "NZD": { - "rate": 95, + "rate": 104, "unit": "km" }, "OMR": { - "rate": 42, + "rate": 43, "unit": "km" }, "PAB": { - "rate": 108, + "rate": 113, "unit": "km" }, "PEN": { - "rate": 401, + "rate": 420, "unit": "km" }, "PGK": { - "rate": 380, + "rate": 455, "unit": "km" }, "PHP": { - "rate": 5234, + "rate": 6582, "unit": "km" }, "PKR": { - "rate": 16785, + "rate": 31411, "unit": "km" }, "PLN": { @@ -5786,43 +5839,43 @@ const CONST = { "unit": "km" }, "PYG": { - "rate": 704732, + "rate": 890772, "unit": "km" }, "QAR": { - "rate": 393, + "rate": 410, "unit": "km" }, "RON": { - "rate": 443, + "rate": 538, "unit": "km" }, "RSD": { - "rate": 10630, + "rate": 12656, "unit": "km" }, "RUB": { - "rate": 8074, + "rate": 11182, "unit": "km" }, "RWF": { - "rate": 107182, + "rate": 156589, "unit": "km" }, "SAR": { - "rate": 404, + "rate": 423, "unit": "km" }, "SBD": { - "rate": 859, + "rate": 951, "unit": "km" }, "SCR": { - "rate": 2287, + "rate": 1611, "unit": "km" }, "SDG": { - "rate": 41029, + "rate": 67705, "unit": "km" }, "SEK": { @@ -5830,155 +5883,159 @@ const CONST = { "unit": "km" }, "SGD": { - "rate": 145, + "rate": 151, "unit": "km" }, "SHP": { - "rate": 77, + "rate": 90, "unit": "km" }, "SLL": { - "rate": 1102723, + "rate": 2362357, + "unit": "km" + }, + "SLE": { + "rate": 2363, "unit": "km" }, "SOS": { - "rate": 62604, + "rate": 64374, "unit": "km" }, "SRD": { - "rate": 1526, + "rate": 3954, "unit": "km" }, "STD": { - "rate": 2223309, + "rate": 2510095, "unit": "km" }, "STN": { - "rate": 2232, + "rate": 2683, "unit": "km" }, "SVC": { - "rate": 943, + "rate": 987, "unit": "km" }, "SYP": { - "rate": 82077, + "rate": 1464664, "unit": "km" }, "SZL": { - "rate": 1585, + "rate": 2099, "unit": "km" }, "THB": { - "rate": 3328, + "rate": 3801, "unit": "km" }, "TJS": { - "rate": 1230, + "rate": 1228, "unit": "km" }, "TMT": { - "rate": 378, + "rate": 394, "unit": "km" }, "TND": { - "rate": 295, + "rate": 360, "unit": "km" }, "TOP": { - "rate": 245, + "rate": 274, "unit": "km" }, "TRY": { - "rate": 845, + "rate": 4035, "unit": "km" }, "TTD": { - "rate": 732, + "rate": 763, "unit": "km" }, "TWD": { - "rate": 3055, + "rate": 3703, "unit": "km" }, "TZS": { - "rate": 250116, + "rate": 286235, "unit": "km" }, "UAH": { - "rate": 2985, + "rate": 4725, "unit": "km" }, "UGX": { - "rate": 395255, + "rate": 416016, "unit": "km" }, "USD": { - "rate": 67, + "rate": 70, "unit": "mi" }, "UYU": { - "rate": 4777, + "rate": 4888, "unit": "km" }, "UZS": { - "rate": 1131331, + "rate": 1462038, "unit": "km" }, "VEB": { - "rate": 679346, + "rate": 709737, "unit": "km" }, "VEF": { - "rate": 26793449, + "rate": 27993155, "unit": "km" }, "VES": { - "rate": 194381905, + "rate": 6457, "unit": "km" }, "VND": { - "rate": 2487242, + "rate": 2825526, "unit": "km" }, "VUV": { - "rate": 11748, + "rate": 13358, "unit": "km" }, "WST": { - "rate": 272, + "rate": 315, "unit": "km" }, "XAF": { - "rate": 59224, + "rate": 70811, "unit": "km" }, "XCD": { - "rate": 291, + "rate": 304, "unit": "km" }, "XOF": { - "rate": 59224, + "rate": 70811, "unit": "km" }, "XPF": { - "rate": 10783, + "rate": 12875, "unit": "km" }, "YER": { - "rate": 27037, + "rate": 28003, "unit": "km" }, "ZAR": { - "rate": 464, + "rate": 484, "unit": "km" }, "ZMK": { - "rate": 566489, + "rate": 591756, "unit": "km" }, "ZMW": { - "rate": 2377, + "rate": 3148, "unit": "km" } }`) as Record, @@ -6136,6 +6193,7 @@ const CONST = { LOWER_THAN: 'lt', LOWER_THAN_OR_EQUAL_TO: 'lte', }, + SYNTAX_RANGE_NAME: 'syntax', SYNTAX_ROOT_KEYS: { TYPE: 'type', STATUS: 'status', @@ -6567,6 +6625,8 @@ const CONST = { ERROR_PERMISSION_DENIED: 'permissionDenied', }, }, + SKIPPABLE_COLLECTION_MEMBER_IDS: [String(DEFAULT_NUMBER_ID), '-1', 'undefined', 'null', 'NaN'] as string[], + SETUP_SPECIALIST_LOGIN: 'Setup Specialist', } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 54b7da704cd1..1fb84c3dd9cf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -727,6 +727,8 @@ const ONYXKEYS = { NETSUITE_TOKEN_INPUT_FORM_DRAFT: 'netsuiteTokenInputFormDraft', NETSUITE_CUSTOM_FORM_ID_FORM: 'netsuiteCustomFormIDForm', NETSUITE_CUSTOM_FORM_ID_FORM_DRAFT: 'netsuiteCustomFormIDFormDraft', + NSQS_OAUTH2_FORM: 'nsqsOAuth2Form', + NSQS_OAUTH2_FORM_DRAFT: 'nsqsOAuth2FormDraft', SAGE_INTACCT_DIMENSION_TYPE_FORM: 'sageIntacctDimensionTypeForm', SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft', SEARCH_ADVANCED_FILTERS_FORM: 'searchAdvancedFiltersForm', @@ -837,6 +839,7 @@ type OnyxFormValuesMapping = { [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.NSQS_OAUTH2_FORM]: FormTypes.NSQSOAuth2Form; [ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm; [ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM]: FormTypes.SearchAdvancedFiltersForm; [ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM]: FormTypes.TextPickerModalForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 393085ab4384..87664b718974 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1114,6 +1114,28 @@ const ROUTES = { getRoute: (policyID: string, connection?: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection as string}/card-reconciliation/account` as const, }, + WORKSPACE_ACCOUNTING_MULTI_CONNECTION_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/:connection/connection-selector', + getRoute: ( + policyID: string, + connection: ValueOf, + integrationToDisconnect?: ConnectionName, + shouldDisconnectIntegrationBeforeConnecting?: boolean, + ) => { + const searchParams = new URLSearchParams(); + + if (integrationToDisconnect) { + searchParams.append('integrationToDisconnect', integrationToDisconnect); + } + if (shouldDisconnectIntegrationBeforeConnecting !== undefined) { + searchParams.append('shouldDisconnectIntegrationBeforeConnecting', shouldDisconnectIntegrationBeforeConnecting.toString()); + } + + const queryParams = searchParams.size ? `?${searchParams.toString()}` : ''; + + return `settings/workspaces/${policyID}/accounting/${connection}/connection-selector${queryParams}` as const; + }, + }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', getRoute: (policyID: string | undefined) => { @@ -1942,6 +1964,50 @@ const ROUTES = { route: 'settings/workspaces/:policyID/connections/netsuite/advanced/autosync/accounting-method', getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/autosync/accounting-method` as const, }, + POLICY_ACCOUNTING_NSQS_SETUP: { + route: 'settings/workspaces/:policyID/accounting/nsqs/setup', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/setup` as const, + }, + POLICY_ACCOUNTING_NSQS_IMPORT: { + route: 'settings/workspaces/:policyID/accounting/nsqs/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import` as const, + }, + POLICY_ACCOUNTING_NSQS_IMPORT_CUSTOMERS: { + route: 'settings/workspaces/:policyID/accounting/nsqs/import/customers', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/customers` as const, + }, + POLICY_ACCOUNTING_NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS: { + route: 'settings/workspaces/:policyID/accounting/nsqs/import/customers/displayed-as', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/customers/displayed-as` as const, + }, + POLICY_ACCOUNTING_NSQS_IMPORT_PROJECTS: { + route: 'settings/workspaces/:policyID/accounting/nsqs/import/projects', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/projects` as const, + }, + POLICY_ACCOUNTING_NSQS_IMPORT_PROJECTS_DISPLAYED_AS: { + route: 'settings/workspaces/:policyID/accounting/nsqs/import/projects/displayed-as', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/projects/displayed-as` as const, + }, + POLICY_ACCOUNTING_NSQS_EXPORT: { + route: 'settings/workspaces/:policyID/accounting/nsqs/export', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/export` as const, + }, + POLICY_ACCOUNTING_NSQS_EXPORT_PREFERRED_EXPORTER: { + route: 'settings/workspaces/:policyID/accounting/nsqs/export/preferred-exporter', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/export/preferred-exporter` as const, + }, + POLICY_ACCOUNTING_NSQS_EXPORT_DATE: { + route: 'settings/workspaces/:policyID/accounting/nsqs/export/date', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/export/date` as const, + }, + POLICY_ACCOUNTING_NSQS_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/nsqs/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/advanced` as const, + }, + POLICY_ACCOUNTING_NSQS_ADVANCED_APPROVAL_ACCOUNT: { + route: 'settings/workspaces/:policyID/accounting/nsqs/advanced/approval-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/advanced/approval-account` 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, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 04bb3c6297ba..4ee20f34cf16 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -431,6 +431,17 @@ const SCREENS = { NETSUITE_CUSTOM_FORM_ID: 'Policy_Accounting_NetSuite_Custom_Form_ID', NETSUITE_AUTO_SYNC: 'Policy_Accounting_NetSuite_Auto_Sync', NETSUITE_ACCOUNTING_METHOD: 'Policy_Accounting_NetSuite_Accounting_Method', + NSQS_SETUP: 'Policy_Accounting_NSQS_Setup', + NSQS_IMPORT: 'Policy_Accounting_NSQS_Import', + NSQS_IMPORT_CUSTOMERS: 'Policy_Accounting_NSQS_Import_Customers', + NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_NSQS_Import_Customers_Displayed_As', + NSQS_IMPORT_PROJECTS: 'Policy_Accounting_NSQS_Import_Projects', + NSQS_IMPORT_PROJECTS_DISPLAYED_AS: 'Policy_Accounting_NSQS_Import_Projects_Displayed_As', + NSQS_EXPORT: 'Policy_Accounting_NSQS_Export', + NSQS_EXPORT_PREFERRED_EXPORTER: 'Policy_Accounting_NSQS_Export_Preferred_Exporter', + NSQS_EXPORT_DATE: 'Policy_Accounting_NSQS_Export_Date', + NSQS_ADVANCED: 'Policy_Accounting_NSQS_Advanced', + NSQS_ADVANCED_APPROVAL_ACCOUNT: 'Policy_Accounting_NSQS_Advanced_Approval_Account', 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', @@ -454,6 +465,7 @@ const SCREENS = { SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account', CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation', RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings', + MULTI_CONNECTION_SELECTOR: 'Policy_Accounting_Multi_Connection_Selector', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx index 4d54258dbef0..448357188b45 100644 --- a/src/components/AmountWithoutCurrencyInput.tsx +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -1,5 +1,7 @@ -import React from 'react'; +import React, {useCallback, useMemo} from 'react'; import type {ForwardedRef} from 'react'; +import useLocalize from '@hooks/useLocalize'; +import {replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount} from '@libs/MoneyRequestUtils'; import CONST from '@src/CONST'; import TextInput from './TextInput'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; @@ -16,21 +18,46 @@ type AmountFormProps = { } & Partial; function AmountWithoutCurrencyInput( - {value: amount, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + {value: amount, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, onInputChange, ...rest}: AmountFormProps, ref: ForwardedRef, ) { + const {toLocaleDigit} = useLocalize(); + const separator = useMemo( + () => + replaceAllDigits('1.1', toLocaleDigit) + .split('') + .filter((char) => char !== '1') + .join(''), + [toLocaleDigit], + ); + /** + * Sets the selection and the amount accordingly to the value passed to the input + * @param newAmount - Changed amount from user input + */ + const setNewAmount = useCallback( + (newAmount: string) => { + // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value + // More info: https://github.com/Expensify/App/issues/16974 + const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); + const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); + onInputChange?.(replacedCommasAmount); + }, + [onInputChange], + ); + return ( ; + + /** Whether we should display the button that opens new SearchRouter */ + shouldDisplaySearchRouter?: boolean; }; // eslint-disable-next-line rulesdir/no-negated-variables @@ -58,6 +65,8 @@ function FullPageNotFoundView({ shouldShowBackButton = true, onLinkPress = () => Navigation.dismissModal(), shouldForceFullScreen = false, + subtitleStyle, + shouldDisplaySearchRouter, }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -68,6 +77,7 @@ function FullPageNotFoundView({ diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx new file mode 100644 index 000000000000..1953acde8ad4 --- /dev/null +++ b/src/components/BookTravelButton.tsx @@ -0,0 +1,120 @@ +import {Str} from 'expensify-common'; +import React, {useCallback, useContext, useState} from 'react'; +import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {openTravelDotLink} from '@libs/actions/Link'; +import {cleanupTravelProvisioningSession} from '@libs/actions/Travel'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import {getAdminsPrivateEmailDomains} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import Button from './Button'; +import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; +import DotIndicatorMessage from './DotIndicatorMessage'; + +type BookTravelButtonProps = { + text: string; +}; + +const navigateToAcceptTerms = (domain: string) => { + // Remove the previous provision session infromation if any is cached. + cleanupTravelProvisioningSession(); + Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); +}; + +function BookTravelButton({text}: BookTravelButtonProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const policy = usePolicy(activePolicyID); + const [errorMessage, setErrorMessage] = useState(''); + const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const primaryLogin = account?.primaryLogin; + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); + + // Flag indicating whether NewDot was launched exclusively for Travel, + // e.g., when the user selects "Trips" from the Expensify Classic menu in HybridApp. + const [wasNewDotLaunchedJustForTravel] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY); + + const bookATrip = useCallback(() => { + setErrorMessage(''); + + // The primary login of the user is where Spotnana sends the emails with booking confirmations, itinerary etc. It can't be a phone number. + if (!primaryLogin || Str.isSMSLogin(primaryLogin)) { + setErrorMessage(translate('travel.phoneError')); + return; + } + + // Spotnana requires an address anytime an entity is created for a policy + if (isEmptyObject(policy?.address)) { + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute())); + return; + } + + const isPolicyProvisioned = policy?.travelSettings?.spotnanaCompanyID ?? policy?.travelSettings?.associatedTravelDomainAccountID; + if (policy?.travelSettings?.hasAcceptedTerms ?? (travelSettings?.hasAcceptedTerms && isPolicyProvisioned)) { + openTravelDotLink(policy?.id) + ?.then(() => { + // When a user selects "Trips" in the Expensify Classic menu, the HybridApp opens the ManageTrips page in NewDot. + // The wasNewDotLaunchedJustForTravel flag indicates if NewDot was launched solely for this purpose. + if (!NativeModules.HybridAppModule || !wasNewDotLaunchedJustForTravel) { + return; + } + + // Close NewDot if it was opened only for Travel, as its purpose is now fulfilled. + Log.info('[HybridApp] Returning to OldDot after opening TravelDot'); + NativeModules.HybridAppModule.closeReactNativeApp(false, false); + setRootStatusBarEnabled(false); + }) + ?.catch(() => { + setErrorMessage(translate('travel.errorMessage')); + }); + } else if (isPolicyProvisioned) { + navigateToAcceptTerms(CONST.TRAVEL.DEFAULT_DOMAIN); + } else { + // Determine the domain to associate with the workspace during provisioning in Spotnana. + // - If all admins share the same private domain, the workspace is tied to it automatically. + // - If admins have multiple private domains, the user must select one. + // - Public domains are not allowed; an error page is shown in that case. + const adminDomains = getAdminsPrivateEmailDomains(policy); + if (adminDomains.length === 0) { + Navigation.navigate(ROUTES.TRAVEL_PUBLIC_DOMAIN_ERROR); + } else if (adminDomains.length === 1) { + navigateToAcceptTerms(adminDomains.at(0) ?? CONST.TRAVEL.DEFAULT_DOMAIN); + } else { + Navigation.navigate(ROUTES.TRAVEL_DOMAIN_SELECTOR); + } + } + }, [policy, wasNewDotLaunchedJustForTravel, travelSettings, translate, primaryLogin, setRootStatusBarEnabled]); + + return ( + <> + {!!errorMessage && ( + + )} +