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/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/deploy.yml b/.github/workflows/deploy.yml index 6ca2f0f8a698..3b84fbc6a6e4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 @@ -373,25 +376,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 +506,11 @@ 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 - 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/testBuild.yml b/.github/workflows/testBuild.yml index 869db3d04be7..ea62bca794fe 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: Install 1Password CLI + uses: 1password/install-cli-action@v1 - - 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 + - name: Load files from 1Password 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 }} + 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 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/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..27205512dbcf 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 1009009407 + versionName "9.0.94-7" // 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/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/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/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/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/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/Info.plist b/ios/NewExpensify/Info.plist index 44b35c2d63fe..55087defb4c0 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.94.1 + 9.0.94.7 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index acfce5c2e675..7fa6784a30e6 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.94.1 + 9.0.94.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2e661cccef7f..d51f8fd912c0 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.94 CFBundleVersion - 9.0.94.1 + 9.0.94.7 NSExtension NSExtensionPointIdentifier diff --git a/ios/ios-fastlane-json-key.json.gpg b/ios/ios-fastlane-json-key.json.gpg deleted file mode 100644 index 06d2109da080..000000000000 --- a/ios/ios-fastlane-json-key.json.gpg +++ /dev/null @@ -1,2 +0,0 @@ -  H46 )E$R `LIu0؄<;\ՠFIι{[3EįL?ʼ-V6vW6}뾆ck)>O##E:AAx|FQe"6Q Є7YQ+pc8~ǓbDYRA!\T `-yGy>IGU ejYC< H}?9J`2 T -,a>"J(,}v;F>i9ѠaQrN;mMM_D3L͖sVDa1nh9ɍX9 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9087c78c68b9..db7a86b0b7ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.94-1", + "version": "9.0.94-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.94-1", + "version": "9.0.94-7", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -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", @@ -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 e250de3bce5a..b5dccb4c03d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.94-1", + "version": "9.0.94-7", "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.", @@ -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 d642c4586d19..c77166e6cbd7 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', @@ -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' + @@ -6573,6 +6626,8 @@ const CONST = { EXPENSE: 'EXPENSE', INVOICE: 'INVOICE', }, + 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/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index fc5c77958635..75f9ac985966 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -38,7 +38,7 @@ function ArchivedReportFooter({report}: ArchivedReportFooterProps) { const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT && archiveReason !== CONST.REPORT.ARCHIVE_REASON.BOOKING_END_DATE_HAS_PASSED; - let policyName = ReportUtils.getPolicyName(report); + let policyName = ReportUtils.getPolicyName({report}); if (archiveReason === CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED) { policyName = originalMessage?.receiverPolicyName ?? ''; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 98e6dd626883..fec3fa6fa6fd 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -71,7 +71,7 @@ function AvatarWithDisplayName({ const [invoiceReceiverPolicy] = useOnyx( `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, ); - const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); + const title = ReportUtils.getReportName(report, invoiceReceiverPolicy); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); const isMoneyRequestOrReport = diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index ad1a659e6d9f..7c4ce77c8a99 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import useLocalize from '@hooks/useLocalize'; @@ -43,6 +44,12 @@ type FullPageNotFoundViewProps = { /** Whether we should force the full page view */ shouldForceFullScreen?: boolean; + + /** The style of the subtitle message */ + subtitleStyle?: StyleProp; + + /** 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/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/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/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..da402f612a2c 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -113,6 +113,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'; @@ -406,6 +407,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/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}) => ( - ( + + - secondaryInteraction(menuItemProps.link, e) : undefined} - 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} - /> - - ))} - + 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/Reactions/ReactionTooltipContent.tsx b/src/components/Reactions/ReactionTooltipContent.tsx index 8f469b01272c..59197fa7984d 100644 --- a/src/components/Reactions/ReactionTooltipContent.tsx +++ b/src/components/Reactions/ReactionTooltipContent.tsx @@ -26,7 +26,10 @@ type ReactionTooltipContentProps = Pick 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 && (