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..128dcf48e934 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" @@ -24,28 +24,18 @@ outputs: runs: using: composite steps: - - name: Check if gpg encrypted private key is present - id: key_check - shell: bash - run: | - if [[ -f .github/workflows/OSBotify-private-key.asc.gpg ]]; then - echo "key_exists=true" >> "$GITHUB_OUTPUT" - fi - - - name: Checkout - uses: actions/checkout@v4 - if: steps.key_check.outputs.key_exists != 'true' - with: - sparse-checkout: | - .github + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 - - 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: 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 +45,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/actions/javascript/getPullRequestDetails/getPullRequestDetails.ts b/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.ts index 3ff0de1b3bb2..8e285389e3f7 100644 --- a/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.ts +++ b/.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.ts @@ -54,6 +54,7 @@ GithubUtils.octokit.pulls .then(({data: PR}) => { if (!isEmptyObject(PR)) { console.log(`Found matching pull request: ${PR.html_url}`); + console.log(`Pull request details: ${JSON.stringify(PR)}}`); core.setOutput('MERGE_COMMIT_SHA', PR.merge_commit_sha); core.setOutput('HEAD_COMMIT_SHA', PR.head?.sha); core.setOutput('IS_MERGED', PR.merged); diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index fa7e7e00e137..07baf462086c 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11502,6 +11502,7 @@ GithubUtils_1.default.octokit.pulls .then(({ data: PR }) => { if (!(0, EmptyObject_1.isEmptyObject)(PR)) { console.log(`Found matching pull request: ${PR.html_url}`); + console.log(`Pull request details: ${JSON.stringify(PR)}}`); core.setOutput('MERGE_COMMIT_SHA', PR.merge_commit_sha); core.setOutput('HEAD_COMMIT_SHA', PR.head?.sha); core.setOutput('IS_MERGED', PR.merged); 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..3f32b2bfa1fe 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,10 +97,15 @@ 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`). -Furthermore, secrets are not accessible in actions. If you need to access a secret in an action, you must declare it as an input and pass it in. GitHub _should_ still obfuscate the value of the secret in workflow run logs. +Furthermore, secrets are not accessible in actions. If you need to access a secret in an action, _you must declare it as an input and pass it in_. GitHub _should_ still obfuscate the value of the secret in workflow run logs. ## Actions diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index 5cb0a99730c9..b7dcf95294be 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -45,7 +45,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 }} @@ -56,14 +56,19 @@ jobs: SEMVER_LEVEL: "PATCH" - name: Fetch history of relevant refs - run: | - git fetch origin main staging --no-tags --shallow-exclude ${{ steps.getPreviousVersion.outputs.PREVIOUS_VERSION }} + run: git fetch origin main staging --no-tags --shallow-exclude ${{ steps.getPreviousVersion.outputs.PREVIOUS_VERSION }} - name: Get version bump commit id: getVersionBumpCommit run: | git switch main VERSION_BUMP_COMMIT="$(git log --format='%H' --author='OSBotify' --grep 'Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}')" + if [ -z "$VERSION_BUMP_COMMIT" ]; then + echo "::error::❌ Could not find version bump commit for ${{ needs.createNewVersion.outputs.NEW_VERSION }}" + git log --oneline + else + echo "::notice::👀 Found version bump commit $VERSION_BUMP_COMMIT" + fi echo "VERSION_BUMP_SHA=$VERSION_BUMP_COMMIT" >> "$GITHUB_OUTPUT" - name: Get merge commit for pull request to CP 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 99c38a2d2e5b..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 }} @@ -301,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: | @@ -314,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 }} 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 ea62bca794fe..80918d65462c 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -242,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 @@ -257,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/Mobile-Expensify b/Mobile-Expensify index 9e5fc5211c4d..daafba5d743d 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 9e5fc5211c4dd2ee130aa6bb9d3f09b1728947df +Subproject commit daafba5d743ddbf9e114e5787aa5fd00b5a1655e 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/build.gradle b/android/app/build.gradle index d954287f7a1f..7ba437feeda7 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 1009009408 - versionName "9.0.94-8" + versionCode 1009009424 + versionName "9.0.94-24" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/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/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/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/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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e6c0f8051831..dec75b7bc653 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.94.8 + 9.0.94.24 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 62cb96059e63..e0b61a75885f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.94.8 + 9.0.94.24 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 10caac53aeff..63805152d6ed 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.94 CFBundleVersion - 9.0.94.8 + 9.0.94.24 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index b976f36b5134..ab3dc7fe78c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.94-8", + "version": "9.0.94-24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.94-8", + "version": "9.0.94-24", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 34e38fe54979..2bf3c28acecb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.94-8", + "version": "9.0.94-24", "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.", diff --git a/src/CONST.ts b/src/CONST.ts index 6cbd983b9225..b8af68ddb934 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2830,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_', @@ -5379,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": { @@ -5563,11 +5563,11 @@ const CONST = { "unit": "km" }, "FJD": { - "rate": 220, + "rate": 264, "unit": "km" }, "FKP": { - "rate": 77, + "rate": 90, "unit": "km" }, "GBP": { @@ -5575,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": { @@ -5631,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": { @@ -5779,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": { @@ -5803,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": { @@ -5839,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": { @@ -5883,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, @@ -6189,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', diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index fec3fa6fa6fd..e9b3b555385d 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,18 +1,33 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import { + getChatRoomSubtitle, + getDisplayNamesWithTooltips, + getIcons, + getParentNavigationSubtitle, + getReportName, + isChatThread, + isExpenseReport, + isInvoiceReport, + isIOUReport, + isMoneyRequest, + isMoneyRequestReport, + isTrackExpenseReport, + navigateToDetailsPage, + shouldReportShowSubscript, +} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {Policy, Report} from '@src/types/onyx'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import CaretWrapper from './CaretWrapper'; import DisplayNames from './DisplayNames'; @@ -23,15 +38,7 @@ import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; -type AvatarWithDisplayNamePropsWithOnyx = { - /** All of the actions of the report */ - parentReportActions: OnyxEntry; - - /** Personal details of all users */ - personalDetails: OnyxEntry; -}; - -type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { +type AvatarWithDisplayNameProps = { /** The report currently being looked at */ report: OnyxEntry; @@ -55,41 +62,38 @@ const fallbackIcon: Icon = { id: -1, }; -function AvatarWithDisplayName({ - policy, - report, - parentReportActions, - isAnonymous = false, - size = CONST.AVATAR_SIZE.DEFAULT, - shouldEnableDetailPageNavigation = false, - personalDetails = CONST.EMPTY_OBJECT, -}: AvatarWithDisplayNameProps) { +function AvatarWithDisplayName({policy, report, isAnonymous = false, size = CONST.AVATAR_SIZE.DEFAULT, shouldEnableDetailPageNavigation = false}: AvatarWithDisplayNameProps) { + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {canEvict: false}); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) ?? CONST.EMPTY_OBJECT; + const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); const [invoiceReceiverPolicy] = useOnyx( - `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, + `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : CONST.DEFAULT_NUMBER_ID}`, ); - const title = ReportUtils.getReportName(report, invoiceReceiverPolicy); - const subtitle = ReportUtils.getChatRoomSubtitle(report); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); - const isMoneyRequestOrReport = - ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); - const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); - const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); - const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); + const title = getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); + const subtitle = getChatRoomSubtitle(report, {isCreateExpenseFlow: true}); + const parentNavigationSubtitleData = getParentNavigationSubtitle(report); + const isMoneyRequestOrReport = isMoneyRequestReport(report) || isMoneyRequest(report) || isTrackExpenseReport(report) || isInvoiceReport(report); + const icons = getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); + const ownerPersonalDetails = getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); + const displayNamesWithTooltips = getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); + const shouldShowSubscriptAvatar = shouldReportShowSubscript(report); const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG; const actorAccountID = useRef(null); useEffect(() => { - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; - actorAccountID.current = parentReportAction?.actorAccountID ?? -1; + if (!report?.parentReportActionID) { + return; + } + const parentReportAction = parentReportActions?.[report?.parentReportActionID]; + actorAccountID.current = parentReportAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID; }, [parentReportActions, report]); const goToDetailsPage = useCallback(() => { - ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute()); + navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute()); }, [report]); const showActorDetails = useCallback(() => { @@ -99,17 +103,17 @@ function AvatarWithDisplayName({ return; } - if (ReportUtils.isExpenseReport(report) && report?.ownerAccountID) { + if (isExpenseReport(report) && report?.ownerAccountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(report.ownerAccountID)); return; } - if (ReportUtils.isIOUReport(report) && report?.reportID) { + if (isIOUReport(report) && report?.reportID) { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report.reportID)); return; } - if (ReportUtils.isChatThread(report)) { + if (isChatThread(report)) { // In an ideal situation account ID won't be 0 if (actorAccountID.current && actorAccountID.current > 0) { Navigation.navigate(ROUTES.PROFILE.getRoute(actorAccountID.current)); @@ -198,12 +202,4 @@ function AvatarWithDisplayName({ AvatarWithDisplayName.displayName = 'AvatarWithDisplayName'; -export default withOnyx({ - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '-1'}`, - canEvict: false, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, -})(AvatarWithDisplayName); +export default AvatarWithDisplayName; 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 && ( + + )} +