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.19FullStoryOrgId
@@ -92,6 +92,8 @@
ExpensifyNewKansas-Medium.otfExpensifyNewKansas-MediumItalic.otfExpensifyMono-Bold.otf
+ ExpensifyMono-BoldItalic.otf
+ ExpensifyMono-Italic.otfExpensifyMono-Regular.otfExpensifyNeue-Bold.otfExpensifyNeue-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 @@
CFBundleShortVersionString9.0.94CFBundleVersion
- 9.0.94.1
+ 9.0.94.19NSExtensionNSExtensionPointIdentifier
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>IGUejYC<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 && (
+
+ )}
+
+ >
+ );
+}
+
+BookTravelButton.displayName = 'BookTravelButton';
+
+export default BookTravelButton;
diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx
index ed5ecf41078a..bc628458c46c 100644
--- a/src/components/BrokenConnectionDescription.tsx
+++ b/src/components/BrokenConnectionDescription.tsx
@@ -56,7 +56,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn
);
}
- if (isReportApproved(report) || isReportManuallyReimbursed(report) || (isProcessingReport(report) && !isInstantSubmitEnabled(policy))) {
+ if (isReportApproved({report}) || isReportManuallyReimbursed(report) || (isProcessingReport(report) && !isInstantSubmitEnabled(policy))) {
return translate('violations.memberBrokenConnectionError');
}
diff --git a/src/components/ConnectToNSQSFlow/index.tsx b/src/components/ConnectToNSQSFlow/index.tsx
new file mode 100644
index 000000000000..87b32007f9f5
--- /dev/null
+++ b/src/components/ConnectToNSQSFlow/index.tsx
@@ -0,0 +1,15 @@
+import {useEffect} from 'react';
+import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+import type {ConnectToNSQSFlowProps} from './types';
+
+function ConnectToNSQSFlow({policyID}: ConnectToNSQSFlowProps) {
+ useEffect(() => {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NSQS_SETUP.getRoute(policyID));
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, []);
+
+ return null;
+}
+
+export default ConnectToNSQSFlow;
diff --git a/src/components/ConnectToNSQSFlow/types.ts b/src/components/ConnectToNSQSFlow/types.ts
new file mode 100644
index 000000000000..7a19bd321b99
--- /dev/null
+++ b/src/components/ConnectToNSQSFlow/types.ts
@@ -0,0 +1,10 @@
+import type {PolicyConnectionName} from '@src/types/onyx/Policy';
+
+type ConnectToNSQSFlowProps = {
+ policyID: string;
+ shouldDisconnectIntegrationBeforeConnecting?: boolean;
+ integrationToDisconnect?: PolicyConnectionName;
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export type {ConnectToNSQSFlowProps};
diff --git a/src/components/ConnectToNetSuiteFlow/index.tsx b/src/components/ConnectToNetSuiteFlow/index.tsx
index 7957896d4006..1bf3712c0f01 100644
--- a/src/components/ConnectToNetSuiteFlow/index.tsx
+++ b/src/components/ConnectToNetSuiteFlow/index.tsx
@@ -18,7 +18,11 @@ function ConnectToNetSuiteFlow({policyID}: ConnectToNetSuiteFlowProps) {
const {translate} = useLocalize();
const hasPoliciesConnectedToNetSuite = !!getAdminPoliciesConnectedToNetSuite()?.length;
- const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
+
const [isReuseConnectionsPopoverOpen, setIsReuseConnectionsPopoverOpen] = useState(false);
const [reuseConnectionPopoverPosition, setReuseConnectionPopoverPosition] = useState({horizontal: 0, vertical: 0});
const {popoverAnchorRefs} = useAccountingContext();
@@ -57,7 +61,7 @@ function ConnectToNetSuiteFlow({policyID}: ConnectToNetSuiteFlowProps) {
}, []);
if (threeDotsMenuContainerRef) {
- if (!shouldUseNarrowLayout) {
+ if (!isSmallScreenWidth) {
threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
const horizontal = x + width;
const vertical = y + height;
diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx
index 9a232e83fb97..c7bc37e38e3e 100644
--- a/src/components/ConnectionLayout.tsx
+++ b/src/components/ConnectionLayout.tsx
@@ -5,7 +5,7 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {getPolicy} from '@libs/PolicyUtils';
import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
@@ -106,7 +106,7 @@ function ConnectionLayout({
}: ConnectionLayoutProps) {
const {translate} = useLocalize();
- const policy = PolicyUtils.getPolicy(policyID);
+ const policy = getPolicy(policyID);
const isConnectionEmpty = isEmpty(policy?.connections?.[connectionName]);
const renderSelectionContent = useMemo(
diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx
index 8a4455e02bd6..a811face3340 100644
--- a/src/components/DistanceRequest/DistanceRequestFooter.tsx
+++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx
@@ -1,7 +1,7 @@
import React, {useCallback, useMemo} from 'react';
import type {ReactNode} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import DistanceMapView from '@components/DistanceMapView';
@@ -9,24 +9,22 @@ import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
import type {WayPoint} from '@components/MapView/MapViewTypes';
import useLocalize from '@hooks/useLocalize';
+import usePolicy from '@hooks/usePolicy';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as TransactionUtils from '@libs/TransactionUtils';
+import DistanceRequestUtils from '@libs/DistanceRequestUtils';
+import {getPersonalPolicy} from '@libs/PolicyUtils';
+import {getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {MapboxAccessToken} from '@src/types/onyx';
+import type {Policy} from '@src/types/onyx';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
import type Transaction from '@src/types/onyx/Transaction';
import type IconAsset from '@src/types/utils/IconAsset';
const MAX_WAYPOINTS = 25;
-type DistanceRequestFooterOnyxProps = {
- /** Data about Mapbox token for calling Mapbox API */
- mapboxAccessToken: OnyxEntry;
-};
-
-type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & {
+type DistanceRequestFooterProps = {
/** The waypoints for the distance expense */
waypoints?: WaypointCollection;
@@ -35,16 +33,26 @@ type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & {
/** The transaction being interacted with */
transaction: OnyxEntry;
+
+ /** The policy */
+ policy: OnyxEntry;
};
-function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}: DistanceRequestFooterProps) {
+function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPage, policy}: DistanceRequestFooterProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+ const activePolicy = usePolicy(activePolicyID);
+ const [mapboxAccessToken] = useOnyx(ONYXKEYS.MAPBOX_ACCESS_TOKEN);
const numberOfWaypoints = Object.keys(waypoints ?? {}).length;
const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => waypoint?.address).length;
const lastWaypointIndex = numberOfWaypoints - 1;
+ const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy);
+ const policyCurrency = (policy ?? activePolicy)?.outputCurrency ?? getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD;
+ const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
+ const {unit} = mileageRate ?? {};
const getMarkerComponent = useCallback(
(icon: IconAsset): ReactNode => (
@@ -66,7 +74,7 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
return;
}
- const index = TransactionUtils.getWaypointIndex(key);
+ const index = getWaypointIndex(key);
let MarkerComponent: IconAsset;
if (index === 0) {
MarkerComponent = Expensicons.DotIndicatorUnfilled;
@@ -114,6 +122,8 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
waypoints={waypointMarkers}
styleURL={CONST.MAPBOX.STYLE_URL}
overlayStyle={styles.mapEditView}
+ distanceInMeters={getDistanceInMeters(transaction, undefined)}
+ unit={unit}
/>
>
@@ -122,8 +132,4 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
DistanceRequestFooter.displayName = 'DistanceRequestFooter';
-export default withOnyx({
- mapboxAccessToken: {
- key: ONYXKEYS.MAPBOX_ACCESS_TOKEN,
- },
-})(DistanceRequestFooter);
+export default DistanceRequestFooter;
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index ae7ea7f09503..69e542e44798 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -23,6 +23,7 @@ function EmptyStateComponent({
title,
titleStyles,
subtitle,
+ children,
headerStyles,
headerContentStyles,
lottieWebViewStyles,
@@ -99,7 +100,8 @@ function EmptyStateComponent({
{HeaderComponent}{title}
- {typeof subtitle === 'string' ? {subtitle} : subtitle}
+ {subtitle}
+ {children}
{buttons?.map(({buttonText, buttonAction, success, icon, isDisabled}, index) => (
= {
SkeletonComponent: ValidSkeletons;
title: string;
titleStyles?: StyleProp;
- subtitle: string | React.ReactNode;
+ subtitle?: string;
+ children?: React.ReactNode;
buttons?: Button[];
containerStyles?: StyleProp;
headerStyles?: StyleProp;
diff --git a/src/components/FeatureList.tsx b/src/components/FeatureList.tsx
index 70c56ad5d963..179eee2d5810 100644
--- a/src/components/FeatureList.tsx
+++ b/src/components/FeatureList.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {ReactNode} from 'react';
import {View} from 'react-native';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
@@ -7,7 +8,6 @@ import variables from '@styles/variables';
import type {TranslationPaths} from '@src/languages/types';
import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
-import DotIndicatorMessage from './DotIndicatorMessage';
import type DotLottieAnimation from './LottieAnimations/types';
import MenuItem from './MenuItem';
import Section from './Section';
@@ -33,15 +33,6 @@ type FeatureListProps = {
/** Action to call on cta button press */
onCtaPress?: () => void;
- /** Text of the secondary button button */
- secondaryButtonText?: string;
-
- /** Accessibility label for the secondary button */
- secondaryButtonAccessibilityLabel?: string;
-
- /** Action to call on secondary button press */
- onSecondaryButtonPress?: () => void;
-
/** A list of menuItems representing the feature list. */
menuItems: FeatureListItem[];
@@ -60,23 +51,19 @@ type FeatureListProps = {
/** The style used for the title */
titleStyles?: StyleProp;
- /** The error message to display for the CTA button */
- ctaErrorMessage?: string;
-
/** Padding for content on large screens */
contentPaddingOnLargeScreens?: {padding: number};
+
+ /** Custom content to display in the footer */
+ footer?: ReactNode;
};
function FeatureList({
title,
subtitle = '',
- ctaText = '',
- ctaAccessibilityLabel = '',
- onCtaPress = () => {},
- secondaryButtonText = '',
- secondaryButtonAccessibilityLabel = '',
- onSecondaryButtonPress = () => {},
- ctaErrorMessage,
+ ctaText,
+ ctaAccessibilityLabel,
+ onCtaPress,
menuItems,
illustration,
illustrationStyle,
@@ -84,6 +71,7 @@ function FeatureList({
illustrationContainerStyle,
titleStyles,
contentPaddingOnLargeScreens,
+ footer,
}: FeatureListProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -122,30 +110,17 @@ function FeatureList({
))}
- {!!secondaryButtonText && (
+ {!!ctaText && (
)}
- {!!ctaErrorMessage && (
-
- )}
-
+ {!!footer && footer}
);
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
index 332255e53995..12b515194928 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
@@ -80,7 +80,6 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}),
'mention-report': HTMLElementModel.fromCustomModel({tagName: 'mention-report', contentModel: HTMLContentModel.textual}),
'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}),
- 'custom-emoji': HTMLElementModel.fromCustomModel({tagName: 'custom-emoji', contentModel: HTMLContentModel.textual}),
'next-step': HTMLElementModel.fromCustomModel({
tagName: 'next-step',
mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16},
diff --git a/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx
deleted file mode 100644
index 8cd33eab6c90..000000000000
--- a/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type {ReactNode} from 'react';
-import React from 'react';
-import FloatingActionButtonAndPopover from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover';
-
-type CustomEmojiWithDefaultPressableActionProps = {
- /* Key name identifying the emoji */
- emojiKey: string;
-
- /* Emoji content to render */
- children: ReactNode;
-};
-
-function CustomEmojiWithDefaultPressableAction({emojiKey, children}: CustomEmojiWithDefaultPressableActionProps) {
- if (emojiKey === 'actionMenuIcon') {
- return {children};
- }
-
- return children;
-}
-
-export default CustomEmojiWithDefaultPressableAction;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.tsx
index a319507bc3ad..6350fe0556da 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.tsx
@@ -28,12 +28,6 @@ function CodeRenderer({TDefaultRenderer, key, style, ...defaultRendererProps}: C
const textStyleOverride = {
fontSize,
fontFamily: font,
-
- // We need to override this properties bellow that was defined in `textStyle`
- // Because by default the `react-native-render-html` add a style in the elements,
- // for example the tag has a fontWeight: "bold" and in the android it break the font
- fontWeight: undefined,
- fontStyle: undefined,
};
return (
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx
deleted file mode 100644
index dab8c89013dd..000000000000
--- a/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import type {FC} from 'react';
-import {View} from 'react-native';
-import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
-import type {SvgProps} from 'react-native-svg';
-import GlobalCreateIcon from '@assets/images/customEmoji/global-create.svg';
-import CustomEmojiWithDefaultPressableAction from '@components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction';
-import ImageSVG from '@components/ImageSVG';
-import useThemeStyles from '@hooks/useThemeStyles';
-import variables from '@styles/variables';
-
-const emojiMap: Record> = {
- actionMenuIcon: GlobalCreateIcon,
-};
-
-function CustomEmojiRenderer({tnode}: CustomRendererProps) {
- const styles = useThemeStyles();
- const emojiKey = tnode.attributes.emoji;
-
- if (emojiMap[emojiKey]) {
- const image = (
-
-
-
- );
-
- if ('pressablewithdefaultaction' in tnode.attributes) {
- return {image};
- }
-
- return image;
- }
-
- return null;
-}
-
-export default CustomEmojiRenderer;
-export {emojiMap};
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
index bcf3d4dfaf94..91ed66f8b931 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
@@ -1,7 +1,6 @@
import type {CustomTagRendererRecord} from 'react-native-render-html';
import AnchorRenderer from './AnchorRenderer';
import CodeRenderer from './CodeRenderer';
-import CustomEmojiRenderer from './CustomEmojiRenderer';
import DeletedActionRenderer from './DeletedActionRenderer';
import EditedRenderer from './EditedRenderer';
import EmojiRenderer from './EmojiRenderer';
@@ -30,7 +29,6 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
'mention-user': MentionUserRenderer,
'mention-report': MentionReportRenderer,
'mention-here': MentionHereRenderer,
- 'custom-emoji': CustomEmojiRenderer,
emoji: EmojiRenderer,
'next-step-email': NextStepEmailRenderer,
'deleted-action': DeletedActionRenderer,
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 5cfa87d472da..b71c9e2402c5 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -88,6 +88,7 @@ import ExpensifyFooterLogoVertical from '@assets/images/expensify-footer-logo-ve
import ExpensifyFooterLogo from '@assets/images/expensify-footer-logo.svg';
import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg';
import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg';
+import Export from '@assets/images/export.svg';
import EyeDisabled from '@assets/images/eye-disabled.svg';
import Eye from '@assets/images/eye.svg';
import Feed from '@assets/images/feed.svg';
@@ -113,6 +114,7 @@ import ImageCropSquareMask from '@assets/images/image-crop-square-mask.svg';
import Inbox from '@assets/images/inbox.svg';
import Info from '@assets/images/info.svg';
import NetSuiteSquare from '@assets/images/integrationicons/netsuite-icon-square.svg';
+import NSQSSquare from '@assets/images/integrationicons/netsuite-quickstart-icon-square.svg';
import QBDSquare from '@assets/images/integrationicons/qbd-icon-square.svg';
import QBOCircle from '@assets/images/integrationicons/qbo-icon-circle.svg';
import QBOSquare from '@assets/images/integrationicons/qbo-icon-square.svg';
@@ -284,6 +286,7 @@ export {
ExpensifyFooterLogo,
ExpensifyFooterLogoVertical,
Expand,
+ Export,
Eye,
EyeDisabled,
FallbackAvatar,
@@ -406,6 +409,7 @@ export {
CheckCircle,
CheckmarkCircle,
NetSuiteSquare,
+ NSQSSquare,
XeroCircle,
QBOCircle,
Filters,
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index e48646204f34..841fb55380e2 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -14,7 +14,6 @@ import TextBlock from '@components/TextBlock';
import useLHNEstimatedListSize from '@hooks/useLHNEstimatedListSize';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {isValidDraftComment} from '@libs/DraftCommentUtils';
@@ -48,9 +47,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const theme = useTheme();
const styles = useThemeStyles();
const {translate, preferredLocale} = useLocalize();
- const {shouldUseNarrowLayout} = useResponsiveLayout();
const estimatedListSize = useLHNEstimatedListSize();
- const shouldShowEmptyLHN = shouldUseNarrowLayout && data.length === 0;
+ const shouldShowEmptyLHN = data.length === 0;
// When the first item renders we want to call the onFirstItemRendered callback.
// At this point in time we know that the list is actually displaying items.
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index 6f1c7aaee458..ac202c1dc6a0 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -6,9 +6,12 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
+import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as UserLocation from '@libs/actions/UserLocation';
+import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation';
+import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import getCurrentPosition from '@libs/getCurrentPosition';
import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types';
@@ -24,7 +27,7 @@ import responder from './responder';
import utils from './utils';
const MapView = forwardRef(
- ({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true}, ref) => {
+ ({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true, distanceInMeters, unit}, ref) => {
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
const navigation = useNavigation();
const {isOffline} = useNetwork();
@@ -39,6 +42,25 @@ const MapView = forwardRef(
const shouldInitializeCurrentPosition = useRef(true);
const [isAccessTokenSet, setIsAccessTokenSet] = useState(false);
+ const [distanceUnit, setDistanceUnit] = useState(unit);
+ useEffect(() => {
+ if (!unit || distanceUnit) {
+ return;
+ }
+ setDistanceUnit(unit);
+ }, [unit, distanceUnit]);
+
+ const toggleDistanceUnit = useCallback(() => {
+ setDistanceUnit((currentUnit) =>
+ currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
+ );
+ }, []);
+
+ const distanceLabelText = useMemo(
+ () => DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters ?? 0, distanceUnit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS),
+ [distanceInMeters, distanceUnit],
+ );
+
// Determines if map can be panned to user's detected
// location without bothering the user. It will return
// false if user has already started dragging the map or
@@ -50,7 +72,7 @@ const MapView = forwardRef(
if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
return;
}
- UserLocation.clearUserLocation();
+ clearUserLocation();
},
[initialLocation],
);
@@ -74,7 +96,7 @@ const MapView = forwardRef(
getCurrentPosition((params) => {
const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
- UserLocation.setUserLocation(currentCoords);
+ setUserLocation(currentCoords);
}, setCurrentPositionToInitialState);
}, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]),
);
@@ -205,6 +227,20 @@ const MapView = forwardRef(
const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]);
const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]);
+ const distanceSymbolCoorinate = useMemo(() => {
+ const length = directionCoordinates?.length;
+ // If the array is empty, return undefined
+ if (!length) {
+ return undefined;
+ }
+
+ // Find the index of the middle element
+ const middleIndex = Math.floor(length / 2);
+
+ // Return the middle element
+ return directionCoordinates.at(middleIndex);
+ }, [directionCoordinates]);
+
return !isOffline && isAccessTokenSet && !!defaultSettings ? (
(
/>
)}
-
{waypoints?.map(({coordinate, markerComponent, id}) => {
const MarkerComponent = markerComponent;
if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) {
@@ -276,6 +311,25 @@ const MapView = forwardRef(
})}
{!!directionCoordinates && }
+ {!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && (
+
+
+
+
+ {distanceLabelText}
+
+
+
+
+ )}
{interactive && (
diff --git a/src/components/MapView/MapViewImpl.website.tsx b/src/components/MapView/MapViewImpl.website.tsx
index c71ce6fb237b..4fdf71252895 100644
--- a/src/components/MapView/MapViewImpl.website.tsx
+++ b/src/components/MapView/MapViewImpl.website.tsx
@@ -12,10 +12,12 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithoutFeedback} from '@components/Pressable';
import usePrevious from '@hooks/usePrevious';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {clearUserLocation, setUserLocation} from '@userActions/UserLocation';
@@ -42,6 +44,8 @@ const MapViewImpl = forwardRef(
directionCoordinates,
initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM},
interactive = true,
+ distanceInMeters,
+ unit,
},
ref,
) => {
@@ -49,6 +53,19 @@ const MapViewImpl = forwardRef(
const {isOffline} = useNetwork();
const {translate} = useLocalize();
+ const [distanceUnit, setDistanceUnit] = useState(unit);
+ useEffect(() => {
+ if (!unit || distanceUnit) {
+ return;
+ }
+ setDistanceUnit(unit);
+ }, [unit, distanceUnit]);
+
+ const toggleDistanceUnit = useCallback(() => {
+ setDistanceUnit((currentUnit) =>
+ currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
+ );
+ }, []);
const theme = useTheme();
const styles = useThemeStyles();
@@ -232,6 +249,20 @@ const MapViewImpl = forwardRef(
};
}, [waypoints, directionCoordinates, interactive, currentPosition, initialState.zoom]);
+ const distanceSymbolCoorinate = useMemo(() => {
+ const length = directionCoordinates?.length;
+ // If the array is empty, return undefined
+ if (!length) {
+ return undefined;
+ }
+
+ // Find the index of the middle element
+ const middleIndex = Math.floor(length / 2);
+
+ // Return the middle element
+ return directionCoordinates.at(middleIndex);
+ }, [directionCoordinates]);
+
return !isOffline && !!accessToken && !!initialViewState ? (
(
)}
+ {!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && (
+
+
+
+ {DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters, distanceUnit)}
+
+
+
+ )}
{waypoints?.map(({coordinate, markerComponent, id}) => {
const MarkerComponent = markerComponent;
if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) {
diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts
index 41170694c9d2..11e90a78fffc 100644
--- a/src/components/MapView/MapViewTypes.ts
+++ b/src/components/MapView/MapViewTypes.ts
@@ -1,5 +1,6 @@
import type {ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
+import type {Unit} from '@src/types/onyx/Policy';
type MapViewProps = {
// Public access token to be used to fetch map data from Mapbox.
@@ -22,6 +23,12 @@ type MapViewProps = {
onMapReady?: () => void;
// Whether the map is interactable or not
interactive?: boolean;
+
+ // Distance displayed on the map in meters.
+ distanceInMeters?: number;
+
+ // Unit of measurement for distance
+ unit?: Unit;
};
type DirectionProps = {
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 40ec431ca893..32f9f7d5a827 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,5 +1,5 @@
import type {ImageContentFit} from 'expo-image';
-import type {ReactElement, ReactNode} from 'react';
+import type {ReactElement, ReactNode, Ref} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
@@ -60,6 +60,10 @@ type NoIcon = {
};
type MenuItemBaseProps = {
+ /* View ref */
+ /* eslint-disable-next-line react/no-unused-prop-types */
+ ref?: Ref;
+
/** Function to fire when component is pressed */
onPress?: (event: GestureResponderEvent | KeyboardEvent) => void | Promise;
diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx
index b2d79b6243ac..21fd73e7353d 100644
--- a/src/components/MenuItemList.tsx
+++ b/src/components/MenuItemList.tsx
@@ -1,6 +1,7 @@
import React, {useRef} from 'react';
import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
import useSingleExecution from '@hooks/useSingleExecution';
+import mergeRefs from '@libs/mergeRefs';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
@@ -70,32 +71,32 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false, wrapper
};
return (
- <>
- {menuItems.map(({key, ...menuItemProps}) => (
- (
+
+
- ))}
- >
+ wrapperStyle={wrapperStyle}
+ onSecondaryInteraction={menuItemProps.link !== undefined ? (e) => secondaryInteraction(menuItemProps.link, e) : undefined}
+ ref={mergeRefs(ref, popoverAnchor)}
+ shouldBlockSelection={!!menuItemProps.link}
+ icon={icon}
+ iconWidth={iconWidth}
+ iconHeight={iconHeight}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...menuItemProps}
+ disabled={!!menuItemProps.disabled || isExecuting}
+ onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress}
+ />
+
+ ))
);
}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 4536b18217a2..5b3877050e7a 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -264,7 +264,7 @@ function MoneyRequestConfirmationList({
const policyTagLists = useMemo(() => getTagLists(policyTags), [policyTags]);
- const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest) && !isPerDiemRequest;
+ const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
const previousTransactionAmount = usePrevious(transaction?.amount);
const previousTransactionCurrency = usePrevious(transaction?.currency);
diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx
index 4fa58ac21ffa..a07f2bd04f7e 100644
--- a/src/components/PopoverWithMeasuredContent.tsx
+++ b/src/components/PopoverWithMeasuredContent.tsx
@@ -2,6 +2,7 @@ import isEqual from 'lodash/isEqual';
import React, {useMemo, useState} from 'react';
import type {LayoutChangeEvent} from 'react-native';
import {View} from 'react-native';
+import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import ComposerFocusManager from '@libs/ComposerFocusManager';
@@ -66,23 +67,12 @@ function PopoverWithMeasuredContent({
const [popoverWidth, setPopoverWidth] = useState(popoverDimensions.width);
const [popoverHeight, setPopoverHeight] = useState(popoverDimensions.height);
const [isContentMeasured, setIsContentMeasured] = useState(popoverWidth > 0 && popoverHeight > 0);
- const [isPopoverVisible, setIsPopoverVisible] = useState(false);
+ const prevIsVisible = usePrevious(isVisible);
const modalId = useMemo(() => ComposerFocusManager.getId(), []);
- /**
- * When Popover becomes visible, we need to recalculate the Dimensions.
- * Skip render on Popover until recalculations are done by setting isContentMeasured to false as early as possible.
- */
- if (!isPopoverVisible && isVisible) {
- if (shouldEnableNewFocusManagement) {
- ComposerFocusManager.saveFocusState(modalId);
- }
- // When Popover is shown recalculate
- setIsContentMeasured(popoverDimensions.width > 0 && popoverDimensions.height > 0);
- setIsPopoverVisible(true);
- } else if (isPopoverVisible && !isVisible) {
- setIsPopoverVisible(false);
+ if (!prevIsVisible && isVisible && shouldEnableNewFocusManagement) {
+ ComposerFocusManager.saveFocusState(modalId);
}
/**
@@ -147,7 +137,7 @@ function PopoverWithMeasuredContent({
return isContentMeasured ? (
PersonalDetailsUtils.getPersonalDetailsByIDs(accountIDs, currentUserPersonalDetails.accountID, true), [currentUserPersonalDetails.accountID, accountIDs]);
+ const users = useMemo(
+ () => PersonalDetailsUtils.getPersonalDetailsByIDs({accountIDs, currentUserAccountID: currentUserPersonalDetails.accountID, shouldChangeUserDisplayName: true}),
+ [currentUserPersonalDetails.accountID, accountIDs],
+ );
const namesString = users
.map((user) => user?.displayName)
diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx
index dcf657137d58..f0e22c9a731b 100644
--- a/src/components/ReportActionItem/IssueCardMessage.tsx
+++ b/src/components/ReportActionItem/IssueCardMessage.tsx
@@ -56,7 +56,9 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
return (
<>
- ${ReportActionsUtils.getCardIssuedMessage(action, true, policyID, !!card)}`} />
+ ${ReportActionsUtils.getCardIssuedMessage({reportAction: action, shouldRenderHTML: true, policyID, shouldDisplayLinkToCard: !!card})}`}
+ />
{shouldShowAddMissingDetailsButton && (