diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml
index b0b2d07f990d..29dddbcd3151 100644
--- a/.github/workflows/createNewVersion.yml
+++ b/.github/workflows/createNewVersion.yml
@@ -106,9 +106,6 @@ jobs:
runs-on: macos-latest
needs: [validateActor, createNewVersion]
if: ${{ fromJSON(needs.validateActor.outputs.HAS_WRITE_ACCESS) }}
- defaults:
- run:
- working-directory: Mobile-Expensify
steps:
- name: Run turnstyle
uses: softprops/turnstyle@49108bdfa571e62371bd2c3094893c547ab3fc03
@@ -121,22 +118,17 @@ jobs:
uses: actions/checkout@v4
with:
ref: main
+ submodules: true
# The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify
# This is a workaround to allow pushes to a protected branch
token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }}
- - name: Check out `Mobile-Expensify` repo
- uses: actions/checkout@v4
- with:
- repository: 'Expensify/Mobile-Expensify'
- submodules: true
- path: 'Mobile-Expensify'
- token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }}
-
- - name: Update submodule
+ - name: Update submodule and checkout the main branch
run: |
- cd react-native
git submodule update --init
+ cd Mobile-Expensify
+ git checkout main
+ git pull origin main
- name: Setup git for OSBotify
uses: ./.github/actions/composite/setupGitForOSBotify
@@ -146,6 +138,7 @@ jobs:
- name: Generate HybridApp version
run: |
+ cd Mobile-Expensify
# Generate all flavors of the version
SHORT_APP_VERSION=$(echo "$NEW_VERSION" | awk -F'-' '{print $1}')
BUILD_NUMBER=$(echo "$NEW_VERSION" | awk -F'-' '{print $2}')
@@ -178,6 +171,7 @@ jobs:
- name: Commit new version
run: |
+ cd Mobile-Expensify
git add \
./Android/AndroidManifest.xml \
./app/config/config.json \
@@ -186,8 +180,14 @@ jobs:
./iOS/NotificationServiceExtension/Info.plist
git commit -m "Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}"
- - name: Update main branch
- run: git push origin main
+ - name: Update main branch on Mobile-Expensify and App
+ run: |
+ cd Mobile-Expensify
+ git push origin main
+ cd ..
+ git add Mobile-Expensify
+ git commit -m "Update Mobile-Expensify to ${{ needs.createNewVersion.outputs.NEW_VERSION }}"
+ git push origin main
- name: Announce failed workflow in Slack
if: ${{ failure() }}
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index d58a81c8d80a..21a8fa73289a 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -183,6 +183,11 @@ jobs:
id: setup-node
uses: ./.github/actions/composite/setupNode
+ - name: Run grunt build
+ run: |
+ cd Mobile-Expensify
+ npm run grunt:build:shared
+
- name: Setup Java
uses: actions/setup-java@v4
with:
@@ -222,7 +227,7 @@ jobs:
- name: Get Android native version
id: getAndroidVersion
- run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' ../Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT"
+ run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT"
- name: Build Android app
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
@@ -239,10 +244,11 @@ jobs:
VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
- name: Get current Android rollout percentage
+ if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
id: getAndroidRolloutPercentage
uses: ./.github/actions/javascript/getAndroidRolloutPercentage
with:
- GOOGLE_KEY_FILE: Mobile-Expensify/react-native/android-fastlane-json-key.json
+ GOOGLE_KEY_FILE: ./android-fastlane-json-key.json
PACKAGE_NAME: org.me.mobiexpensifyg
- name: Submit production build for Google Play review and a slow rollout
@@ -507,12 +513,12 @@ jobs:
uses: actions/cache@v4
id: pods-cache
with:
- path: Mobile-Expensify/ios/Pods
- key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/ios/Podfile.lock', 'firebase.json') }}
+ path: Mobile-Expensify/iOS/Pods
+ key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }}
- name: Compare Podfile.lock and Manifest.lock
id: compare-podfile-and-manifest
- run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/ios/Podfile.lock') == hashFiles('Mobile-Expensify/ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
+ run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
- name: Install cocoapods
uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml
index 1e79bceb403a..42a5f15f8910 100644
--- a/.github/workflows/testBuildHybrid.yml
+++ b/.github/workflows/testBuildHybrid.yml
@@ -9,7 +9,6 @@ on:
OLD_DOT_COMMIT:
description: The branch, tag or SHA to checkout on Old Dot side
required: false
- default: 'main'
pull_request_target:
types: [opened, synchronize, labeled]
branches: ['*ci-test/**']
@@ -86,38 +85,41 @@ jobs:
androidHybrid:
name: Build Android HybridApp
needs: [validateActor, getBranchRef]
+ if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }}
runs-on: ubuntu-latest-xl
- defaults:
- run:
- working-directory: Mobile-Expensify/react-native
outputs:
S3_APK_PATH: ${{ steps.exportAndroidS3Path.outputs.S3_APK_PATH }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
- repository: 'Expensify/Mobile-Expensify'
submodules: true
- path: 'Mobile-Expensify'
- ref: ${{ env.OLD_DOT_COMMIT }}
+ ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }}
token: ${{ secrets.OS_BOTIFY_TOKEN }}
# fetch-depth: 0 is required in order to fetch the correct submodule branch
fetch-depth: 0
- - name: Update submodule
+ - name: Update submodule to match main
+ env:
+ OLD_DOT_COMMIT: ${{ env.OLD_DOT_COMMIT }}
run: |
- git submodule update --init
- git fetch
- git checkout ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }}
+ git submodule update --init --remote
+ if [[ -z "$OLD_DOT_COMMIT" ]]; then
+ git fetch
+ git checkout ${{ env.OLD_DOT_COMMIT }}
+ fi
- name: Configure MapBox SDK
run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
- - uses: actions/setup-node@v4
- with:
- node-version-file: 'Mobile-Expensify/react-native/.nvmrc'
- cache: npm
- cache-dependency-path: 'Mobile-Expensify/react-native'
+ - name: Setup Node
+ id: setup-node
+ uses: ./.github/actions/composite/setupNode
+
+ - name: Run grunt build
+ run: |
+ cd Mobile-Expensify
+ npm run grunt:build:shared
- name: Setup dotenv
run: |
@@ -125,14 +127,6 @@ jobs:
sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc
- - name: Install node modules
- run: |
- npm install
- cd .. && npm install
-
- # Fixes https://github.com/Expensify/App/issues/51682
- npm run grunt:build:shared
-
- name: Setup Java
uses: actions/setup-java@v4
with:
@@ -143,7 +137,6 @@ jobs:
uses: ruby/setup-ruby@v1.190.0
with:
bundler-cache: true
- working-directory: 'Mobile-Expensify/react-native'
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
@@ -155,7 +148,7 @@ jobs:
op document get --output ./upload-key.keystore upload-key.keystore
op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json
# Copy the keystore to the Android directory for Fullstory
- cp ./upload-key.keystore ../Android
+ cp ./upload-key.keystore Mobile-Expensify/Android
- name: Load Android upload keystore credentials from 1Password
id: load-credentials
@@ -168,10 +161,6 @@ jobs:
ANDROID_UPLOAD_KEYSTORE_ALIAS: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS
ANDROID_UPLOAD_KEY_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD
- - name: Get Android native version
- id: getAndroidVersion
- run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT"
-
- name: Build Android app
id: build
env:
@@ -200,11 +189,120 @@ jobs:
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]
+ if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }}
+ env:
+ DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer
+ runs-on: macos-13-xlarge
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: true
+ ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }}
+ token: ${{ secrets.OS_BOTIFY_TOKEN }}
+ # fetch-depth: 0 is required in order to fetch the correct submodule branch
+ fetch-depth: 0
+
+ - name: Update submodule to match main
+ env:
+ OLD_DOT_COMMIT: ${{ env.OLD_DOT_COMMIT }}
+ run: |
+ git submodule update --init --remote
+ if [[ -z "$OLD_DOT_COMMIT" ]]; then
+ git fetch
+ git checkout ${{ env.OLD_DOT_COMMIT }}
+ fi
+
+ - 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
+
+ - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it
+ run: |
+ cp .env.staging .env.adhoc
+ sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
+ echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1.190.0
+ with:
+ bundler-cache: true
+
+ - name: Install New Expensify Gems
+ run: bundle install
+
+ - name: Cache Pod dependencies
+ uses: actions/cache@v4
+ id: pods-cache
+ with:
+ path: Mobile-Expensify/iOS/Pods
+ key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }}
+
+ - name: Compare Podfile.lock and Manifest.lock
+ id: compare-podfile-and-manifest
+ run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
+
+ - name: Install cocoapods
+ uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
+ if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true'
+ with:
+ timeout_minutes: 10
+ max_attempts: 5
+ command: npm run pod-install
+
+ - name: Install 1Password CLI
+ uses: 1password/install-cli-action@v1
+
+ - name: Load files from 1Password
+ env:
+ OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+ run: |
+ op document get --output ./OldApp_AdHoc.mobileprovision OldApp_AdHoc
+ op document get --output ./OldApp_AdHoc_Share_Extension.mobileprovision OldApp_AdHoc_Share_Extension
+ op document get --output ./OldApp_AdHoc_Notification_Service.mobileprovision OldApp_AdHoc_Notification_Service
+
+ - 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: Build AdHoc app
+ run: bundle exec fastlane ios 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 AdHoc build to S3
+ run: bundle exec fastlane ios 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
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ios
+ path: ./ios_paths.json
+
+
postGithubComment:
runs-on: ubuntu-latest
name: Post a GitHub comment with app download links for testing
- needs: [validateActor, getBranchRef, androidHybrid]
+ needs: [validateActor, getBranchRef, androidHybrid, iosHybrid]
if: ${{ always() }}
steps:
- name: Checkout
@@ -217,6 +315,17 @@ jobs:
uses: actions/download-artifact@v4
if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }}
+ - name: Read JSONs with iOS paths
+ id: get_ios_path
+ if: ${{ needs.iosHybrid.result == 'success' }}
+ run: |
+ content_ios="$(cat ./ios/ios_paths.json)"
+ content_ios="${content_ios//'%'/'%25'}"
+ content_ios="${content_ios//$'\n'/'%0A'}"
+ content_ios="${content_ios//$'\r'/'%0D'}"
+ ios_path=$(echo "$content_ios" | jq -r '.html_path')
+ echo "ios_path=$ios_path" >> "$GITHUB_OUTPUT"
+
- name: Publish links to apps for download
if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }}
uses: ./.github/actions/javascript/postTestBuildComment
@@ -224,4 +333,6 @@ jobs:
PR_NUMBER: ${{ env.PULL_REQUEST_NUMBER }}
GITHUB_TOKEN: ${{ github.token }}
ANDROID: ${{ needs.androidHybrid.result }}
- ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }}
\ No newline at end of file
+ IOS: ${{ needs.iosHybrid.result }}
+ ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }}
+ IOS_LINK: ${{ steps.get_ios_path.outputs.ios_path }}
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 7df7a0a1002d..af549932c171 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 7df7a0a1002d7622fd8b9c59a5dbfcc39164e736
+Subproject commit af549932c17151a57655466e4912e038b21f501a
diff --git a/README.md b/README.md
index 77b9d509a74d..8f7161b7fe96 100644
--- a/README.md
+++ b/README.md
@@ -456,7 +456,7 @@ You can only build HybridApp if you have been granted access to [`Mobile-Expensi
## Getting started with HybridApp
1. If you haven't, please follow [these instructions](https://github.com/Expensify/App?tab=readme-ov-file#getting-started) to setup the NewDot local environment.
-2. Run `git submodule update --init` to download the `Mobile-Expensify` sourcecode.
+2. Run `git submodule update --init --progress` to download the `Mobile-Expensify` sourcecode.
- If you have access to `Mobile-Expensify` and the command fails with a https-related error add this to your `~/.gitconfig` file:
```
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 641aa6b5d62b..1391594d72a8 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 1009007401
- versionName "9.0.74-1"
+ versionCode 1009007506
+ versionName "9.0.75-6"
// 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/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md
index fd0a6ca59069..f49ac1ead30e 100644
--- a/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md
+++ b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md
@@ -1,7 +1,42 @@
---
title: Accelo Troubleshooting
-description: Accelo Troubleshooting
-order: 3
+description: Resources to help you solve issues with your Accelo integration.
---
-# Coming soon
+# Overview
+Most of the Accelo integration with Expensify is managed on the Accelo side. You will find their [help site](https://help.accelo.com/guides/integrations-guide/expensify/) helpful, especially the [FAQs](https://help.accelo.com/guides/integrations-guide/expensify/#faq).
+
+## Information sync between Expensify and Accelo
+The Accelo integration does a one-way sync, bringing expenses from Expensify into Accelo. When this happens, it transfers specific information from Expensify expenses to Accelo:
+
+| Expensify | Accelo |
+|---------------------|-----------------------|
+| Description | Title |
+| Date | Date Incurred |
+| Category | Type |
+| Tags | Against (relevant Project, Ticket or Retainer) |
+| Distance (mileage) | Quantity |
+| Hours (time expenses) | Quantity |
+| Amount | Purchase Price and Sale Price |
+| Reimbursable? | Reimbursable? |
+| Billable? | Billable? |
+| Receipt | Attachment |
+| Tax Rate | Tax Code |
+| Attendees | Submitted By |
+
+## Expense Status
+The status of your expense report in Expensify is also synced in Accelo.
+
+| Expensify Report Status | Accelo Expense Status |
+|-------------------------|-----------------------|
+| Open | Submitted |
+| Submitted | Submitted |
+| Approved | Approved |
+| Reimbursed | Approved |
+| Rejected | Declined |
+| Archived | Approved |
+| Closed | Approved |
+
+
+## Can I use an Accelo and an accounting integration in Expensify at the same time?
+Yes, you can use Accelo and an accounting system simultaneously. In order to update your Expensify tags with your Accelo Projects, Tickets, or Retainers, you will need to have a special switch enabled that allows you to have non-accounting tags alongside your accounting connection. Please contact Concierge to request that our support team enable the “Indirect Tag Uploads” switch for you.
diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
index 068e4dd5bca9..68bca5228913 100644
--- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
+++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md
@@ -36,6 +36,12 @@ The three options for the date your report will export with are:
- Submitted date: The date the employee submitted the report
- Exported date: The date you export the report to NetSuite
+## Accounting Method
+
+This dictates when reimbursable expenses will export, according to your preferred accounting method:
+- Accrual: Out of pocket expenses will export immediately when the report is final approved
+- Cash: Out of pocket expenses will export when paid via Expensify or marked as Reimbursed
+
## Export Settings for Reimbursable Expenses
**Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite.
diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md
index 1952ba9539cd..6f7292245f00 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md
@@ -6,7 +6,7 @@ description: Find expenses and export expense data to a CSV file
Expensify allows you to export expense data to a downloaded CSV file, which you can then import into your favorite spreadsheet tool for deeper analysis.
-##Search Expenses
+## Search Expenses
The first step to exporting and downloading expenses is finding the data you need.
@@ -15,7 +15,7 @@ The first step to exporting and downloading expenses is finding the data you nee
3. Select your Filters on the top right to filter by credit card used, coding, date range, keyword, expense value and a number of other useful criteria
4. Hit View Results to see all expenses that match your filters
- ##Download Expenses
+ ## Download Expenses
1. Select the checkbox to the left of the expenses or select all with the very top checkbox.
2. Click **# selected** at the top-right and select **Download**.
diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-1.png b/docs/assets/images/ExpensifyHelp-CreateExpense-1.png
new file mode 100644
index 000000000000..7b6459440d5e
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-1.png differ
diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-2.png b/docs/assets/images/ExpensifyHelp-CreateExpense-2.png
new file mode 100644
index 000000000000..65aaf8017a32
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-2.png differ
diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-3.png b/docs/assets/images/ExpensifyHelp-CreateExpense-3.png
new file mode 100644
index 000000000000..0173de29d68d
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-3.png differ
diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-4.png b/docs/assets/images/ExpensifyHelp-CreateExpense-4.png
new file mode 100644
index 000000000000..901d08f1771d
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-4.png differ
diff --git a/docs/assets/images/Tax Exempt - Classic.png b/docs/assets/images/Tax Exempt - Classic.png
new file mode 100644
index 000000000000..0987f5e4ca7d
Binary files /dev/null and b/docs/assets/images/Tax Exempt - Classic.png differ
diff --git a/docs/assets/images/Tax Exempt - New Expensify.png b/docs/assets/images/Tax Exempt - New Expensify.png
new file mode 100644
index 000000000000..9ff6673da6b3
Binary files /dev/null and b/docs/assets/images/Tax Exempt - New Expensify.png differ
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 9e0ba567ac48..798e328f73fa 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -88,9 +88,9 @@ platform :android do
desc "Generate AdHoc HybridApp apk"
lane :build_adhoc_hybrid do
- ENV["ENVFILE"]="../.env.adhoc.hybridapp"
+ ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp"
gradle(
- project_dir: '../Android',
+ project_dir: 'Mobile-Expensify/Android',
task: 'assembleAdhoc',
properties: {
"android.injected.signing.store.file" => './upload-key.keystore',
@@ -406,6 +406,42 @@ platform :ios do
setIOSBuildOutputsInEnv()
end
+ desc "Build an iOS HybridApp Adhoc build"
+ lane :build_adhoc_hybrid do
+ ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp"
+
+ setupIOSSigningCertificate()
+
+ install_provisioning_profile(
+ path: "./OldApp_AdHoc.mobileprovision"
+ )
+
+ install_provisioning_profile(
+ path: "./OldApp_AdHoc_Share_Extension.mobileprovision"
+ )
+
+ install_provisioning_profile(
+ path: "./OldApp_AdHoc_Notification_Service.mobileprovision"
+ )
+
+ build_app(
+ workspace: "Mobile-Expensify/iOS/Expensify.xcworkspace",
+ scheme: "Expensify",
+ output_name: "Expensify.ipa",
+ export_method: "app-store",
+ export_options: {
+ manageAppVersionAndBuildNumber: false,
+ provisioningProfiles: {
+ "com.expensify.expensifylite.adhoc" => "(OldApp) AppStore",
+ "com.expensify.expensifylite.adhoc.SmartScanExtension" => "(OldApp) AppStore: Share Extension",
+ "com.expensify.expensifylite.adhoc.NotificationServiceExtension" => "(OldApp) AppStore: Notification Service",
+ }
+ }
+ )
+
+ setIOSBuildOutputsInEnv()
+ end
+
desc "Build an unsigned iOS production build"
lane :build_unsigned do
ENV["ENVFILE"]=".env.production"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 11881aaa884c..74d34f52214b 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.74
+ 9.0.75
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.74.1
+ 9.0.75.6
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 9bd26e6f9d25..c594f105f833 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.74
+ 9.0.75
CFBundleSignature
????
CFBundleVersion
- 9.0.74.1
+ 9.0.75.6
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 6fdb96653e16..2b8181d88d5b 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.74
+ 9.0.75
CFBundleVersion
- 9.0.74.1
+ 9.0.75.6
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 18eba3d79c27..0db33e40e7fb 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1981,8 +1981,27 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-view-shot (3.8.0):
+ - react-native-view-shot (4.0.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
- React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- react-native-webview (13.8.6):
- DoubleConversion
- glog
@@ -3231,7 +3250,7 @@ SPEC CHECKSUMS:
react-native-quick-sqlite: 7c793c9f5834e756b336257a8d8b8239b7ceb451
react-native-release-profiler: 131ec5e4145d900b2be2a8d6641e2ce0dd784259
react-native-safe-area-context: 38fdd9b3c5561de7cabae64bd0cd2ce05d2768a1
- react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688
+ react-native-view-shot: 6bafd491eb295b5834e05c469a37ecbd796d5b22
react-native-webview: ad29375839c9aa0409ce8e8693291b42bdc067a4
React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d
React-NativeModulesApple: 7ff2e2cfb2e5fa5bdedcecf28ce37e696c6ef1e1
diff --git a/package-lock.json b/package-lock.json
index 00621db538e2..c9bc9ec2f28e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.74-1",
+ "version": "9.0.75-6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.74-1",
+ "version": "9.0.75-6",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -95,7 +95,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.82",
+ "react-native-onyx": "2.0.86",
"react-native-pager-view": "6.5.1",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -114,7 +114,7 @@
"react-native-svg": "15.9.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
- "react-native-view-shot": "3.8.0",
+ "react-native-view-shot": "4.0.0",
"react-native-vision-camera": "^4.6.1",
"react-native-web": "0.19.13",
"react-native-webview": "13.8.6",
@@ -32150,9 +32150,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "2.0.82",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.82.tgz",
- "integrity": "sha512-12+NgkC4fOeGu2J6s985NKUuLHP4aijBhpE6Us5IfVL+9dwxr/KqUVgV00OzXtYAABcWcpMC5PrvESqe8T5Iyw==",
+ "version": "2.0.86",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.86.tgz",
+ "integrity": "sha512-3pjyzlo8We4tSx/xf+4IRnBMcm5rk0E+aHBUSUxJ5jaFermx0SXZJlnvE5Emkw+iu0bXKkwea6zt2LhxD1JSsg==",
"license": "MIT",
"dependencies": {
"ascii-table": "0.0.9",
@@ -32454,7 +32454,9 @@
}
},
"node_modules/react-native-view-shot": {
- "version": "3.8.0",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.0.tgz",
+ "integrity": "sha512-e7wtfdm981DQVqkW+YE9mkemYarI0VZQ7PzRcHzQOmXlVrGKvNVD2MzRXOg+gK8msQIQ95QxATJKzG/QkQ9QHQ==",
"license": "MIT",
"dependencies": {
"html2canvas": "^1.4.1"
diff --git a/package.json b/package.json
index 6ac8fe17cb14..59602f900e14 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.74-1",
+ "version": "9.0.75-6",
"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.",
@@ -158,7 +158,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.82",
+ "react-native-onyx": "2.0.86",
"react-native-pager-view": "6.5.1",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -177,7 +177,7 @@
"react-native-svg": "15.9.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
- "react-native-view-shot": "3.8.0",
+ "react-native-view-shot": "4.0.0",
"react-native-vision-camera": "^4.6.1",
"react-native-web": "0.19.13",
"react-native-webview": "13.8.6",
diff --git a/react-native.config.js b/react-native.config.js
index a8c2436688e4..773375378acd 100644
--- a/react-native.config.js
+++ b/react-native.config.js
@@ -1,7 +1,10 @@
+const iosSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'ios' : 'ios';
+const androidSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'android' : 'android';
+
module.exports = {
project: {
- ios: {sourceDir: process.env.PROJECT_ROOT_PATH + 'ios'},
- android: {sourceDir: process.env.PROJECT_ROOT_PATH + 'android'},
+ ios: {sourceDir: iosSourceDir},
+ android: {sourceDir: androidSourceDir},
},
assets: ['./assets/fonts/native'],
};
diff --git a/src/App.tsx b/src/App.tsx
index 52904e0a06c4..cc824b78fa4c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -18,6 +18,7 @@ import KeyboardProvider from './components/KeyboardProvider';
import {LocaleContextProvider} from './components/LocaleContextProvider';
import OnyxProvider from './components/OnyxProvider';
import PopoverContextProvider from './components/PopoverProvider';
+import {ProductTrainingContextProvider} from './components/ProductTrainingContext';
import SafeArea from './components/SafeArea';
import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider';
import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext';
@@ -95,6 +96,7 @@ function App({url}: AppProps) {
VideoPopoverMenuContextProvider,
KeyboardProvider,
SearchRouterContextProvider,
+ ProductTrainingContextProvider,
]}
>
diff --git a/src/CONST.ts b/src/CONST.ts
index 2d38d26d8820..4fcc1cada6ff 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -344,11 +344,14 @@ const CONST = {
ANIMATION_GYROSCOPE_VALUE: 0.4,
ANIMATION_PAID_DURATION: 200,
ANIMATION_PAID_CHECKMARK_DELAY: 300,
+ ANIMATION_THUMBSUP_DURATION: 250,
+ ANIMATION_THUMBSUP_DELAY: 200,
ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000,
BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
SCREEN_TRANSITION_END_TIMEOUT: 1000,
ARROW_HIDE_DELAY: 3000,
MAX_IMAGE_CANVAS_AREA: 16777216,
+ CHUNK_LOAD_ERROR: 'ChunkLoadError',
API_ATTACHMENT_VALIDATIONS: {
// 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion
@@ -6404,6 +6407,13 @@ const CONST = {
},
MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal',
+
+ PRODUCT_TRAINING_TOOLTIP_NAMES: {
+ CONCEIRGE_LHN_GBR: 'conciergeLHNGBR',
+ RENAME_SAVED_SEARCH: 'renameSavedSearch',
+ QUICK_ACTION_BUTTON: 'quickActionButton',
+ WORKSAPCE_CHAT_CREATE: 'workspaceChatCreate',
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 45d636c0b1df..2e65b5f372b4 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -111,9 +111,6 @@ const ONYXKEYS = {
/** NVP keys */
- /** Boolean flag only true when first set */
- NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser',
-
/** This NVP contains list of at most 5 recent attendees */
NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees',
@@ -216,18 +213,9 @@ const ONYXKEYS = {
/** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */
NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd',
- /** The NVP containing all information related to educational tooltip in workspace chat */
- NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip',
-
/** The NVP containing the target url to navigate to when deleting a transaction */
NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL: 'nvp_deleteTransactionNavigateBackURL',
- /** Whether to show save search rename tooltip */
- SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP: 'shouldShowSavedSearchRenameTooltip',
-
- /** Whether to hide gbr tooltip */
- NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip',
-
/** Does this user have push notifications enabled for this device? */
PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',
@@ -876,7 +864,6 @@ type OnyxCollectionValuesMapping = {
type OnyxValuesMapping = {
[ONYXKEYS.ACCOUNT]: OnyxTypes.Account;
[ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string;
- [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean;
// NVP_ONBOARDING is an array for old users.
[ONYXKEYS.NVP_ONBOARDING]: Onboarding | [];
@@ -1017,9 +1004,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_BILLING_FUND_ID]: number;
[ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number;
[ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number;
- [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip;
[ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL]: string | undefined;
- [ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP]: boolean;
[ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[];
[ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string;
[ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx;
@@ -1027,7 +1012,6 @@ type OnyxValuesMapping = {
[ONYXKEYS.LAST_ROUTE]: string;
[ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined;
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
- [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean;
[ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record;
[ONYXKEYS.CONCIERGE_REPORT_ID]: string;
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx
index af77a20b4caa..fc5c77958635 100644
--- a/src/components/ArchivedReportFooter.tsx
+++ b/src/components/ArchivedReportFooter.tsx
@@ -1,7 +1,6 @@
import lodashEscape from 'lodash/escape';
import React from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {getCurrentUserAccountID} from '@libs/actions/Report';
@@ -10,26 +9,20 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx';
+import type {Report} from '@src/types/onyx';
import Banner from './Banner';
-type ArchivedReportFooterOnyxProps = {
- /** The reason this report was archived */
- reportClosedAction: OnyxEntry;
-
- /** Personal details of all users */
- personalDetails: OnyxEntry;
-};
-
-type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & {
+type ArchivedReportFooterProps = {
/** The archived report */
report: Report;
};
-function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}}: ArchivedReportFooterProps) {
+function ArchivedReportFooter({report}: ArchivedReportFooterProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {initialValue: {}});
+ const [reportClosedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, selector: ReportActionsUtils.getLastClosedReportAction});
const originalMessage = ReportActionsUtils.isClosedAction(reportClosedAction) ? ReportActionsUtils.getOriginalMessage(reportClosedAction) : null;
const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT;
const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? -1];
@@ -78,13 +71,4 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}}
ArchivedReportFooter.displayName = 'ArchivedReportFooter';
-export default withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- reportClosedAction: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`,
- canEvict: false,
- selector: ReportActionsUtils.getLastClosedReportAction,
- },
-})(ArchivedReportFooter);
+export default ArchivedReportFooter;
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 07edd148778d..84767c6347e7 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -122,6 +122,9 @@ type ButtonProps = Partial & {
/** Id to use for this button */
id?: string;
+ /** Used to locate this button in ui tests */
+ testID?: string;
+
/** Accessibility label for the component */
accessibilityLabel?: string;
@@ -237,6 +240,7 @@ function Button(
shouldShowRightIcon = false,
id = '',
+ testID = undefined,
accessibilityLabel = '',
isSplitButton = false,
link = false,
@@ -405,6 +409,7 @@ function Button(
]}
disabledStyle={disabledStyle}
id={id}
+ testID={testID}
accessibilityLabel={accessibilityLabel}
role={CONST.ROLE.BUTTON}
hoverDimmingValue={1}
diff --git a/src/components/ConfirmationPage.tsx b/src/components/ConfirmationPage.tsx
index a95cf9bf87d2..7b55f2317d46 100644
--- a/src/components/ConfirmationPage.tsx
+++ b/src/components/ConfirmationPage.tsx
@@ -82,6 +82,7 @@ function ConfirmationPage({
success
large
text={buttonText}
+ testID="confirmation-button"
style={styles.mt6}
pressOnEnter
onPress={onButtonPress}
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
index 0d8acd5eef38..336d7043d4ed 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
+++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
@@ -2,6 +2,7 @@ import type {FlashList} from '@shopify/flash-list';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import emojis from '@assets/emojis';
import {useFrequentlyUsedEmojis} from '@components/OnyxProvider';
+import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import usePreferredEmojiSkinTone from '@hooks/usePreferredEmojiSkinTone';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -23,12 +24,15 @@ const useEmojiPickerMenu = () => {
const [preferredSkinTone] = usePreferredEmojiSkinTone();
const {windowHeight} = useWindowDimensions();
const StyleUtils = useStyleUtils();
+ const {keyboardHeight} = useKeyboardState();
+
/**
- * At EmojiPicker has set innerContainerStyle with maxHeight: '95%' by styles.popoverInnerContainer
- * to avoid the list style to be cut off due to the list height being larger than the container height
- * so we need to calculate listStyle based on the height of the window and innerContainerStyle at the EmojiPicker
+ * The EmojiPicker sets the `innerContainerStyle` with `maxHeight: '95%'` in `styles.popoverInnerContainer`
+ * to prevent the list from being cut off when the list height exceeds the container's height.
+ * To calculate the available list height, we subtract the keyboard height from the `windowHeight`
+ * to ensure the list is properly adjusted when the keyboard is visible.
*/
- const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95);
+ const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95 - keyboardHeight);
useEffect(() => {
setFilteredEmojis(allEmojis);
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
index f56441316f7c..880c833e18e8 100644
--- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
@@ -27,7 +27,7 @@ function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseEr
return (
: }
+ FallbackComponent={updateRequired ? UpdateRequiredView : GenericErrorPage}
onError={catchError}
>
{children}
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index c423d3101d92..6b8cf173b0fd 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -11,6 +11,7 @@ import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {useSession} from '@components/OnyxProvider';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import SubscriptAvatar from '@components/SubscriptAvatar';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
@@ -22,7 +23,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
import DomUtils from '@libs/DomUtils';
-import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import Performance from '@libs/Performance';
@@ -32,7 +32,6 @@ import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportA
import FreeTrial from '@pages/settings/Subscription/FreeTrial';
import variables from '@styles/variables';
import Timing from '@userActions/Timing';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -48,18 +47,19 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`);
- const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER);
- const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
- selector: hasCompletedGuidedSetupFlowSelector,
- });
+
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const session = useSession();
// Guides are assigned for the MANAGE_TEAM onboarding action, except for emails that have a '+'.
const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+');
const shouldShowToooltipOnThisReport = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report);
- const [shouldHideGBRTooltip] = useOnyx(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, {initialValue: true});
+ const shouldShowGetStartedTooltip = shouldShowToooltipOnThisReport && isScreenFocused;
+ const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
+ CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR,
+ shouldShowGetStartedTooltip,
+ );
const {translate} = useLocalize();
const [isContextMenuActive, setIsContextMenuActive] = useState(false);
@@ -72,30 +72,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
}, []),
);
- const renderGBRTooltip = useCallback(
- () => (
-
-
- {translate('sidebarScreen.tooltip')}
-
- ),
- [
- styles.alignItemsCenter,
- styles.flexRow,
- styles.justifyContentCenter,
- styles.flexWrap,
- styles.textAlignCenter,
- styles.gap1,
- styles.quickActionTooltipSubtitle,
- theme.tooltipHighlightText,
- translate,
- ],
- );
-
const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT;
const sidebarInnerRowStyle = StyleSheet.flatten(
isInFocusMode
@@ -180,17 +156,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
needsOffscreenAlphaCompositing
>
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx
index ccf12aa4ce24..7c992dbeae24 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -40,8 +40,6 @@ function OptionRowLHNData({
const shouldDisplayViolations = ReportUtils.shouldDisplayViolationsRBRInLHN(fullReport, transactionViolations);
const isSettled = ReportUtils.isSettled(fullReport);
const shouldDisplayReportViolations = !isSettled && ReportUtils.isReportOwner(fullReport) && ReportUtils.hasReportViolations(reportID);
- // We only want to show RBR for expense reports with transaction violations not for transaction threads reports.
- const doesExpenseReportHasViolations = ReportUtils.isExpenseReport(fullReport) && !isSettled && ReportUtils.hasViolations(reportID, transactionViolations, true);
const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
@@ -52,7 +50,7 @@ function OptionRowLHNData({
preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
- hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations || doesExpenseReportHasViolations,
+ hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations,
lastMessageTextFromReport,
transactionViolations,
invoiceReceiverPolicy,
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 6a888a09b60b..19af05a1581b 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -303,8 +303,6 @@ function MoneyRequestConfirmationList({
return false;
};
- const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0);
-
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -320,6 +318,7 @@ function MoneyRequestConfirmationList({
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
}, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
+ const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0);
const isFirstUpdatedDistanceAmount = useRef(false);
useEffect(() => {
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 340be8a6c3e1..e32c4eae410f 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -8,7 +8,6 @@ import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -216,7 +215,6 @@ function MoneyRequestConfirmationListFooter({
unit,
}: MoneyRequestConfirmationListFooterProps) {
const styles = useThemeStyles();
- const theme = useTheme();
const {translate, toLocaleDigit} = useLocalize();
const {isOffline} = useNetwork();
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
@@ -534,7 +532,6 @@ function MoneyRequestConfirmationListFooter({
onToggle={(isOn) => onToggleBillable?.(isOn)}
isActive={iouIsBillable}
disabled={isReadOnly}
- titleStyle={!iouIsBillable && {color: theme.textSupporting}}
wrapperStyle={styles.flex1}
/>
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index 3d6ad9006dc5..ba320a594135 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -66,6 +66,9 @@ function ProcessMoneyReportHoldMenu({
const onSubmit = (full: boolean) => {
if (isApprove) {
+ if (startAnimation) {
+ startAnimation();
+ }
IOU.approveMoneyRequest(moneyRequestReport, full);
if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport?.reportID ?? '')) {
Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport?.reportID ?? ''));
diff --git a/src/components/ProductTrainingContext/PRODUCT_TRAINING_TOOLTIP_DATA.ts b/src/components/ProductTrainingContext/PRODUCT_TRAINING_TOOLTIP_DATA.ts
new file mode 100644
index 000000000000..d7f2a27d94d2
--- /dev/null
+++ b/src/components/ProductTrainingContext/PRODUCT_TRAINING_TOOLTIP_DATA.ts
@@ -0,0 +1,67 @@
+import type {ValueOf} from 'type-fest';
+import {dismissProductTraining} from '@libs/actions/Welcome';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+
+const {CONCEIRGE_LHN_GBR, RENAME_SAVED_SEARCH, WORKSAPCE_CHAT_CREATE, QUICK_ACTION_BUTTON} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES;
+
+type ProductTrainingTooltipName = ValueOf;
+
+type ShouldShowConditionProps = {
+ shouldUseNarrowLayout?: boolean;
+};
+
+type TooltipData = {
+ content: Array<{text: TranslationPaths; isBold: boolean}>;
+ onHideTooltip: () => void;
+ name: ProductTrainingTooltipName;
+ priority: number;
+ shouldShow: (props: ShouldShowConditionProps) => boolean;
+};
+
+const PRODUCT_TRAINING_TOOLTIP_DATA: Record = {
+ [CONCEIRGE_LHN_GBR]: {
+ content: [
+ {text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false},
+ {text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true},
+ ],
+ onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR),
+ name: CONCEIRGE_LHN_GBR,
+ priority: 1300,
+ shouldShow: ({shouldUseNarrowLayout}) => !!shouldUseNarrowLayout,
+ },
+ [RENAME_SAVED_SEARCH]: {
+ content: [
+ {text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true},
+ {text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false},
+ ],
+ onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH),
+ name: RENAME_SAVED_SEARCH,
+ priority: 1250,
+ shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout,
+ },
+ [QUICK_ACTION_BUTTON]: {
+ content: [
+ {text: 'productTrainingTooltip.quickActionButton.part1', isBold: true},
+ {text: 'productTrainingTooltip.quickActionButton.part2', isBold: false},
+ ],
+ onHideTooltip: () => dismissProductTraining(QUICK_ACTION_BUTTON),
+ name: QUICK_ACTION_BUTTON,
+ priority: 1200,
+ shouldShow: () => true,
+ },
+ [WORKSAPCE_CHAT_CREATE]: {
+ content: [
+ {text: 'productTrainingTooltip.workspaceChatCreate.part1', isBold: false},
+ {text: 'productTrainingTooltip.workspaceChatCreate.part2', isBold: true},
+ {text: 'productTrainingTooltip.workspaceChatCreate.part3', isBold: false},
+ ],
+ onHideTooltip: () => dismissProductTraining(WORKSAPCE_CHAT_CREATE),
+ name: WORKSAPCE_CHAT_CREATE,
+ priority: 1100,
+ shouldShow: () => true,
+ },
+};
+
+export default PRODUCT_TRAINING_TOOLTIP_DATA;
+export type {ProductTrainingTooltipName};
diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx
new file mode 100644
index 000000000000..92997fe70af3
--- /dev/null
+++ b/src/components/ProductTrainingContext/index.tsx
@@ -0,0 +1,214 @@
+import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
+import Permissions from '@libs/Permissions';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import type {ProductTrainingTooltipName} from './PRODUCT_TRAINING_TOOLTIP_DATA';
+import PRODUCT_TRAINING_TOOLTIP_DATA from './PRODUCT_TRAINING_TOOLTIP_DATA';
+
+type ProductTrainingContextType = {
+ shouldRenderTooltip: (tooltipName: ProductTrainingTooltipName) => boolean;
+ registerTooltip: (tooltipName: ProductTrainingTooltipName) => void;
+ unregisterTooltip: (tooltipName: ProductTrainingTooltipName) => void;
+};
+
+const ProductTrainingContext = createContext({
+ shouldRenderTooltip: () => false,
+ registerTooltip: () => {},
+ unregisterTooltip: () => {},
+});
+
+function ProductTrainingContextProvider({children}: ChildrenProps) {
+ const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT);
+ const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp;
+ const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
+ selector: hasCompletedGuidedSetupFlowSelector,
+ });
+ const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING);
+ const [allBetas] = useOnyx(ONYXKEYS.BETAS);
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ const [activeTooltips, setActiveTooltips] = useState>(new Set());
+
+ const unregisterTooltip = useCallback(
+ (tooltipName: ProductTrainingTooltipName) => {
+ setActiveTooltips((prev) => {
+ const next = new Set(prev);
+ next.delete(tooltipName);
+ return next;
+ });
+ },
+ [setActiveTooltips],
+ );
+
+ const determineVisibleTooltip = useCallback(() => {
+ if (activeTooltips.size === 0) {
+ return null;
+ }
+
+ const sortedTooltips = Array.from(activeTooltips)
+ .map((name) => ({
+ name,
+ priority: PRODUCT_TRAINING_TOOLTIP_DATA[name]?.priority ?? 0,
+ }))
+ .sort((a, b) => b.priority - a.priority);
+
+ const highestPriorityTooltip = sortedTooltips.at(0);
+
+ if (!highestPriorityTooltip) {
+ return null;
+ }
+
+ return highestPriorityTooltip.name;
+ }, [activeTooltips]);
+
+ const shouldTooltipBeVisible = useCallback(
+ (tooltipName: ProductTrainingTooltipName) => {
+ const isDismissed = !!dismissedProductTraining?.[tooltipName];
+
+ if (isDismissed || !Permissions.shouldShowProductTrainingElements(allBetas)) {
+ return false;
+ }
+ const tooltipConfig = PRODUCT_TRAINING_TOOLTIP_DATA[tooltipName];
+
+ if (!isOnboardingCompleted && !hasBeenAddedToNudgeMigration) {
+ return false;
+ }
+
+ return tooltipConfig.shouldShow({
+ shouldUseNarrowLayout,
+ });
+ },
+ [allBetas, dismissedProductTraining, hasBeenAddedToNudgeMigration, isOnboardingCompleted, shouldUseNarrowLayout],
+ );
+
+ const registerTooltip = useCallback(
+ (tooltipName: ProductTrainingTooltipName) => {
+ const shouldRegister = shouldTooltipBeVisible(tooltipName);
+ if (!shouldRegister) {
+ return;
+ }
+ setActiveTooltips((prev) => new Set([...prev, tooltipName]));
+ },
+ [shouldTooltipBeVisible],
+ );
+
+ const shouldRenderTooltip = useCallback(
+ (tooltipName: ProductTrainingTooltipName) => {
+ // First check base conditions
+ const shouldShow = shouldTooltipBeVisible(tooltipName);
+ if (!shouldShow) {
+ return false;
+ }
+ const visibleTooltip = determineVisibleTooltip();
+
+ // If this is the highest priority visible tooltip, show it
+ if (tooltipName === visibleTooltip) {
+ return true;
+ }
+
+ return false;
+ },
+ [shouldTooltipBeVisible, determineVisibleTooltip],
+ );
+
+ const contextValue = useMemo(
+ () => ({
+ shouldRenderTooltip,
+ registerTooltip,
+ unregisterTooltip,
+ }),
+ [shouldRenderTooltip, registerTooltip, unregisterTooltip],
+ );
+
+ return {children};
+}
+
+const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shouldShow = true) => {
+ const context = useContext(ProductTrainingContext);
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+
+ if (!context) {
+ throw new Error('useProductTourContext must be used within a ProductTourProvider');
+ }
+
+ const {shouldRenderTooltip, registerTooltip, unregisterTooltip} = context;
+
+ useEffect(() => {
+ if (shouldShow) {
+ registerTooltip(tooltipName);
+ return () => {
+ unregisterTooltip(tooltipName);
+ };
+ }
+ return () => {};
+ }, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]);
+
+ const renderProductTrainingTooltip = useCallback(() => {
+ const tooltip = PRODUCT_TRAINING_TOOLTIP_DATA[tooltipName];
+ return (
+
+
+
+ {tooltip.content.map(({text, isBold}) => {
+ const translatedText = translate(text);
+ return (
+
+ {translatedText}
+
+ );
+ })}
+
+
+ );
+ }, [
+ styles.alignItemsCenter,
+ styles.flexRow,
+ styles.flexWrap,
+ styles.gap1,
+ styles.justifyContentCenter,
+ styles.p2,
+ styles.quickActionTooltipSubtitle,
+ styles.textAlignCenter,
+ styles.textBold,
+ theme.tooltipHighlightText,
+ tooltipName,
+ translate,
+ ]);
+
+ const shouldShowProductTrainingTooltip = useMemo(() => {
+ return shouldRenderTooltip(tooltipName);
+ }, [shouldRenderTooltip, tooltipName]);
+
+ const hideProductTrainingTooltip = useCallback(() => {
+ const tooltip = PRODUCT_TRAINING_TOOLTIP_DATA[tooltipName];
+ tooltip.onHideTooltip();
+ unregisterTooltip(tooltipName);
+ }, [tooltipName, unregisterTooltip]);
+
+ return {
+ renderProductTrainingTooltip,
+ hideProductTrainingTooltip,
+ shouldShowProductTrainingTooltip: shouldShow && shouldShowProductTrainingTooltip,
+ };
+};
+
+export {ProductTrainingContextProvider, useProductTrainingContext};
diff --git a/src/components/RNMarkdownTextInput.tsx b/src/components/RNMarkdownTextInput.tsx
index d36af6e13826..11f8a852dbcf 100644
--- a/src/components/RNMarkdownTextInput.tsx
+++ b/src/components/RNMarkdownTextInput.tsx
@@ -5,13 +5,14 @@ import React from 'react';
import type {TextInput} from 'react-native';
import Animated from 'react-native-reanimated';
import useTheme from '@hooks/useTheme';
+import CONST from '@src/CONST';
// Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet
const AnimatedMarkdownTextInput = Animated.createAnimatedComponent(MarkdownTextInput);
type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & TextInput & HTMLInputElement;
-function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: ForwardedRef) {
+function RNMarkdownTextInputWithRef({maxLength, ...props}: MarkdownTextInputProps, ref: ForwardedRef) {
const theme = useTheme();
return (
@@ -27,6 +28,10 @@ function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: Forwarde
}}
// eslint-disable-next-line
{...props}
+ /**
+ * If maxLength is not set, we should set the it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text
+ */
+ maxLength={maxLength ?? CONST.MAX_COMMENT_LENGTH + 1}
/>
);
}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index de20575aeef4..d1dcdb2f57f5 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -79,8 +79,10 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !ReportUtils.isReportFieldDisabled(report, reportField, policy));
const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && ReportUtils.isReportFieldOfTypeTitle(enabledReportFields.at(0));
- const shouldShowReportField =
- !ReportUtils.isClosedExpenseReportWithNoExpenses(report) && ReportUtils.isPaidGroupPolicyExpenseReport(report) && (!isCombinedReport || !isOnlyTitleFieldEnabled);
+ const isClosedExpenseReportWithNoExpenses = ReportUtils.isClosedExpenseReportWithNoExpenses(report);
+ const isPaidGroupPolicyExpenseReport = ReportUtils.isPaidGroupPolicyExpenseReport(report);
+ const isInvoiceReport = ReportUtils.isInvoiceReport(report);
+ const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isPaidGroupPolicyExpenseReport || isInvoiceReport) && (!isCombinedReport || !isOnlyTitleFieldEnabled);
const renderThreadDivider = useMemo(
() =>
@@ -102,9 +104,9 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo
<>
- {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && (
+ {!isClosedExpenseReportWithNoExpenses && (
<>
- {ReportUtils.isPaidGroupPolicyExpenseReport(report) &&
+ {(isPaidGroupPolicyExpenseReport || isInvoiceReport) &&
policy?.areReportFieldsEnabled &&
(!isCombinedReport || !isOnlyTitleFieldEnabled) &&
sortedPolicyReportFields.map((reportField) => {
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index ca50e93e536f..e3d4a8d31cf6 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -15,7 +15,6 @@ import ViolationMessages from '@components/ViolationMessages';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useViolations from '@hooks/useViolations';
import type {ViolationField} from '@hooks/useViolations';
@@ -79,7 +78,6 @@ const getTransactionID = (report: OnyxEntry, parentReportActio
};
function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) {
- const theme = useTheme();
const styles = useThemeStyles();
const session = useSession();
const {isOffline} = useNetwork();
@@ -390,7 +388,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const shouldShowReceiptAudit = isReceiptAllowed && (shouldShowReceiptEmptyState || hasReceipt);
const errors = {
- ...(transaction?.errorFields?.route ?? transaction?.errors),
+ ...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
...parentReportAction?.errors,
};
@@ -468,8 +466,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
return;
}
if (parentReportAction) {
- const urlToNavigateBack = IOU.cleanUpMoneyRequest(transaction?.transactionID ?? linkedTransactionID, parentReportAction, true);
- Navigation.goBack(urlToNavigateBack);
+ IOU.cleanUpMoneyRequest(transaction?.transactionID ?? linkedTransactionID, parentReportAction, true);
return;
}
}
@@ -730,7 +727,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
{shouldShowBillable && (
- {translate('common.billable')}
+ {translate('common.billable')}
{!!getErrorForField('billable') && (
();
const [paymentType, setPaymentType] = useState();
const getCanIOUBePaid = useCallback(
- (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere),
+ (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
+ IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
[iouReport, chatReport, policy, allTransactions],
);
const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
+ const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
+ const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning;
+
+ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport);
const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = ReportUtils.getNonHeldAndFullAmount(iouReport, shouldShowPayButton);
const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? '');
@@ -151,12 +157,18 @@ function ReportPreview({
}));
const checkMarkScale = useSharedValue(iouSettled ? 1 : 0);
+ const isApproved = ReportUtils.isReportApproved(iouReport, action);
+ const thumbsUpScale = useSharedValue(isApproved ? 1 : 0);
+ const thumbsUpStyle = useAnimatedStyle(() => ({
+ ...styles.defaultCheckmarkWrapper,
+ transform: [{scale: thumbsUpScale.get()}],
+ }));
+
const moneyRequestComment = action?.childLastMoneyRequestComment ?? '';
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport);
const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport);
const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport);
- const isApproved = ReportUtils.isReportApproved(iouReport, action);
const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy);
const numberOfRequests = allTransactions.length;
const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID);
@@ -207,11 +219,19 @@ function ReportPreview({
const {isDelegateAccessRestricted} = useDelegateUserDetails();
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
- const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []);
+ const stopAnimation = useCallback(() => {
+ setIsPaidAnimationRunning(false);
+ setIsApprovedAnimationRunning(false);
+ }, []);
const startAnimation = useCallback(() => {
setIsPaidAnimationRunning(true);
HapticFeedback.longPress();
}, []);
+ const startApprovedAnimation = useCallback(() => {
+ setIsApprovedAnimationRunning(true);
+ HapticFeedback.longPress();
+ }, []);
+
const confirmPayment = useCallback(
(type: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type) {
@@ -243,6 +263,8 @@ function ReportPreview({
} else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
setIsHoldMenuVisible(true);
} else {
+ setIsApprovedAnimationRunning(true);
+ HapticFeedback.longPress();
IOU.approveMoneyRequest(iouReport, true);
}
};
@@ -340,9 +362,6 @@ function ReportPreview({
]);
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
- const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]);
-
- const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport);
const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
@@ -427,7 +446,7 @@ function ReportPreview({
const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport);
useEffect(() => {
- if (!isPaidAnimationRunning) {
+ if (!isPaidAnimationRunning || isApprovedAnimationRunning) {
return;
}
@@ -448,6 +467,14 @@ function ReportPreview({
checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1);
}, [isPaidAnimationRunning, iouSettled, checkMarkScale]);
+ useEffect(() => {
+ if (!isApproved) {
+ return;
+ }
+
+ thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBSUP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBSUP_DURATION})) : 1);
+ }, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
+
const openReportFromPreview = useCallback(() => {
Performance.markStart(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
@@ -483,7 +510,7 @@ function ReportPreview({
- {previewMessage}
+ {previewMessage}
{shouldShowRBR && (
)}
+ {isApproved && (
+
+
+
+ )}
{shouldShowSubtitle && !!supportText && (
@@ -537,6 +572,8 @@ function ReportPreview({
shouldUseSuccessStyle={!hasHeldExpenses}
onlyShowPayElsewhere={onlyShowPayElsewhere}
isPaidAnimationRunning={isPaidAnimationRunning}
+ isApprovedAnimationRunning={isApprovedAnimationRunning}
+ canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
onAnimationFinish={stopAnimation}
formattedAmount={getSettlementAmount() ?? ''}
currency={iouReport?.currency}
@@ -604,7 +641,13 @@ function ReportPreview({
chatReport={chatReport}
moneyRequestReport={iouReport}
transactionCount={numberOfRequests}
- startAnimation={startAnimation}
+ startAnimation={() => {
+ if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
+ startApprovedAnimation();
+ } else {
+ startAnimation();
+ }
+ }}
/>
)}
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 09315bfb8a8e..1e3df2a34817 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -177,7 +177,7 @@ function ScreenWrapper(
}, [route?.params]);
UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => {
- NativeModules.HybridAppModule?.closeReactNativeApp(false, false);
+ NativeModules.HybridAppModule?.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
});
const panResponder = useRef(
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 07f23a96424d..4c08c477f29d 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -78,7 +78,10 @@ function mapToItemWithSelectionInfo(
shouldAnimateInHighlight: boolean,
) {
if (SearchUIUtils.isReportActionListItemType(item)) {
- return item;
+ return {
+ ...item,
+ shouldAnimateInHighlight,
+ };
}
return SearchUIUtils.isTransactionListItemType(item)
@@ -134,6 +137,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const previousTransactions = usePrevious(transactions);
+ const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+ const previousReportActions = usePrevious(reportActions);
useEffect(() => {
if (!currentSearchResults?.search?.type) {
@@ -211,6 +216,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
previousTransactions,
queryJSON,
offset,
+ reportActions,
+ previousReportActions,
});
// There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
@@ -323,15 +330,20 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
const ListItem = SearchUIUtils.getListItem(type, status);
const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder);
+ const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT;
const sortedSelectedData = sortedData.map((item) => {
- const baseKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
+ const baseKey = isChat
+ ? `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${(item as ReportActionListItemType).reportActionID}`
+ : `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
// Check if the base key matches the newSearchResultKey (TransactionListItemType)
const isBaseKeyMatch = baseKey === newSearchResultKey;
// Check if any transaction within the transactions array (ReportListItemType) matches the newSearchResultKey
- const isAnyTransactionMatch = (item as ReportListItemType)?.transactions?.some((transaction) => {
- const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`;
- return transactionKey === newSearchResultKey;
- });
+ const isAnyTransactionMatch =
+ !isChat &&
+ (item as ReportListItemType)?.transactions?.some((transaction) => {
+ const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`;
+ return transactionKey === newSearchResultKey;
+ });
// Determine if either the base key or any transaction key matches
const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch;
diff --git a/src/components/SelectionList/ChatListItem.tsx b/src/components/SelectionList/ChatListItem.tsx
index a3e04c9088f1..4807aa7760c8 100644
--- a/src/components/SelectionList/ChatListItem.tsx
+++ b/src/components/SelectionList/ChatListItem.tsx
@@ -5,11 +5,13 @@ import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/M
import MultipleAvatars from '@components/MultipleAvatars';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import TextWithTooltip from '@components/TextWithTooltip';
+import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import ReportActionItemDate from '@pages/home/report/ReportActionItemDate';
import ReportActionItemFragment from '@pages/home/report/ReportActionItemFragment';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {ChatListItemProps, ListItem, ReportActionListItemType} from './types';
@@ -56,11 +58,24 @@ function ChatListItem({
const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
const mentionReportContextValue = useMemo(() => ({currentReportID: item?.reportID ?? '-1'}), [item.reportID]);
-
+ const animatedHighlightStyle = useAnimatedHighlightStyle({
+ borderRadius: variables.componentBorderRadius,
+ shouldHighlight: item?.shouldAnimateInHighlight ?? false,
+ highlightColor: theme.messageHighlightBG,
+ backgroundColor: theme.highlightBG,
+ });
+ const pressableStyle = [
+ styles.selectionListPressableItemWrapper,
+ styles.textAlignLeft,
+ // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle
+ styles.bgTransparent,
+ item.isSelected && styles.activeComponentBG,
+ item.cursorStyle,
+ ];
return (
({
keyForList={item.keyForList}
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
+ pressableWrapperStyle={[styles.mh5, animatedHighlightStyle]}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
{(hovered) => (
diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx
index db5077beca9a..2608e4e2de8c 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Button from '@components/Button';
@@ -36,7 +36,7 @@ type ActionCellProps = {
function ActionCell({
action = CONST.SEARCH.ACTION_TYPES.VIEW,
- shouldUseSuccessStyle = true,
+ shouldUseSuccessStyle: shouldUseSuccessStyleProp = true,
isLargeScreenWidth = true,
isSelected = false,
goToItem,
@@ -52,6 +52,16 @@ function ActionCell({
const text = translate(actionTranslationsMap[action]);
+ const getButtonInnerStyles = useCallback(
+ (shouldUseSuccessStyle: boolean) => {
+ if (!isSelected) {
+ return {};
+ }
+ return shouldUseSuccessStyle ? styles.buttonSuccessHovered : styles.buttonDefaultHovered;
+ },
+ [isSelected, styles],
+ );
+
const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID);
if ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE) {
@@ -77,31 +87,32 @@ function ActionCell({
);
}
- if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) {
- const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {};
+ if (action === CONST.SEARCH.ACTION_TYPES.VIEW || action === CONST.SEARCH.ACTION_TYPES.REVIEW || shouldUseViewAction) {
return isLargeScreenWidth ? (
) : null;
}
- const buttonInnerStyles = isSelected ? styles.buttonSuccessHovered : {};
return (
);
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index 11ddf8fa8d0e..73d0ec8f8c10 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -41,7 +41,7 @@ function TotalCell({showTooltip, isLargeScreenWidth, reportItem}: ReportCellProp
// Only invert non-zero values otherwise we'll end up with -0.00
if (total) {
- total *= reportItem?.type === CONST.REPORT.TYPE.EXPENSE ? -1 : 1;
+ total *= reportItem?.type === CONST.REPORT.TYPE.EXPENSE || reportItem?.type === CONST.REPORT.TYPE.INVOICE ? -1 : 1;
}
return (
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 65c2fd2f493b..9a9e6f8de01c 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -11,9 +11,18 @@ import type SettlementButtonProps from './types';
type AnimatedSettlementButtonProps = SettlementButtonProps & {
isPaidAnimationRunning: boolean;
onAnimationFinish: () => void;
+ isApprovedAnimationRunning: boolean;
+ canIOUBePaid: boolean;
};
-function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
+function AnimatedSettlementButton({
+ isPaidAnimationRunning,
+ onAnimationFinish,
+ isApprovedAnimationRunning,
+ isDisabled,
+ canIOUBePaid,
+ ...settlementButtonProps
+}: AnimatedSettlementButtonProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const buttonScale = useSharedValue(1);
@@ -38,12 +47,13 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
overflow: 'hidden',
marginTop: buttonMarginTop.get(),
}));
- const buttonDisabledStyle = isPaidAnimationRunning
- ? {
- opacity: 1,
- ...styles.cursorDefault,
- }
- : undefined;
+ const buttonDisabledStyle =
+ isPaidAnimationRunning || isApprovedAnimationRunning
+ ? {
+ opacity: 1,
+ ...styles.cursorDefault,
+ }
+ : undefined;
const resetAnimation = useCallback(() => {
buttonScale.set(1);
@@ -55,7 +65,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
}, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
useEffect(() => {
- if (!isPaidAnimationRunning) {
+ if (!isApprovedAnimationRunning && !isPaidAnimationRunning) {
resetAnimation();
return;
}
@@ -65,15 +75,30 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
// Wait for the above animation + 1s delay before hiding the component
const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
+ const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
height.set(
withDelay(
totalDelay,
- withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
+ withTiming(willShowPaymentButton ? variables.componentSizeNormal : 0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
),
);
+ buttonMarginTop.set(withDelay(totalDelay, withTiming(willShowPaymentButton ? styles.expenseAndReportPreviewTextButtonContainer.gap : 0, {duration: CONST.ANIMATION_PAID_DURATION})));
buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
paymentCompleteTextOpacity.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
- }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]);
+ }, [
+ isPaidAnimationRunning,
+ isApprovedAnimationRunning,
+ onAnimationFinish,
+ buttonOpacity,
+ buttonScale,
+ height,
+ paymentCompleteTextOpacity,
+ paymentCompleteTextScale,
+ buttonMarginTop,
+ resetAnimation,
+ canIOUBePaid,
+ styles.expenseAndReportPreviewTextButtonContainer.gap,
+ ]);
return (
@@ -82,11 +107,16 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
{translate('iou.paymentComplete')}
)}
+ {isApprovedAnimationRunning && (
+
+ {translate('iou.approved')}
+
+ )}
diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx
index 678cc0f1c805..00675ca4ccd6 100644
--- a/src/components/TextInput/BaseTextInput/index.tsx
+++ b/src/components/TextInput/BaseTextInput/index.tsx
@@ -24,6 +24,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
+import * as InputUtils from '@libs/InputUtils';
import isInputAutoFilled from '@libs/isInputAutoFilled';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -104,6 +105,7 @@ function BaseTextInput(
const input = useRef(null);
const isLabelActive = useRef(initialActiveLabel);
+ const didScrollToEndRef = useRef(false);
// AutoFocus which only works on mount:
useEffect(() => {
@@ -420,7 +422,19 @@ function BaseTextInput(
)}
- {isFocused && !isReadOnly && shouldShowClearButton && !!value && setValue('')} />}
+ {isFocused && !isReadOnly && shouldShowClearButton && !!value && (
+ {
+ if (didScrollToEndRef.current || !input.current) {
+ return;
+ }
+ InputUtils.scrollToRight(input.current);
+ didScrollToEndRef.current = true;
+ }}
+ >
+ setValue('')} />
+
+ )}
{!!inputProps.isLoading && (
{
- document.execCommand('insertText', false, text);
-};
+const insertAtCaret = (target: HTMLElement, insertedText: string, maxLength: number) => {
+ const currentText = target.textContent ?? '';
+
+ let availableLength = maxLength - currentText.length;
+ if (availableLength <= 0) {
+ return;
+ }
+
+ let text = insertedText;
-const insertAtCaret = (target: HTMLElement, text: string) => {
const selection = window.getSelection();
if (selection?.rangeCount) {
const range = selection.getRangeAt(0);
+ const selectedText = range.toString();
+ availableLength -= selectedText.length;
+ if (availableLength <= 0) {
+ return;
+ }
+ text = text.slice(0, availableLength);
range.deleteContents();
+
const node = document.createTextNode(text);
range.insertNode(node);
@@ -22,40 +35,43 @@ const insertAtCaret = (target: HTMLElement, text: string) => {
// Dispatch input event to trigger Markdown Input to parse the new text
target.dispatchEvent(new Event('input', {bubbles: true}));
- } else {
- insertByCommand(text);
}
};
-const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false) => {
+const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => {
const navigation = useNavigation();
/**
* Set pasted text to clipboard
* @param {String} text
*/
- const paste = useCallback((text: string) => {
- try {
- const textInputHTMLElement = textInputRef.current as HTMLElement;
- if (textInputHTMLElement?.hasAttribute('contenteditable')) {
- insertAtCaret(textInputHTMLElement, text);
- } else {
- insertByCommand(text);
- }
+ const paste = useCallback(
+ (text: string) => {
+ try {
+ const textInputHTMLElement = textInputRef.current as HTMLElement;
+ if (textInputHTMLElement?.hasAttribute('contenteditable')) {
+ insertAtCaret(textInputHTMLElement, text, maxLength);
+ } else {
+ const htmlInput = textInputRef.current as unknown as HTMLInputElement;
+ const availableLength = maxLength - (htmlInput.value?.length ?? 0);
+ htmlInput.setRangeText(text.slice(0, availableLength));
+ }
- // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
- // To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler
- // We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered
- textInputHTMLElement.dispatchEvent(
- new FocusEvent('focusin', {
- bubbles: true,
- }),
- );
- // eslint-disable-next-line no-empty
- } catch (e) {}
- // We only need to set the callback once.
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, []);
+ // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view.
+ // To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler
+ // We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered
+ textInputHTMLElement.dispatchEvent(
+ new FocusEvent('focusin', {
+ bubbles: true,
+ }),
+ );
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
+ // We only need to set the callback once.
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ },
+ [maxLength, textInputRef],
+ );
/**
* Manually place the pasted HTML into Composer
@@ -64,9 +80,9 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
*/
const handlePastedHTML = useCallback(
(html: string) => {
- paste(Parser.htmlToMarkdown(html));
+ paste(Parser.htmlToMarkdown(html.slice(0, maxLength)));
},
- [paste],
+ [paste, maxLength],
);
/**
diff --git a/src/hooks/useHtmlPaste/types.ts b/src/hooks/useHtmlPaste/types.ts
index 305ebe5fbd0f..0aaa12a49ac9 100644
--- a/src/hooks/useHtmlPaste/types.ts
+++ b/src/hooks/useHtmlPaste/types.ts
@@ -5,6 +5,7 @@ type UseHtmlPaste = (
textInputRef: MutableRefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>,
preHtmlPasteCallback?: (event: ClipboardEvent) => boolean,
removeListenerOnScreenBlur?: boolean,
+ maxLength?: number, // Maximum length of the text input value after pasting
) => void;
export default UseHtmlPaste;
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 3a821dc44caf..84ac886cf88b 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx';
import Navigation from '@libs/Navigation/Navigation';
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import Permissions from '@libs/Permissions';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -37,6 +38,9 @@ function useOnboardingFlowRouter() {
}
if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.migratedUserWelcomeModal && Permissions.shouldShowProductTrainingElements(allBetas)) {
+ const defaultCannedQuery = SearchQueryUtils.buildCannedSearchQuery();
+ const query = defaultCannedQuery;
+ Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
Navigation.navigate(ROUTES.MIGRATED_USER_WELCOME_MODAL);
return;
}
diff --git a/src/hooks/usePageRefresh/index.ts b/src/hooks/usePageRefresh/index.ts
index 82f274f7fc3f..e8f4f9bc6877 100644
--- a/src/hooks/usePageRefresh/index.ts
+++ b/src/hooks/usePageRefresh/index.ts
@@ -6,10 +6,10 @@ import type UsePageRefresh from './type';
const usePageRefresh: UsePageRefresh = () => {
const {resetBoundary} = useErrorBoundary();
- return () => {
+ return (isChunkLoadError?: boolean) => {
const lastRefreshTimestamp = JSON.parse(sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.LAST_REFRESH_TIMESTAMP) ?? 'null') as string;
- if (lastRefreshTimestamp === null || differenceInMilliseconds(Date.now(), Number(lastRefreshTimestamp)) > CONST.ERROR_WINDOW_RELOAD_TIMEOUT) {
+ if (!isChunkLoadError && (lastRefreshTimestamp === null || differenceInMilliseconds(Date.now(), Number(lastRefreshTimestamp)) > CONST.ERROR_WINDOW_RELOAD_TIMEOUT)) {
resetBoundary();
sessionStorage.setItem(CONST.SESSION_STORAGE_KEYS.LAST_REFRESH_TIMESTAMP, Date.now().toString());
diff --git a/src/hooks/usePageRefresh/type.ts b/src/hooks/usePageRefresh/type.ts
index f800cc0224a9..c4477fffb1e6 100644
--- a/src/hooks/usePageRefresh/type.ts
+++ b/src/hooks/usePageRefresh/type.ts
@@ -1,3 +1,3 @@
-type UsePageRefresh = () => () => void;
+type UsePageRefresh = () => (isChunkLoadError?: boolean) => void;
export default UsePageRefresh;
diff --git a/src/hooks/useRestoreInputFocus.ts b/src/hooks/useRestoreInputFocus.ts
new file mode 100644
index 000000000000..a8049eb70b77
--- /dev/null
+++ b/src/hooks/useRestoreInputFocus.ts
@@ -0,0 +1,21 @@
+import {useEffect, useRef} from 'react';
+import {InteractionManager, Keyboard} from 'react-native';
+import {KeyboardController} from 'react-native-keyboard-controller';
+
+const useRestoreInputFocus = (isLostFocus: boolean) => {
+ const keyboardVisibleBeforeLoosingFocusRef = useRef(false);
+
+ useEffect(() => {
+ if (isLostFocus) {
+ keyboardVisibleBeforeLoosingFocusRef.current = Keyboard.isVisible();
+ }
+
+ if (!isLostFocus && keyboardVisibleBeforeLoosingFocusRef.current) {
+ InteractionManager.runAfterInteractions(() => {
+ KeyboardController.setFocusTo('current');
+ });
+ }
+ }, [isLostFocus]);
+};
+
+export default useRestoreInputFocus;
diff --git a/src/hooks/useSearchHighlightAndScroll.ts b/src/hooks/useSearchHighlightAndScroll.ts
index 95a953139ebe..a5937559fd2f 100644
--- a/src/hooks/useSearchHighlightAndScroll.ts
+++ b/src/hooks/useSearchHighlightAndScroll.ts
@@ -3,43 +3,51 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {SearchQueryJSON} from '@components/Search/types';
import type {ReportActionListItemType, ReportListItemType, SelectionListHandle, TransactionListItemType} from '@components/SelectionList/types';
import * as SearchActions from '@libs/actions/Search';
+import {isReportActionEntry} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {SearchResults, Transaction} from '@src/types/onyx';
+import type {ReportActions, SearchResults, Transaction} from '@src/types/onyx';
import usePrevious from './usePrevious';
type UseSearchHighlightAndScroll = {
searchResults: OnyxEntry;
transactions: OnyxCollection;
previousTransactions: OnyxCollection;
+ reportActions: OnyxCollection;
+ previousReportActions: OnyxCollection;
queryJSON: SearchQueryJSON;
offset: number;
};
/**
- * Hook used to trigger a search when a new transaction is added and handle highlighting and scrolling.
+ * Hook used to trigger a search when a new transaction or report action is added and handle highlighting and scrolling.
*/
-function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, queryJSON, offset}: UseSearchHighlightAndScroll) {
+function useSearchHighlightAndScroll({searchResults, transactions, previousTransactions, reportActions, previousReportActions, queryJSON, offset}: UseSearchHighlightAndScroll) {
// Ref to track if the search was triggered by this hook
const triggeredByHookRef = useRef(false);
const searchTriggeredRef = useRef(false);
const previousSearchResults = usePrevious(searchResults?.data);
const [newSearchResultKey, setNewSearchResultKey] = useState(null);
- const highlightedTransactionIDs = useRef>(new Set());
+ const highlightedIDs = useRef>(new Set());
const initializedRef = useRef(false);
+ const isChat = queryJSON.type === CONST.SEARCH.DATA_TYPES.CHAT;
- // Trigger search when a new transaction is added
+ // Trigger search when a new report action is added while on chat or when a new transaction is added for the other search types.
useEffect(() => {
const previousTransactionsLength = previousTransactions && Object.keys(previousTransactions).length;
const transactionsLength = transactions && Object.keys(transactions).length;
- // Return early if search was already triggered or there's no change in transactions length
- if (searchTriggeredRef.current || previousTransactionsLength === transactionsLength) {
+ const reportActionsLength = reportActions && Object.values(reportActions).reduce((sum, curr) => sum + Object.keys(curr ?? {}).length, 0);
+ const prevReportActionsLength = previousReportActions && Object.values(previousReportActions).reduce((sum, curr) => sum + Object.keys(curr ?? {}).length, 0);
+ // Return early if search was already triggered or there's no change in current and previous data length
+ if (searchTriggeredRef.current || (!isChat && previousTransactionsLength === transactionsLength) || (isChat && reportActionsLength === prevReportActionsLength)) {
return;
}
+ const newTransactionAdded = transactionsLength && typeof previousTransactionsLength === 'number' && transactionsLength > previousTransactionsLength;
+ const newReportActionAdded = reportActionsLength && typeof prevReportActionsLength === 'number' && reportActionsLength > prevReportActionsLength;
- // Check if a new transaction was added
- if (transactionsLength && typeof previousTransactionsLength === 'number' && transactionsLength > previousTransactionsLength) {
+ // Check if a new transaction or report action was added
+ if ((!isChat && !!newTransactionAdded) || (isChat && !!newReportActionAdded)) {
// Set the flag indicating the search is triggered by the hook
triggeredByHookRef.current = true;
@@ -50,45 +58,62 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans
searchTriggeredRef.current = true;
}
- // Reset the ref when transactions are updated
+ // Reset the ref when transactions or report actions in chat search type are updated
return () => {
searchTriggeredRef.current = false;
};
- }, [transactions, previousTransactions, queryJSON, offset]);
+ }, [transactions, previousTransactions, queryJSON, offset, reportActions, previousReportActions, isChat]);
- // Initialize the set with existing transaction IDs only once
+ // Initialize the set with existing IDs only once
useEffect(() => {
if (initializedRef.current || !searchResults?.data) {
return;
}
- const existingTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data);
- highlightedTransactionIDs.current = new Set(existingTransactionIDs);
+ const existingIDs = isChat ? extractReportActionIDsFromSearchResults(searchResults.data) : extractTransactionIDsFromSearchResults(searchResults.data);
+ highlightedIDs.current = new Set(existingIDs);
initializedRef.current = true;
- }, [searchResults?.data]);
+ }, [searchResults?.data, isChat]);
- // Detect new transactions
+ // Detect new items (transactions or report actions)
useEffect(() => {
if (!previousSearchResults || !searchResults?.data) {
return;
}
+ if (isChat) {
+ const previousReportActionIDs = extractReportActionIDsFromSearchResults(previousSearchResults);
+ const currentReportActionIDs = extractReportActionIDsFromSearchResults(searchResults.data);
- const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults);
- const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data);
+ // Find new report action IDs that are not in the previousReportActionIDs and not already highlighted
+ const newReportActionIDs = currentReportActionIDs.filter((id) => !previousReportActionIDs.includes(id) && !highlightedIDs.current.has(id));
- // Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted
- const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedTransactionIDs.current.has(id));
+ if (!triggeredByHookRef.current || newReportActionIDs.length === 0) {
+ return;
+ }
- if (!triggeredByHookRef.current || newTransactionIDs.length === 0) {
- return;
- }
+ const newReportActionID = newReportActionIDs.at(0) ?? '';
+ const newReportActionKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newReportActionID}`;
- const newTransactionID = newTransactionIDs.at(0) ?? '';
- const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`;
+ setNewSearchResultKey(newReportActionKey);
+ highlightedIDs.current.add(newReportActionID);
+ } else {
+ const previousTransactionIDs = extractTransactionIDsFromSearchResults(previousSearchResults);
+ const currentTransactionIDs = extractTransactionIDsFromSearchResults(searchResults.data);
- setNewSearchResultKey(newTransactionKey);
- highlightedTransactionIDs.current.add(newTransactionID);
- }, [searchResults, previousSearchResults]);
+ // Find new transaction IDs that are not in the previousTransactionIDs and not already highlighted
+ const newTransactionIDs = currentTransactionIDs.filter((id) => !previousTransactionIDs.includes(id) && !highlightedIDs.current.has(id));
+
+ if (!triggeredByHookRef.current || newTransactionIDs.length === 0) {
+ return;
+ }
+
+ const newTransactionID = newTransactionIDs.at(0) ?? '';
+ const newTransactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${newTransactionID}`;
+
+ setNewSearchResultKey(newTransactionKey);
+ highlightedIDs.current.add(newTransactionID);
+ }
+ }, [searchResults?.data, previousSearchResults, isChat]);
// Reset newSearchResultKey after it's been used
useEffect(() => {
@@ -114,35 +139,41 @@ function useSearchHighlightAndScroll({searchResults, transactions, previousTrans
return;
}
- // Extract the transaction ID from the newSearchResultKey
- const newTransactionID = newSearchResultKey.replace(ONYXKEYS.COLLECTION.TRANSACTION, '');
-
- // Find the index of the new transaction in the data array
- const indexOfNewTransaction = data.findIndex((item) => {
- // Handle TransactionListItemType
- if ('transactionID' in item && item.transactionID === newTransactionID) {
- return true;
- }
-
- // Handle ReportListItemType with transactions array
- if ('transactions' in item && Array.isArray(item.transactions)) {
- return item.transactions.some((transaction) => transaction?.transactionID === newTransactionID);
+ // Extract the transaction/report action ID from the newSearchResultKey
+ const newID = newSearchResultKey.replace(isChat ? ONYXKEYS.COLLECTION.REPORT_ACTIONS : ONYXKEYS.COLLECTION.TRANSACTION, '');
+
+ // Find the index of the new transaction/report action in the data array
+ const indexOfNewItem = data.findIndex((item) => {
+ if (isChat) {
+ if ('reportActionID' in item && item.reportActionID === newID) {
+ return true;
+ }
+ } else {
+ // Handle TransactionListItemType
+ if ('transactionID' in item && item.transactionID === newID) {
+ return true;
+ }
+
+ // Handle ReportListItemType with transactions array
+ if ('transactions' in item && Array.isArray(item.transactions)) {
+ return item.transactions.some((transaction) => transaction?.transactionID === newID);
+ }
}
return false;
});
- // Early return if the transaction is not found in the data array
- if (indexOfNewTransaction <= 0) {
+ // Early return if the new item is not found in the data array
+ if (indexOfNewItem <= 0) {
return;
}
// Perform the scrolling action
- ref.scrollToIndex(indexOfNewTransaction);
+ ref.scrollToIndex(indexOfNewItem);
// Reset the trigger flag to prevent unintended future scrolls and highlights
triggeredByHookRef.current = false;
},
- [newSearchResultKey],
+ [newSearchResultKey, isChat],
);
return {newSearchResultKey, handleSelectionListScroll};
@@ -174,4 +205,14 @@ function extractTransactionIDsFromSearchResults(searchResultsData: Partial): string[] {
+ return Object.keys(searchResultsData ?? {})
+ .filter(isReportActionEntry)
+ .map((key) => Object.keys(searchResultsData[key] ?? {}))
+ .flat();
+}
+
export default useSearchHighlightAndScroll;
diff --git a/src/hooks/useWaitForNavigation.ts b/src/hooks/useWaitForNavigation.ts
index 73c0eb2bb14c..05981ec3322b 100644
--- a/src/hooks/useWaitForNavigation.ts
+++ b/src/hooks/useWaitForNavigation.ts
@@ -1,5 +1,5 @@
-import {useNavigation} from '@react-navigation/native';
-import {useEffect, useRef} from 'react';
+import {useFocusEffect} from '@react-navigation/native';
+import {useCallback, useRef} from 'react';
type UseWaitForNavigation = (navigate: () => void) => () => Promise;
@@ -8,21 +8,18 @@ type UseWaitForNavigation = (navigate: () => void) => () => Promise;
* Only use when navigating by react-navigation
*/
export default function useWaitForNavigation(): UseWaitForNavigation {
- const navigation = useNavigation();
const resolvePromises = useRef void>>([]);
- useEffect(() => {
- const unsubscribeBlur = navigation.addListener('blur', () => {
- resolvePromises.current.forEach((resolve) => {
- resolve();
- });
- resolvePromises.current = [];
- });
-
- return () => {
- unsubscribeBlur();
- };
- }, [navigation]);
+ useFocusEffect(
+ useCallback(() => {
+ return () => {
+ resolvePromises.current.forEach((resolve) => {
+ resolve();
+ });
+ resolvePromises.current = [];
+ };
+ }, []),
+ );
return (navigate: () => void) => () => {
navigate();
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 7088d9df8a51..ceccfc61057e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -216,7 +216,7 @@ const translations = {
no: 'No',
ok: 'OK',
notNow: 'Not now',
- learnMore: 'Learn more',
+ learnMore: 'Learn more.',
buttonConfirm: 'Got it',
name: 'Name',
attachment: 'Attachment',
@@ -228,6 +228,7 @@ const translations = {
optional: 'Optional',
new: 'New',
search: 'Search',
+ reports: 'Reports',
find: 'Find',
searchWithThreeDots: 'Search...',
next: 'Next',
@@ -646,10 +647,6 @@ const translations = {
emoji: 'Emoji',
collapse: 'Collapse',
expand: 'Expand',
- tooltip: {
- title: 'Get started!',
- subtitle: ' Submit your first expense',
- },
},
reportActionContextMenu: {
copyToClipboard: 'Copy to clipboard',
@@ -836,10 +833,6 @@ const translations = {
trackDistance: 'Track distance',
noLongerHaveReportAccess: 'You no longer have access to your previous quick action destination. Pick a new one below.',
updateDestination: 'Update destination',
- tooltip: {
- title: 'Quick action! ',
- subtitle: 'Just a tap away.',
- },
},
iou: {
amount: 'Amount',
@@ -1311,8 +1304,8 @@ const translations = {
addKey: 'Or add this secret key to your authenticator app:',
enterCode: 'Then enter the six-digit code generated from your authenticator app.',
stepSuccess: 'Finished',
- enabled: 'Two-factor authentication is now enabled!',
- congrats: 'Congrats, now you’ve got that extra security.',
+ enabled: 'Two-factor authentication enabled',
+ congrats: 'Congrats! Now you’ve got that extra security.',
copy: 'Copy',
disable: 'Disable',
enableTwoFactorAuth: 'Enable two-factor authentication',
@@ -4439,7 +4432,7 @@ const translations = {
roomNameInvalidError: 'Room names can only include lowercase letters, numbers, and hyphens.',
pleaseEnterRoomName: 'Please enter a room name.',
pleaseSelectWorkspace: 'Please select a workspace.',
- renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => `renamed this room from ${oldName} to ${newName}`,
+ renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => `renamed this room to "${newName}" (previously "${oldName}")`,
roomRenamedTo: ({newName}: RoomRenamedToParams) => `Room renamed to ${newName}`,
social: 'social',
selectAWorkspace: 'Select a workspace',
@@ -4453,7 +4446,7 @@ const translations = {
},
},
workspaceActions: {
- renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `updated the name of this workspace from ${oldName} to ${newName}`,
+ renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `updated the name of this workspace to "${newName}" (previously "${oldName}")`,
removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => {
let joinedNames = '';
if (submittersNames.length === 1) {
@@ -4553,7 +4546,6 @@ const translations = {
},
},
saveSearch: 'Save search',
- saveSearchTooltipText: 'You can rename your saved search',
deleteSavedSearch: 'Delete saved search',
deleteSavedSearchConfirm: 'Are you sure you want to delete this search?',
searchName: 'Search name',
@@ -5455,6 +5447,25 @@ const translations = {
crossPlatform: 'Do everything from your phone or browser',
},
},
+ productTrainingTooltip: {
+ conciergeLHNGBR: {
+ part1: 'Get started',
+ part2: ' here!',
+ },
+ saveSearchTooltip: {
+ part1: 'Rename your saved searches',
+ part2: ' here!',
+ },
+ quickActionButton: {
+ part1: 'Quick action!',
+ part2: ' Just a tap away',
+ },
+ workspaceChatCreate: {
+ part1: 'Submit your',
+ part2: ' expenses',
+ part3: ' here!',
+ },
+ },
};
export default translations satisfies TranslationDeepObject;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 47e11bf716ac..6b20790415b3 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -207,7 +207,7 @@ const translations = {
no: 'No',
ok: 'OK',
notNow: 'Ahora no',
- learnMore: 'Más información',
+ learnMore: 'Más información.',
buttonConfirm: 'Ok, entendido',
name: 'Nombre',
attachment: 'Archivo adjunto',
@@ -219,6 +219,7 @@ const translations = {
new: 'Nuevo',
center: 'Centrar',
search: 'Buscar',
+ reports: 'Informes',
find: 'Encontrar',
searchWithThreeDots: 'Buscar...',
select: 'Seleccionar',
@@ -638,10 +639,6 @@ const translations = {
emoji: 'Emoji',
collapse: 'Colapsar',
expand: 'Expandir',
- tooltip: {
- title: '¡Empecemos!',
- subtitle: ' Presenta tu primer gasto',
- },
},
reportActionContextMenu: {
copyToClipboard: 'Copiar al portapapeles',
@@ -831,10 +828,6 @@ const translations = {
trackDistance: 'Crear gasto por desplazamiento',
noLongerHaveReportAccess: 'Ya no tienes acceso al destino previo de esta acción rápida. Escoge uno nuevo a continuación.',
updateDestination: 'Actualiza el destino',
- tooltip: {
- title: '¡Acción rápida! ',
- subtitle: 'A un click.',
- },
},
iou: {
amount: 'Importe',
@@ -1311,8 +1304,8 @@ const translations = {
addKey: 'O añade esta clave secreta a tu aplicación de autenticación:',
enterCode: 'Luego introduce el código de seis dígitos generado por tu aplicación de autenticación.',
stepSuccess: 'Finalizado',
- enabled: '¡La autenticación de dos factores está ahora habilitada!',
- congrats: 'Felicidades, ahora tienes esa seguridad adicional.',
+ enabled: 'La autenticación de dos factores habilitada',
+ congrats: '¡Felicidades! Ahora tienes esa seguridad adicional.',
copy: 'Copiar',
disable: 'Deshabilitar',
enableTwoFactorAuth: 'Activar la autenticación de dos factores',
@@ -4488,7 +4481,7 @@ const translations = {
roomNameInvalidError: 'Los nombres de las salas solo pueden contener minúsculas, números y guiones.',
pleaseEnterRoomName: 'Por favor, escribe el nombre de una sala.',
pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo.',
- renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => `cambió el nombre de la sala de ${oldName} a ${newName}`,
+ renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => `cambió el nombre de la sala a "${newName}" (previamente "${oldName}")`,
roomRenamedTo: ({newName}: RoomRenamedToParams) => `Sala renombrada a ${newName}`,
social: 'social',
selectAWorkspace: 'Seleccionar un espacio de trabajo',
@@ -4502,7 +4495,7 @@ const translations = {
},
},
workspaceActions: {
- renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `actualizó el nombre de este espacio de trabajo de ${oldName} a ${newName}`,
+ renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `actualizó el nombre de este espacio de trabajo a "${newName}" (previamente "${oldName}")`,
removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => {
let joinedNames = '';
if (submittersNames.length === 1) {
@@ -4602,7 +4595,6 @@ const translations = {
},
},
saveSearch: 'Guardar búsqueda',
- saveSearchTooltipText: 'Puedes cambiar el nombre de tu búsqueda guardada',
savedSearchesMenuItemTitle: 'Guardadas',
searchName: 'Nombre de la búsqueda',
deleteSavedSearch: 'Eliminar búsqueda guardada',
@@ -5975,6 +5967,25 @@ const translations = {
crossPlatform: 'Haz todo desde tu teléfono o navegador',
},
},
+ productTrainingTooltip: {
+ conciergeLHNGBR: {
+ part1: '¡Comienza',
+ part2: ' aquí!',
+ },
+ saveSearchTooltip: {
+ part1: 'Renombra tus búsquedas guardadas',
+ part2: ' aquí',
+ },
+ quickActionButton: {
+ part1: '¡Acción rápida!',
+ part2: ' A solo un toque',
+ },
+ workspaceChatCreate: {
+ part1: 'Envía tus',
+ part2: ' gastos',
+ part3: ' aquí',
+ },
+ },
};
export default translations satisfies TranslationDeepObject;
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index 3b8e26c9cd33..fe40ea67f905 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -1,15 +1,14 @@
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {LastSelectedDistanceRates, OnyxInputOrEntry, Transaction} from '@src/types/onyx';
+import type {LastSelectedDistanceRates, OnyxInputOrEntry, Report, Transaction} from '@src/types/onyx';
import type {Unit} from '@src/types/onyx/Policy';
import type Policy from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as CurrencyUtils from './CurrencyUtils';
import * as PolicyUtils from './PolicyUtils';
-import * as ReportConnection from './ReportConnection';
import * as ReportUtils from './ReportUtils';
import * as TransactionUtils from './TransactionUtils';
@@ -30,6 +29,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
const METERS_TO_KM = 0.001; // 1 kilometer is 1000 meters
const METERS_TO_MILES = 0.000621371; // There are approximately 0.000621371 miles in a meter
@@ -282,7 +290,6 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number {
* Returns custom unit rate ID for the distance transaction
*/
function getCustomUnitRateID(reportID: string) {
- const allReports = ReportConnection.getAllReports();
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID ?? '-1');
diff --git a/src/libs/E2E/tests/linkingTest.e2e.ts b/src/libs/E2E/tests/linkingTest.e2e.ts
index 2a85a5dabe6c..38c659c0efe3 100644
--- a/src/libs/E2E/tests/linkingTest.e2e.ts
+++ b/src/libs/E2E/tests/linkingTest.e2e.ts
@@ -25,6 +25,9 @@ const test = (config: NativeConfig) => {
const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config);
const name = getConfigValueOrThrow('name', config);
+ const startTestTime = Date.now();
+ console.debug('[E2E] Test started at:', startTestTime);
+
E2ELogin().then((neededLogin) => {
if (neededLogin) {
return waitForAppLoaded().then(() => E2EClient.submitTestDone());
@@ -32,39 +35,70 @@ const test = (config: NativeConfig) => {
const [appearMessagePromise, appearMessageResolve] = getPromiseWithResolve();
const [openReportPromise, openReportResolve] = getPromiseWithResolve();
+ let lastVisibleMessageId: string | undefined;
+ let verificationStarted = false;
+ let hasNavigatedToLinkedMessage = false;
+
+ const subscription = DeviceEventEmitter.addListener('onViewableItemsChanged', (res: ViewableItemResponse) => {
+ console.debug('[E2E] Viewable items event triggered at:', Date.now());
+
+ // update the last visible message
+ lastVisibleMessageId = res?.at(0)?.item?.reportActionID;
+ console.debug('[E2E] Current visible message:', lastVisibleMessageId);
+
+ if (!verificationStarted && lastVisibleMessageId === linkedReportActionID) {
+ console.debug('[E2E] Target message found, starting verification');
+ verificationStarted = true;
+
+ setTimeout(() => {
+ console.debug('[E2E] Verification timeout completed');
+ console.debug('[E2E] Last visible message ID:', lastVisibleMessageId);
+ console.debug('[E2E] Expected message ID:', linkedReportActionID);
+
+ subscription.remove();
+ if (lastVisibleMessageId === linkedReportActionID) {
+ console.debug('[E2E] Message position verified successfully');
+ appearMessageResolve();
+ } else {
+ console.debug('[E2E] Linked message not found, failing test!');
+ E2EClient.submitTestResults({
+ branch: Config.E2E_BRANCH,
+ error: 'Linked message not found',
+ name: `${name} test can't find linked message`,
+ }).then(() => E2EClient.submitTestDone());
+ }
+ }, 3000);
+ }
+ });
Promise.all([appearMessagePromise, openReportPromise])
.then(() => {
- console.debug('[E2E] Test completed successfully, exiting…');
+ console.debug('[E2E] Test completed successfully at:', Date.now());
+ console.debug('[E2E] Total test duration:', Date.now() - startTestTime, 'ms');
E2EClient.submitTestDone();
})
.catch((err) => {
console.debug('[E2E] Error while submitting test results:', err);
});
- const subscription = DeviceEventEmitter.addListener('onViewableItemsChanged', (res: ViewableItemResponse) => {
- console.debug('[E2E] Viewable items retrieved, verifying correct message…', res);
-
- if (!!res && res?.at(0)?.item?.reportActionID === linkedReportActionID) {
- appearMessageResolve();
- subscription.remove();
- } else {
- console.debug(`[E2E] Provided message id '${res?.at(0)?.item?.reportActionID}' doesn't match to an expected '${linkedReportActionID}'. Waiting for a next one…`);
- }
- });
-
Performance.subscribeToMeasurements((entry) => {
+ console.debug(`[E2E] Performance entry captured: ${entry.name} at ${entry.startTime}, duration: ${entry.duration} ms`);
+
if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
- console.debug('[E2E] Sidebar loaded, navigating to a report…');
+ console.debug('[E2E] Sidebar loaded, navigating to a report at:', Date.now());
+ const startNavigateTime = Date.now();
Performance.markStart(CONST.TIMING.OPEN_REPORT);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
+ console.debug('[E2E] Navigation to report took:', Date.now() - startNavigateTime, 'ms');
return;
}
- if (entry.name === CONST.TIMING.OPEN_REPORT) {
- console.debug('[E2E] Linking: 1');
- console.debug('[E2E] Navigating to the linked report action…');
+ if (entry.name === CONST.TIMING.OPEN_REPORT && !hasNavigatedToLinkedMessage) {
+ console.debug('[E2E] Navigating to the linked report action at:', Date.now());
+ const startLinkedNavigateTime = Date.now();
+ hasNavigatedToLinkedMessage = true; // Set flag to prevent duplicate navigation
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(linkedReportID, linkedReportActionID));
+ console.debug('[E2E] Navigation to linked report took:', Date.now() - startLinkedNavigateTime, 'ms');
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts
index 9cc38aad3346..4ead6016418f 100644
--- a/src/libs/ExportOnyxState/common.ts
+++ b/src/libs/ExportOnyxState/common.ts
@@ -3,6 +3,23 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Session} from '@src/types/onyx';
const MASKING_PATTERN = '***';
+const keysToMask = [
+ 'plaidLinkToken',
+ 'plaidAccessToken',
+ 'plaidAccountID',
+ 'addressName',
+ 'addressCity',
+ 'addressStreet',
+ 'addressZipCode',
+ 'street',
+ 'city',
+ 'state',
+ 'zip',
+ 'edits',
+ 'lastMessageHtml',
+ 'lastMessageText',
+];
+
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
const emailMap = new Map();
@@ -94,12 +111,18 @@ const maskFragileData = (data: Record | unknown[] | null, paren
const value = data[propertyName];
- if (typeof value === 'string' && Str.isValidEmail(value)) {
+ if (keysToMask.includes(key)) {
+ if (Array.isArray(value)) {
+ maskedData[key] = value.map(() => MASKING_PATTERN);
+ } else {
+ maskedData[key] = MASKING_PATTERN;
+ }
+ } else if (typeof value === 'string' && Str.isValidEmail(value)) {
maskedData[propertyName] = maskEmail(value);
} else if (typeof value === 'string' && stringContainsEmail(value)) {
maskedData[propertyName] = replaceEmailInString(value, maskEmail(extractEmail(value) ?? ''));
} else if (parentKey && parentKey.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) && (propertyName === 'text' || propertyName === 'html')) {
- maskedData[propertyName] = MASKING_PATTERN;
+ maskedData[key] = MASKING_PATTERN;
} else if (typeof value === 'object') {
maskedData[propertyName] = maskFragileData(value as Record, propertyName.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS) ? propertyName : parentKey);
} else {
diff --git a/src/libs/Firebase/utils.ts b/src/libs/Firebase/utils.ts
index 01df2bfc8a7e..385fda4c817b 100644
--- a/src/libs/Firebase/utils.ts
+++ b/src/libs/Firebase/utils.ts
@@ -1,18 +1,30 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import {getAllTransactions, getAllTransactionViolationsLength} from '@libs/actions/Transaction';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import {getActivePolicy, getAllPoliciesLength} from '@libs/PolicyUtils';
import {getReportActionsLength} from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import * as SessionUtils from '@libs/SessionUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report} from '@src/types/onyx';
import type {PerfAttributes} from './types';
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
function getAttributes(attributes?: T[]): Pick {
const session = SessionUtils.getSession();
const policy = getActivePolicy();
const allAttributes: PerfAttributes = {
accountId: session?.accountID?.toString() ?? 'N/A',
- reportsLength: ReportConnection.getAllReportsLength().toString(),
+ reportsLength: Object.keys(allReports ?? {}).length.toString(),
reportActionsLength: getReportActionsLength().toString(),
personalDetailsLength: PersonalDetailsUtils.getPersonalDetailsLength().toString(),
transactionViolationsLength: getAllTransactionViolationsLength().toString(),
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 2ad25f77c249..c3c12600f882 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -2,14 +2,13 @@ import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PolicyTagLists, ReportAction} from '@src/types/onyx';
+import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import * as Localize from './Localize';
import Log from './Log';
import * as PolicyUtils from './PolicyUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
-import * as ReportConnection from './ReportConnection';
import * as TransactionUtils from './TransactionUtils';
let allPolicyTags: OnyxCollection = {};
@@ -25,6 +24,13 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => (allReports = value),
+});
+
/**
* Utility to get message based on boolean literal value.
*/
@@ -137,7 +143,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
return '';
}
const reportActionOriginalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
- const policyID = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';
+ const policyID = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';
const removalFragments: string[] = [];
const setFragments: string[] = [];
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 7d897d485a78..e01d0fe3115f 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -169,7 +169,6 @@ Onyx.connect({
// If the current timezone is different than the user's timezone, and their timezone is set to automatic
// then update their timezone.
if (!isEmptyObject(currentTimezone) && timezone?.automatic && timezone?.selected !== currentTimezone) {
- timezone.selected = currentTimezone;
PersonalDetails.updateAutomaticTimezone({
automatic: true,
selected: currentTimezone,
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index 88a735884493..b478f09c2e01 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -163,7 +163,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
@@ -184,7 +184,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
styles.bottomTabBarLabel,
]}
>
- {translate('common.search')}
+ {translate('common.reports')}
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index d54668bf3f69..eeb6db21447e 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -1,10 +1,10 @@
import {findFocusedRoute} from '@react-navigation/core';
import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native';
import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
import {isCentralPaneName, removePolicyIDParamFromState} from '@libs/NavigationUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
@@ -30,6 +30,15 @@ import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotas
import switchPolicyID from './switchPolicyID';
import type {NavigationStateRoute, RootStackParamList, State, StateOrRoute, SwitchPolicyIDParams} from './types';
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
let resolveNavigationIsReadyPromise: () => void;
const navigationIsReadyPromise = new Promise((resolve) => {
resolveNavigationIsReadyPromise = resolve;
@@ -66,7 +75,7 @@ const dismissModal = (reportID?: string, ref = navigationRef) => {
originalDismissModal(ref);
return;
}
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
originalDismissModalWithReport({reportID, ...report}, ref);
};
// Re-exporting the closeRHPFlow here to fill in default value for navigationRef. The closeRHPFlow isn't defined in this file to avoid cyclic dependencies.
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index b0fba017b367..ef927e6f2cf5 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -1,6 +1,8 @@
import type {NavigationState, PartialState, Route} from '@react-navigation/native';
import {findFocusedRoute, getStateFromPath} from '@react-navigation/native';
import pick from 'lodash/pick';
+import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import type {TupleToUnion} from 'type-fest';
import type {TopTabScreen} from '@components/FocusTrap/TOP_TAB_SCREENS';
import {isAnonymousUser} from '@libs/actions/Session';
@@ -8,13 +10,13 @@ import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import extractPolicyIDFromQuery from '@navigation/extractPolicyIDFromQuery';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Screen} from '@src/SCREENS';
import SCREENS from '@src/SCREENS';
+import type {Report} from '@src/types/onyx';
import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING';
import config, {normalizedConfigs} from './config';
import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING';
@@ -23,6 +25,15 @@ import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteFo
import getOnboardingAdaptedState from './getOnboardingAdaptedState';
import replacePathInNestedState from './replacePathInNestedState';
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
const RHP_SCREENS_OPENED_FROM_LHN = [
SCREENS.SETTINGS.SHARE_CODE,
SCREENS.SETTINGS.PROFILE.STATUS,
@@ -160,7 +171,7 @@ function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): Navigat
// check for valid reportID in the route params
// if the reportID is valid, we should navigate back to screen report in CPN
const reportID = (route.params as Record)?.reportID;
- if (ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) {
+ if (allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) {
return {name: SCREENS.REPORT, params: {reportID}};
}
}
diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
index 34d982469825..61af079f9ed1 100644
--- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
+++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
@@ -1,5 +1,6 @@
import {NativeModules} from 'react-native';
import Onyx from 'react-native-onyx';
+import type {OnyxCollection} from 'react-native-onyx';
import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import Log from '@libs/Log';
@@ -7,7 +8,6 @@ import Navigation from '@libs/Navigation/Navigation';
import type {ReportActionPushNotificationData} from '@libs/Notification/PushNotification/NotificationType';
import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import {extractPolicyIDFromPath} from '@libs/PolicyUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import {doesReportBelongToWorkspace} from '@libs/ReportUtils';
import Visibility from '@libs/Visibility';
import {updateLastVisitedPath} from '@userActions/App';
@@ -15,7 +15,7 @@ import * as Modal from '@userActions/Modal';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {OnyxUpdatesFromServer} from '@src/types/onyx';
+import type {OnyxUpdatesFromServer, Report} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import PushNotification from '..';
@@ -30,6 +30,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
function getLastUpdateIDAppliedToClient(): Promise {
return new Promise((resolve) => {
Onyx.connect({
@@ -77,7 +86,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati
Log.info('[PushNotification] Navigating to report', false, {reportID, reportActionID});
const policyID = lastVisitedPath && extractPolicyIDFromPath(lastVisitedPath);
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : [];
const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID);
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index fc8c16754cb4..a7f738790f92 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -351,7 +351,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant
const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1];
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const login = detail?.login || participant.login || '';
- const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login) || participant.text);
+ const displayName = LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(detail, login || participant.text));
return {
keyForList: String(detail?.accountID),
@@ -401,7 +401,7 @@ function uniqFast(items: string[]): string[] {
function getLastActorDisplayName(lastActorDetails: Partial | null, hasMultipleParticipants: boolean) {
return hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID
? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- lastActorDetails.firstName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails)
+ lastActorDetails.firstName || LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails))
: '';
}
@@ -499,7 +499,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
case CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY:
case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: {
lastMessageTextFromReport = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, {
- displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails),
+ displayName: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails)),
policyName: ReportUtils.getPolicyName(report, false, policy),
});
break;
@@ -1351,10 +1351,10 @@ function getShareLogOptions(options: OptionList, betas: Beta[] = []): Options {
* Build the IOUConfirmation options for showing the payee personalDetail
*/
function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: OnyxEntry, amountText?: string): PayeePersonalDetails {
- const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '');
+ const login = personalDetail?.login ?? '';
return {
- text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin),
- alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false),
+ text: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, login)),
+ alternateText: LocalePhoneNumber.formatPhoneNumber(login || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false)),
icons: [
{
source: personalDetail?.avatar ?? FallbackAvatar,
diff --git a/src/libs/Parser.ts b/src/libs/Parser.ts
index 9d791b1d4f7b..8076496f2f79 100644
--- a/src/libs/Parser.ts
+++ b/src/libs/Parser.ts
@@ -3,10 +3,27 @@ import {ExpensiMark} from 'expensify-common';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import Log from './Log';
-import * as ReportConnection from './ReportConnection';
const accountIDToNameMap: Record = {};
+const reportIDToNameMap: Record = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ if (!value) {
+ return;
+ }
+
+ Object.values(value).forEach((report) => {
+ if (!report) {
+ return;
+ }
+ reportIDToNameMap[report.reportID] = report.reportName ?? report.reportID;
+ });
+ },
+});
+
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (personalDetailsList) => {
@@ -30,7 +47,7 @@ type Extras = {
class ExpensiMarkWithContext extends ExpensiMark {
htmlToMarkdown(htmlString: string, extras?: Extras): string {
return super.htmlToMarkdown(htmlString, {
- reportIDToName: extras?.reportIDToName ?? ReportConnection.getAllReportsNameMap(),
+ reportIDToName: extras?.reportIDToName ?? reportIDToNameMap,
accountIDToName: extras?.accountIDToName ?? accountIDToNameMap,
cacheVideoAttributes: extras?.cacheVideoAttributes,
});
@@ -38,7 +55,7 @@ class ExpensiMarkWithContext extends ExpensiMark {
htmlToText(htmlString: string, extras?: Extras): string {
return super.htmlToText(htmlString, {
- reportIDToName: extras?.reportIDToName ?? ReportConnection.getAllReportsNameMap(),
+ reportIDToName: extras?.reportIDToName ?? reportIDToNameMap,
accountIDToName: extras?.accountIDToName ?? accountIDToNameMap,
cacheVideoAttributes: extras?.cacheVideoAttributes,
});
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index f6b277d69d6b..2647c029aa7f 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -520,7 +520,7 @@ function isPolicyFeatureEnabled(policy: OnyxEntry, featureName: PolicyFe
return !!policy?.[featureName];
}
-function getApprovalWorkflow(policy: OnyxEntry): ValueOf {
+function getApprovalWorkflow(policy: OnyxEntry | SearchPolicy): ValueOf {
if (policy?.type === CONST.POLICY.TYPE.PERSONAL) {
return CONST.POLICY.APPROVAL_MODE.OPTIONAL;
}
@@ -528,14 +528,14 @@ function getApprovalWorkflow(policy: OnyxEntry): ValueOf): string {
+function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string {
return policy?.approver ?? policy?.owner ?? '';
}
/**
* Returns the accountID to whom the given expenseReport submits reports to in the given Policy.
*/
-function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntry): number {
+function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number {
const employeeAccountID = expenseReport?.ownerAccountID ?? -1;
const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? '';
const defaultApprover = getDefaultApprover(policy);
@@ -555,8 +555,8 @@ function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntr
return getAccountIDsByLogins([categoryAppover]).at(0) ?? -1;
}
- if (!tagApprover && getTagApproverRule(policy?.id ?? '-1', tag)?.approver) {
- tagApprover = getTagApproverRule(policy?.id ?? '-1', tag)?.approver;
+ if (!tagApprover && getTagApproverRule(policy ?? '-1', tag)?.approver) {
+ tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver;
}
}
@@ -1084,8 +1084,8 @@ function hasVBBA(policyID: string) {
return !!policy?.achAccount?.bankAccountID;
}
-function getTagApproverRule(policyID: string, tagName: string) {
- const policy = getPolicy(policyID);
+function getTagApproverRule(policyOrID: string | SearchPolicy | OnyxEntry, tagName: string) {
+ const policy = typeof policyOrID === 'string' ? getPolicy(policyOrID) : policyOrID;
const approvalRules = policy?.rules?.approvalRules ?? [];
const approverRule = approvalRules.find((rule) =>
diff --git a/src/libs/ReportActionsConnection.ts b/src/libs/ReportActionsConnection.ts
deleted file mode 100644
index e3c8a4c3cf60..000000000000
--- a/src/libs/ReportActionsConnection.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type {OnyxCollection} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReportActions} from '@src/types/onyx/ReportAction';
-
-let allReportActions: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
- waitForCollectionCallback: true,
- callback: (actions) => {
- if (!actions) {
- return;
- }
-
- allReportActions = actions;
- },
-});
-
-// This function is used to get all reports
-function getAllReportActions() {
- return allReportActions;
-}
-
-// eslint-disable-next-line import/prefer-default-export
-export {getAllReportActions};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 8d828f457ece..1e360855c5f3 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -20,13 +20,13 @@ import DateUtils from './DateUtils';
import * as Environment from './Environment/Environment';
import getBase62ReportID from './getBase62ReportID';
import isReportMessageAttachment from './isReportMessageAttachment';
+import {formatPhoneNumber} from './LocalePhoneNumber';
import * as Localize from './Localize';
import Log from './Log';
import type {MessageElementBase, MessageTextElement} from './MessageElement';
import Parser from './Parser';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PolicyUtils from './PolicyUtils';
-import * as ReportConnection from './ReportConnection';
import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils';
import StringUtils from './StringUtils';
// eslint-disable-next-line import/no-cycle
@@ -63,6 +63,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
let isNetworkOffline = false;
Onyx.connect({
key: ONYXKEYS.NETWORK,
@@ -441,7 +450,7 @@ function getCombinedReportActions(
filteredParentReportActions = reportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED);
}
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const isSelfDM = report?.chatType === CONST.REPORT.CHAT_TYPE.SELF_DM;
// Filter out request and send money request actions because we don't want to show any preview actions for one transaction reports
const filteredReportActions = [...filteredParentReportActions, ...filteredTransactionThreadReportActions].filter((action) => {
@@ -951,7 +960,7 @@ function getMostRecentReportActionLastModified(): string {
// We might not have actions so we also look at the report objects to see if any have a lastVisibleActionLastModified that is more recent. We don't need to get
// any reports that have been updated before either a recently updated report or reportAction as we should be up to date on these
- Object.values(ReportConnection.getAllReports() ?? {}).forEach((report) => {
+ Object.values(allReports ?? {}).forEach((report) => {
const reportLastVisibleActionLastModified = report?.lastVisibleActionLastModified ?? report?.lastVisibleActionCreated;
if (!reportLastVisibleActionLastModified || reportLastVisibleActionLastModified < mostRecentReportActionLastModified) {
return;
@@ -1044,7 +1053,7 @@ const iouRequestTypes = new Set>([
*/
function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined {
// If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report.
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) {
return;
}
@@ -1571,7 +1580,7 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const reportActions = getAllReportActions(report?.reportID ?? '');
const action = Object.values(reportActions ?? {})?.find((reportAction) => {
const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : -1;
@@ -1697,7 +1706,8 @@ function getPolicyChangeLogAddEmployeeMessage(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
diff --git a/src/libs/ReportConnection.ts b/src/libs/ReportConnection.ts
deleted file mode 100644
index 7b61b22681e5..000000000000
--- a/src/libs/ReportConnection.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import type {OnyxCollection} from 'react-native-onyx';
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Report} from '@src/types/onyx';
-import * as PriorityModeActions from './actions/PriorityMode';
-import * as ReportHelperActions from './actions/Report';
-
-// Dynamic Import to avoid circular dependency
-const UnreadIndicatorUpdaterHelper = () => import('./UnreadIndicatorUpdater');
-
-const reportIDToNameMap: Record = {};
-let allReports: OnyxCollection;
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT,
- waitForCollectionCallback: true,
- callback: (value) => {
- allReports = value;
- UnreadIndicatorUpdaterHelper().then((module) => {
- module.triggerUnreadUpdate();
- });
- // Each time a new report is added we will check to see if the user should be switched
- PriorityModeActions.autoSwitchToFocusMode();
-
- if (!value) {
- return;
- }
- Object.values(value).forEach((report) => {
- if (!report) {
- return;
- }
- reportIDToNameMap[report.reportID] = report.reportName ?? report.reportID;
- ReportHelperActions.handleReportChanged(report);
- });
- },
-});
-
-// This function is used to get all reports
-function getAllReports() {
- return allReports;
-}
-
-// This function is used to get all reports name map
-function getAllReportsNameMap() {
- return reportIDToNameMap;
-}
-
-function getAllReportsLength() {
- return Object.keys(allReports ?? {}).length;
-}
-
-function getReport(reportID: string) {
- if (!reportID || !allReports) {
- return;
- }
- return allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
-}
-
-function updateReportData(reportID: string, reportData?: Partial) {
- const report = getReport(reportID);
-
- if (!allReports || !report || !report.reportID) {
- return;
- }
-
- allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = {
- ...report,
- ...reportData,
- };
-}
-
-export {getAllReports, getAllReportsNameMap, getAllReportsLength, updateReportData, getReport};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index a304ee800131..669966321436 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -60,7 +60,9 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import * as IOU from './actions/IOU';
import * as PolicyActions from './actions/Policy/Policy';
+import * as PriorityModeActions from './actions/PriorityMode';
import * as store from './actions/ReimbursementAccount/store';
+import * as ReportHelperActions from './actions/Report';
import * as SessionUtils from './actions/Session';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
@@ -84,12 +86,14 @@ import * as PhoneNumber from './PhoneNumber';
import * as PolicyUtils from './PolicyUtils';
import type {LastVisibleMessage} from './ReportActionsUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
-import * as ReportConnection from './ReportConnection';
import * as TransactionUtils from './TransactionUtils';
import * as Url from './Url';
import type {AvatarSource} from './UserUtils';
import * as UserUtils from './UserUtils';
+// Dynamic Import to avoid circular dependency
+const UnreadIndicatorUpdaterHelper = () => import('./UnreadIndicatorUpdater');
+
type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18;
type SpendBreakdown = {
@@ -627,6 +631,31 @@ Onyx.connect({
callback: (value) => (allPolicies = value),
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ UnreadIndicatorUpdaterHelper().then((module) => {
+ module.triggerUnreadUpdate();
+ });
+
+ // Each time a new report is added we will check to see if the user should be switched
+ PriorityModeActions.autoSwitchToFocusMode();
+
+ if (!value) {
+ return;
+ }
+ Object.values(value).forEach((report) => {
+ if (!report) {
+ return;
+ }
+ ReportHelperActions.handleReportChanged(report);
+ });
+ },
+});
+
let allBetas: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.BETAS,
@@ -744,7 +773,6 @@ function getChatType(report: OnyxInputOrEntry | Participant): ValueOf {
- const allReports = ReportConnection.getAllReports();
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
}
@@ -761,7 +789,7 @@ function isDraftReport(reportID: string | undefined): boolean {
* Returns the report
*/
function getReport(reportID: string): OnyxEntry {
- return ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
}
/**
@@ -778,7 +806,7 @@ function getParentReport(report: OnyxEntry): OnyxEntry {
if (!report?.parentReportID) {
return undefined;
}
- return ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`];
+ return getReport(report.parentReportID);
}
/**
@@ -879,7 +907,7 @@ function isExpenseReport(report: OnyxInputOrEntry | SearchReport): boole
* Checks if a report is an IOU report using report or reportID
*/
function isIOUReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) ?? null : reportOrID;
return report?.type === CONST.REPORT.TYPE.IOU;
}
@@ -944,7 +972,7 @@ function isReportManager(report: OnyxEntry): boolean {
* Checks if the supplied report has been approved
*/
function isReportApproved(reportOrID: OnyxInputOrEntry | string, parentReportAction: OnyxEntry = undefined): boolean {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) ?? null : reportOrID;
if (!report) {
return parentReportAction?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && parentReportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
}
@@ -991,7 +1019,7 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string
if (!reportOrID) {
return false;
}
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) ?? null : reportOrID;
if (!report) {
return false;
}
@@ -1013,7 +1041,6 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string
* Whether the current user is the submitter of the report
*/
function isCurrentUserSubmitter(reportID: string): boolean {
- const allReports = ReportConnection.getAllReports();
if (!allReports) {
return false;
}
@@ -1076,9 +1103,11 @@ function isInvoiceRoom(report: OnyxEntry): boolean {
}
function isInvoiceRoomWithID(reportID?: string): boolean {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`];
- return isInvoiceRoom(report);
+ if (!reportID) {
+ return false;
+ }
+
+ return isInvoiceRoom(getReport(reportID));
}
/**
@@ -1206,7 +1235,7 @@ function isWorkspaceTaskReport(report: OnyxEntry): boolean {
if (!isTaskReport(report)) {
return false;
}
- const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
+ const parentReport = report?.parentReportID ? getReport(report?.parentReportID) : undefined;
return isPolicyExpenseChat(parentReport);
}
@@ -1292,7 +1321,6 @@ function isConciergeChatReport(report: OnyxInputOrEntry): boolean {
}
function findSelfDMReportID(): string | undefined {
- const allReports = ReportConnection.getAllReports();
if (!allReports) {
return;
}
@@ -1413,7 +1441,6 @@ function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = fa
const policyMemberAccountIDs = PolicyUtils.getPolicyEmployeeListByIdWithoutCurrentUser(allPolicies, policyID, currentUserAccountID);
- const allReports = ReportConnection.getAllReports();
let reportsValues = Object.values(allReports ?? {});
if (!!policyID || policyMemberAccountIDs.length > 0) {
@@ -1483,9 +1510,11 @@ function isArchivedRoom(report: OnyxInputOrEntry | SearchReport, reportN
* Whether the report with the provided reportID is an archived room
*/
function isArchivedRoomWithID(reportID?: string) {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`];
- return isArchivedRoom(report, getReportNameValuePairs(reportID));
+ if (!reportID) {
+ return false;
+ }
+
+ return isArchivedRoom(getReport(reportID), getReportNameValuePairs(reportID));
}
/**
@@ -1629,7 +1658,7 @@ function isChildReport(report: OnyxEntry): boolean {
function isExpenseRequest(report: OnyxInputOrEntry): report is Thread {
if (isThread(report)) {
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
- const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
+ const parentReport = getReport(report?.parentReportID);
return isExpenseReport(parentReport) && !isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction);
}
return false;
@@ -1642,7 +1671,7 @@ function isExpenseRequest(report: OnyxInputOrEntry): report is Thread {
function isIOURequest(report: OnyxInputOrEntry): boolean {
if (isThread(report)) {
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
- const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
+ const parentReport = getReport(report?.parentReportID);
return isIOUReport(parentReport) && !isEmptyObject(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction);
}
return false;
@@ -1665,7 +1694,7 @@ function isTrackExpenseReport(report: OnyxInputOrEntry): boolean {
* Checks if a report is an IOU or expense request.
*/
function isMoneyRequest(reportOrID: OnyxEntry | string): boolean {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) ?? null : reportOrID;
return isIOURequest(report) || isExpenseRequest(report);
}
@@ -1673,7 +1702,7 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean {
* Checks if a report is an IOU or expense report.
*/
function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) ?? null : reportOrID;
return isIOUReport(report) || isExpenseReport(report);
}
@@ -1902,7 +1931,7 @@ function getReportRecipientAccountIDs(report: OnyxEntry, currentLoginAcc
// In 1:1 chat threads, the participants will be the same as parent report. If a report is specifically a 1:1 chat thread then we will
// get parent report and use its participants array.
if (isThread(report) && !(isTaskReport(report) || isMoneyRequestReport(report))) {
- const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
+ const parentReport = getReport(report?.parentReportID);
if (isOneOnOneChat(parentReport)) {
finalReport = parentReport;
}
@@ -2160,7 +2189,7 @@ function getDisplayNameForParticipant(
// If the user's personal details (first name) should be hidden, make sure we return "hidden" instead of the short name
if (shouldFallbackToHidden && longName === hiddenTranslation) {
- return longName;
+ return LocalePhoneNumber.formatPhoneNumber(longName);
}
const shortName = personalDetails.firstName ? personalDetails.firstName : longName;
@@ -2348,7 +2377,7 @@ function getIcons(
const actorIcon = {
id: actorAccountID,
source: personalDetails?.[actorAccountID ?? -1]?.avatar ?? FallbackAvatar,
- name: actorDisplayName,
+ name: LocalePhoneNumber.formatPhoneNumber(actorDisplayName),
type: CONST.ICON_TYPE_AVATAR,
fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon,
};
@@ -2573,7 +2602,7 @@ function getReimbursementQueuedActionMessage(
reportOrID: OnyxEntry | string,
shouldUseShortDisplayName = true,
): string {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) : reportOrID;
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, shouldUseShortDisplayName) ?? '';
const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
let messageKey: TranslationPaths;
@@ -2594,7 +2623,7 @@ function getReimbursementDeQueuedActionMessage(
reportOrID: OnyxEntry | string,
isLHNPreview = false,
): string {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) : reportOrID;
const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
const amount = originalMessage?.amount;
const currency = originalMessage?.currency;
@@ -2848,7 +2877,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea
}
function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allReportsDict?: OnyxCollection): SpendBreakdown {
- const allAvailableReports = allReportsDict ?? ReportConnection.getAllReports();
+ const allAvailableReports = allReportsDict ?? allReports;
let moneyRequestReport;
if (isMoneyRequestReport(report) || isInvoiceReport(report)) {
moneyRequestReport = report;
@@ -3226,7 +3255,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxInputOrEntry
}
const iouMessage = ReportActionsUtils.getOriginalMessage(reportAction);
- const moneyRequestReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${iouMessage?.IOUReportID}`] ?? ({} as Report);
+ const moneyRequestReport = iouMessage?.IOUReportID ? getReport(iouMessage?.IOUReportID) ?? ({} as Report) : ({} as Report);
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${iouMessage?.IOUTransactionID}`] ?? ({} as Transaction);
if (isSettled(String(moneyRequestReport.reportID)) || isReportApproved(String(moneyRequestReport.reportID))) {
@@ -3510,7 +3539,7 @@ function getReportPreviewMessage(
isForListPreview = false,
originalReportAction: OnyxInputOrEntry = iouReportAction,
): string {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) : reportOrID;
const reportActionMessage = ReportActionsUtils.getReportActionHtml(iouReportAction);
if (isEmptyObject(report) || !report?.reportID) {
@@ -3810,7 +3839,7 @@ function getInvoicePayerName(report: OnyxEntry, invoiceReceiverPolicy?:
const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL;
if (isIndividual) {
- return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiver.accountID]);
+ return LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiver.accountID]));
}
return getPolicyName(report, false, invoiceReceiverPolicy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]);
@@ -3902,7 +3931,7 @@ function getInvoicesChatName(report: OnyxEntry, receiverPolicy: OnyxEntr
}
if (isIndividual) {
- return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]);
+ return LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]));
}
return getPolicyName(report, false, invoiceReceiverPolicy);
@@ -4050,6 +4079,10 @@ function getReportName(
}
if (isInvoiceReport(report)) {
+ if (!isInvoiceRoom(getReport(report?.chatReportID ?? ''))) {
+ return report?.reportName ?? getMoneyRequestReportName(report, policy, invoiceReceiverPolicy);
+ }
+
formattedName = getMoneyRequestReportName(report, policy, invoiceReceiverPolicy);
}
@@ -4699,7 +4732,7 @@ function buildOptimisticExpenseReport(
// The amount for Expense reports are stored as negative value in the database
const storedTotal = total * -1;
const storedNonReimbursableTotal = nonReimbursableTotal * -1;
- const policyName = getPolicyName(ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]);
+ const policyName = getPolicyName(getReport(chatReportID));
const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency);
const policy = getPolicy(policyID);
@@ -6285,7 +6318,7 @@ function isUnread(report: OnyxEntry): boolean {
}
function isIOUOwnedByCurrentUser(report: OnyxEntry, allReportsDict?: OnyxCollection): boolean {
- const allAvailableReports = allReportsDict ?? ReportConnection.getAllReports();
+ const allAvailableReports = allReportsDict ?? allReports;
if (!report || !allAvailableReports) {
return false;
}
@@ -6373,8 +6406,8 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV
// - Are either open or submitted
// - Belong to the same workspace
// And if any have a violation, then it should have a RBR
- const allReports = Object.values(ReportConnection.getAllReports() ?? {}) as Report[];
- const potentialReports = allReports.filter((r) => r?.ownerAccountID === currentUserAccountID && (r?.stateNum ?? 0) <= 1 && r?.policyID === report.policyID);
+ const reports = Object.values(allReports ?? {}) as Report[];
+ const potentialReports = reports.filter((r) => r?.ownerAccountID === currentUserAccountID && (r?.stateNum ?? 0) <= 1 && r?.policyID === report.policyID);
return potentialReports.some(
(potentialReport) => hasViolations(potentialReport.reportID, transactionViolations) || hasWarningTypeViolations(potentialReport.reportID, transactionViolations),
);
@@ -6686,7 +6719,7 @@ function shouldReportBeInOptionList(params: ShouldReportBeInOptionListParams) {
/**
* Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, expense, room, and policy expense chat.
*/
-function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = ReportConnection.getAllReports(), shouldIncludeGroupChats = false): OnyxEntry {
+function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports, shouldIncludeGroupChats = false): OnyxEntry {
const sortedNewParticipantList = newParticipantList.sort();
return Object.values(reports ?? {}).find((report) => {
const participantAccountIDs = Object.keys(report?.participants ?? {});
@@ -6711,7 +6744,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec
/**
* Attempts to find an invoice chat report in onyx with the provided policyID and receiverID.
*/
-function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = ReportConnection.getAllReports()): OnyxEntry {
+function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = allReports): OnyxEntry {
return Object.values(reports ?? {}).find((report) => {
if (!report || !isInvoiceRoom(report) || isArchivedRoom(report)) {
return false;
@@ -6730,7 +6763,7 @@ function getInvoiceChatByParticipants(policyID: string, receiverID: string | num
* Attempts to find a policy expense report in onyx that is owned by ownerAccountID in a given policy
*/
function getPolicyExpenseChat(ownerAccountID: number, policyID: string): OnyxEntry {
- return Object.values(ReportConnection.getAllReports() ?? {}).find((report: OnyxEntry) => {
+ return Object.values(allReports ?? {}).find((report: OnyxEntry) => {
// If the report has been deleted, then skip it
if (!report) {
return false;
@@ -6741,7 +6774,7 @@ function getPolicyExpenseChat(ownerAccountID: number, policyID: string): OnyxEnt
}
function getAllPolicyReports(policyID: string): Array> {
- return Object.values(ReportConnection.getAllReports() ?? {}).filter((report) => report?.policyID === policyID);
+ return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID);
}
/**
@@ -6753,9 +6786,11 @@ function chatIncludesChronos(report: OnyxInputOrEntry): boolean {
}
function chatIncludesChronosWithID(reportID?: string): boolean {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`];
- return chatIncludesChronos(report);
+ if (!reportID) {
+ return false;
+ }
+
+ return chatIncludesChronos(getReport(reportID));
}
/**
@@ -6906,7 +6941,7 @@ function getReportIDFromLink(url: string | null): string {
*/
function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxInputOrEntry): boolean {
if (chatReport?.iouReportID) {
- const iouReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`];
+ const iouReport = getReport(chatReport.iouReportID);
if (iouReport?.isWaitingOnBankAccount && iouReport?.ownerAccountID === currentUserAccountID) {
return true;
}
@@ -7234,7 +7269,6 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean {
* Return true if reports data exists
*/
function isReportDataReady(): boolean {
- const allReports = ReportConnection.getAllReports();
return !isEmptyObject(allReports) && Object.keys(allReports ?? {}).some((key) => allReports?.[key]?.reportID);
}
@@ -7258,7 +7292,7 @@ function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Error
* Return true if the expense report is marked for deletion.
*/
function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | string): boolean {
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID;
+ const report = typeof reportOrID === 'string' ? getReport(reportOrID) : reportOrID;
if (!isMoneyRequestReport(report)) {
return false;
}
@@ -7288,7 +7322,7 @@ function getOriginalReportID(reportID: string, reportAction: OnyxInputOrEntry, policy: OnyxEntry,
return requestOptions.includes(iouType);
}
-function getWorkspaceChats(policyID: string, accountIDs: number[], allReports: OnyxCollection = ReportConnection.getAllReports()): Array> {
- return Object.values(allReports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '-1') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1));
+function getWorkspaceChats(policyID: string, accountIDs: number[], reports: OnyxCollection = allReports): Array> {
+ return Object.values(reports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '-1') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1));
}
/**
@@ -7338,7 +7372,6 @@ function getWorkspaceChats(policyID: string, accountIDs: number[], allReports: O
* @param policyID - the workspace ID to get all associated reports
*/
function getAllWorkspaceReports(policyID: string): Array> {
- const allReports = ReportConnection.getAllReports();
return Object.values(allReports ?? {}).filter((report) => (report?.policyID ?? '-1') === policyID);
}
@@ -7654,7 +7687,7 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean {
}
function getRoom(type: ValueOf, policyID: string): OnyxEntry {
- const room = Object.values(ReportConnection.getAllReports() ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report));
+ const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report));
return room;
}
@@ -8049,7 +8082,7 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxInputOrEntry report && report?.[reportFieldToCompare] === tripRoomReportID)
.map((report) => report?.reportID);
return tripTransactionReportIDs.flatMap((reportID) => reportsTransactions[reportID ?? ''] ?? []);
@@ -8302,7 +8335,6 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: s
}
function shouldShowMerchantColumn(transactions: Transaction[]) {
- const allReports = ReportConnection.getAllReports();
return transactions.some((transaction) => isExpenseReport(allReports?.[transaction.reportID] ?? null));
}
@@ -8325,7 +8357,7 @@ function isChatUsedForOnboarding(optionOrReport: OnyxEntry | OptionData)
* we also used the system DM for A/B tests.
*/
function getChatUsedForOnboarding(): OnyxEntry {
- return Object.values(ReportConnection.getAllReports() ?? {}).find(isChatUsedForOnboarding);
+ return Object.values(allReports ?? {}).find(isChatUsedForOnboarding);
}
/**
@@ -8367,7 +8399,7 @@ function getReportViolations(reportID: string): ReportViolations | undefined {
}
function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry {
- return Object.values(ReportConnection.getAllReports() ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID);
+ return Object.values(allReports ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID);
}
/**
@@ -8474,8 +8506,8 @@ function hasMissingInvoiceBankAccount(iouReportID: string): boolean {
}
function hasInvoiceReports() {
- const allReports = Object.values(ReportConnection.getAllReports() ?? {});
- return allReports.some((report) => isInvoiceReport(report));
+ const reports = Object.values(allReports ?? {});
+ return reports.some((report) => isInvoiceReport(report));
}
function getReportMetadata(reportID?: string) {
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index e100fb885fff..b1e522bc566f 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -257,6 +257,12 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr
const transaction = isTransaction ? data[key] : undefined;
const report = isTransaction ? data[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] : data[key];
+ // We need to check both options for a falsy value since the transaction might not have an error but the report associated with it might
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (transaction?.hasError || report.hasError) {
+ return CONST.SEARCH.ACTION_TYPES.REVIEW;
+ }
+
if (ReportUtils.isSettled(report)) {
return CONST.SEARCH.ACTION_TYPES.PAID;
}
@@ -295,10 +301,16 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr
return CONST.SEARCH.ACTION_TYPES.PAY;
}
- if (IOU.canApproveIOU(report, policy) && ReportUtils.isAllowedToApproveExpenseReport(report, undefined, policy)) {
+ const isAllowedToApproveExpenseReport = ReportUtils.isAllowedToApproveExpenseReport(report, undefined, policy);
+ if (IOU.canApproveIOU(report, policy) && isAllowedToApproveExpenseReport) {
return CONST.SEARCH.ACTION_TYPES.APPROVE;
}
+ // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead
+ if (IOU.canSubmitReport(report, policy) && isAllowedToApproveExpenseReport) {
+ return CONST.SEARCH.ACTION_TYPES.SUBMIT;
+ }
+
return CONST.SEARCH.ACTION_TYPES.VIEW;
}
@@ -585,4 +597,5 @@ export {
getExpenseTypeTranslationKey,
getOverflowMenu,
isCorrectSearchUserName,
+ isReportActionEntry,
};
diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts
index 11434373dafb..975c3e617ce5 100644
--- a/src/libs/TaskUtils.ts
+++ b/src/libs/TaskUtils.ts
@@ -1,4 +1,5 @@
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -8,7 +9,15 @@ import type ReportAction from '@src/types/onyx/ReportAction';
import * as Localize from './Localize';
import Navigation from './Navigation/Navigation';
import {getReportActionHtml, getReportActionText} from './ReportActionsUtils';
-import * as ReportConnection from './ReportConnection';
+
+let allReports: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
/**
* Check if the active route belongs to task edit flow.
@@ -46,7 +55,7 @@ function getTaskTitleFromReport(taskReport: OnyxEntry, fallbackTitle = '
}
function getTaskTitle(taskReportID: string, fallbackTitle = ''): string {
- const taskReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`];
+ const taskReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`];
return getTaskTitleFromReport(taskReport, fallbackTitle);
}
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 944c431aaad9..81a738f724e0 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -19,7 +19,6 @@ import {getCleanedTagName, getDistanceRateCustomUnitRate} from '@libs/PolicyUtil
import * as PolicyUtils from '@libs/PolicyUtils';
// eslint-disable-next-line import/no-cycle
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
@@ -45,6 +44,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
let allTransactionViolations: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
@@ -232,8 +240,7 @@ function isCreatedMissing(transaction: OnyxEntry) {
}
function areRequiredFieldsEmpty(transaction: OnyxEntry): boolean {
- const allReports = ReportConnection.getAllReports();
- const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] ?? null;
+ const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
const isFromExpenseReport = parentReport?.type === CONST.REPORT.TYPE.EXPENSE;
const isSplitPolicyExpenseChat = !!transaction?.comment?.splits?.some((participant) => allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`]?.isOwnPolicyExpenseChat);
const isMerchantRequired = isFromExpenseReport || isSplitPolicyExpenseChat;
@@ -1119,7 +1126,7 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor
const keys = fieldsToCompare[fieldName];
const firstTransaction = transactions.at(0);
const isFirstTransactionCommentEmptyObject = typeof firstTransaction?.comment === 'object' && firstTransaction?.comment?.comment === '';
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null;
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const policy = PolicyUtils.getPolicy(report?.policyID);
const areAllFieldsEqualForKey = areAllFieldsEqual(transactions, (item) => keys.map((key) => item?.[key]).join('|'));
@@ -1194,7 +1201,7 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor
}
function getTransactionID(threadReportID: string): string {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`] ?? null;
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`];
const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts
index f2ce5113af81..6379c3612539 100644
--- a/src/libs/TripReservationUtils.ts
+++ b/src/libs/TripReservationUtils.ts
@@ -108,7 +108,7 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag
}
Log.info('[HybridApp] Returning to OldDot after opening TravelDot');
- NativeModules.HybridAppModule.closeReactNativeApp(false, false);
+ NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
})
?.catch(() => {
setCtaErrorMessage(translate('travel.errorMessage'));
diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts
index f4b8e3281308..4d38d410cbfd 100644
--- a/src/libs/UnreadIndicatorUpdater/index.ts
+++ b/src/libs/UnreadIndicatorUpdater/index.ts
@@ -1,13 +1,23 @@
import debounce from 'lodash/debounce';
import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
import memoize from '@libs/memoize';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import Navigation, {navigationRef} from '@navigation/Navigation';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
import updateUnread from './updateUnread';
+let allReports: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
function getUnreadReportsForUnreadIndicator(reports: OnyxCollection, currentReportID: string) {
return Object.values(reports ?? {}).filter((report) => {
const notificationPreference = ReportUtils.getReportNotificationPreference(report);
@@ -42,7 +52,7 @@ const triggerUnreadUpdate = debounce(() => {
const currentReportID = navigationRef?.isReady?.() ? Navigation.getTopmostReportId() ?? '-1' : '-1';
// We want to keep notification count consistent with what can be accessed from the LHN list
- const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(ReportConnection.getAllReports(), currentReportID);
+ const unreadReports = memoizedGetUnreadReportsForUnreadIndicator(allReports, currentReportID);
updateUnread(unreadReports.length);
}, CONST.TIMING.UNREAD_UPDATE_DEBOUNCE_TIME);
diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts
index f99400d87d3e..3901c63ad8dd 100644
--- a/src/libs/actions/Delegate.ts
+++ b/src/libs/actions/Delegate.ts
@@ -140,7 +140,7 @@ function connect(email: string) {
confirmReadyToOpenApp();
openApp();
- NativeModules.HybridAppModule.switchAccount(email);
+ NativeModules.HybridAppModule.switchAccount({newDotCurrentAccount: email});
});
})
.catch((error) => {
@@ -210,7 +210,7 @@ function disconnect() {
confirmReadyToOpenApp();
openApp();
- NativeModules.HybridAppModule.switchAccount(getCurrentUserEmail() ?? '');
+ NativeModules.HybridAppModule.switchAccount({newDotCurrentAccount: getCurrentUserEmail() ?? ''});
});
})
.catch((error) => {
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 6c804ba8d256..c094873379b9 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -48,7 +48,6 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as SessionUtils from '@libs/SessionUtils';
@@ -348,6 +347,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
let userAccountID = -1;
let currentUserEmail = '';
Onyx.connect({
@@ -658,7 +666,7 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR
if (TransactionUtils.isDistanceRequest(transaction)) {
newQuickAction = CONST.QUICK_ACTIONS.REQUEST_DISTANCE;
}
- const existingTransactionThreadReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null;
+ const existingTransactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null;
if (chat.report) {
optimisticData.push({
@@ -1471,7 +1479,7 @@ function buildOnyxDataForTrackExpense(
} else if (isDistanceRequest) {
newQuickAction = CONST.QUICK_ACTIONS.TRACK_DISTANCE;
}
- const existingTransactionThreadReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null;
+ const existingTransactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingTransactionThreadReportID}`] ?? null;
if (chatReport) {
optimisticData.push(
@@ -1834,7 +1842,6 @@ function getDeleteTrackExpenseInformation(
actionableWhisperReportActionID = '',
resolution = '',
) {
- const allReports = ReportConnection.getAllReports();
// STEP 1: Get all collections we're updating
const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null;
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
@@ -2193,7 +2200,6 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma
let isNewChatReport = false;
let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null;
- const allReports = ReportConnection.getAllReports();
// If this is a policyExpenseChat, the chatReport must exist and we can get it from Onyx.
// report is null if the flow is initiated from the global create menu. However, participant always stores the reportID if it exists, which is the case for policyExpenseChats
if (!chatReport && isPolicyExpenseChat) {
@@ -2427,7 +2433,7 @@ function getTrackExpenseInformation(
// STEP 1: Get existing chat report
let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null;
- const allReports = ReportConnection.getAllReports();
+
// The chatReport always exists, and we can get it from Onyx if chatReport is null.
if (!chatReport) {
chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null;
@@ -2673,7 +2679,6 @@ function getUpdateMoneyRequestParams(
const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null]));
const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}]));
- const allReports = ReportConnection.getAllReports();
// Step 2: Get all the collections being updated
const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
@@ -3016,7 +3021,6 @@ function getUpdateTrackExpenseParams(
const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null]));
const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}]));
- const allReports = ReportConnection.getAllReports();
// Step 2: Get all the collections being updated
const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
@@ -3186,7 +3190,6 @@ function updateMoneyRequestDate(
const transactionChanges: TransactionChanges = {
created: value,
};
- const allReports = ReportConnection.getAllReports();
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
let data: UpdateMoneyRequestData;
@@ -3227,7 +3230,6 @@ function updateMoneyRequestMerchant(
const transactionChanges: TransactionChanges = {
merchant: value,
};
- const allReports = ReportConnection.getAllReports();
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
let data: UpdateMoneyRequestData;
@@ -3318,6 +3320,7 @@ type UpdateMoneyRequestDistanceParams = {
policy?: OnyxEntry;
policyTagList?: OnyxEntry;
policyCategories?: OnyxEntry;
+ transactionBackup: OnyxEntry;
};
/** Updates the waypoints of a distance expense */
@@ -3329,12 +3332,12 @@ function updateMoneyRequestDistance({
policy = {} as OnyxTypes.Policy,
policyTagList = {},
policyCategories = {},
+ transactionBackup,
}: UpdateMoneyRequestDistanceParams) {
const transactionChanges: TransactionChanges = {
waypoints: sanitizeRecentWaypoints(waypoints),
routes,
};
- const allReports = ReportConnection.getAllReports();
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
let data: UpdateMoneyRequestData;
@@ -3352,6 +3355,39 @@ function updateMoneyRequestDistance({
value: recentServerValidatedWaypoints,
});
+ if (transactionBackup) {
+ const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+
+ // We need to include all keys of the optimisticData's waypoints in the failureData for onyx merge to properly reset
+ // waypoint keys that do not exist in the failureData's waypoints. For instance, if the optimisticData waypoints had
+ // three keys and the failureData waypoint had only 2 keys then the third key that doesn't exist in the failureData
+ // waypoints should be explicitly reset otherwise onyx merge will leave it intact.
+ const allWaypointKeys = [...new Set([...Object.keys(transactionBackup.comment?.waypoints ?? {}), ...Object.keys(transaction?.comment?.waypoints ?? {})])];
+ const onyxWaypoints = allWaypointKeys.reduce((acc: NullishDeep, key) => {
+ acc[key] = transactionBackup.comment?.waypoints?.[key] ? {...transactionBackup.comment?.waypoints?.[key]} : null;
+ return acc;
+ }, {});
+ const allModifiedWaypointsKeys = [...new Set([...Object.keys(waypoints ?? {}), ...Object.keys(transaction?.modifiedWaypoints ?? {})])];
+ const onyxModifiedWaypoints = allModifiedWaypointsKeys.reduce((acc: NullishDeep, key) => {
+ acc[key] = transactionBackup.modifiedWaypoints?.[key] ? {...transactionBackup.modifiedWaypoints?.[key]} : null;
+ return acc;
+ }, {});
+ onyxData?.failureData?.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ comment: {
+ waypoints: onyxWaypoints,
+ customUnit: {
+ quantity: transactionBackup?.comment?.customUnit?.quantity,
+ },
+ },
+ modifiedWaypoints: onyxModifiedWaypoints,
+ routes: null,
+ },
+ });
+ }
+
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE, params, onyxData);
}
@@ -3383,7 +3419,6 @@ function updateMoneyRequestDescription(
const transactionChanges: TransactionChanges = {
comment,
};
- const allReports = ReportConnection.getAllReports();
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
let data: UpdateMoneyRequestData;
@@ -3412,7 +3447,6 @@ function updateMoneyRequestDistanceRate(
...(typeof updatedTaxAmount === 'number' ? {taxAmount: updatedTaxAmount} : {}),
...(updatedTaxCode ? {taxCode: updatedTaxCode} : {}),
};
- const allReports = ReportConnection.getAllReports();
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
@@ -4087,7 +4121,7 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string,
const existingChatReportID = existingSplitChatReportID || (participants.at(0)?.reportID ?? '-1');
// Check if the report is available locally if we do have one
- let existingSplitChatReport = existingChatReportID ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`] : null;
+ let existingSplitChatReport = existingChatReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`] : null;
const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID];
if (!existingSplitChatReport) {
@@ -4386,9 +4420,7 @@ function createSplitsAndOnyxData(
}
// STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one
- let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID
- ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`]
- : null;
+ let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null;
const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport);
if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) {
@@ -5151,7 +5183,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
let oneOnOneChatReport: OnyxEntry;
let isNewOneOnOneChatReport = false;
- const allReports = ReportConnection.getAllReports();
+
if (isPolicyExpenseChat) {
// The workspace chat reportID is saved in the splits array when starting a split expense with a workspace
oneOnOneChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`];
@@ -5508,7 +5540,6 @@ function updateMoneyRequestAmountAndCurrency({
taxCode,
taxAmount,
};
- const allReports = ReportConnection.getAllReports();
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
let data: UpdateMoneyRequestData;
@@ -5529,7 +5560,6 @@ function updateMoneyRequestAmountAndCurrency({
*/
function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.ReportAction) {
// STEP 1: Get all collections we're updating
- const allReports = ReportConnection.getAllReports();
const iouReportID = ReportActionsUtils.isMoneyRequestAction(reportAction) ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID : '-1';
const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null;
const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`];
@@ -5694,7 +5724,7 @@ function getNavigationUrlOnMoneyRequestDelete(transactionID: string, reportActio
* @returns The URL to navigate to
*/
function getNavigationUrlAfterTrackExpenseDelete(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false): Route | undefined {
- const chatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null;
+ const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null;
// If not a self DM, handle it as a regular money request
if (!ReportUtils.isSelfDM(chatReport)) {
@@ -5737,27 +5767,28 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo
// build Onyx data
// Onyx operations to delete the transaction, update the IOU report action and chat report action
+ const reportActionsOnyxUpdates: OnyxUpdate[] = [];
const onyxUpdates: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: null,
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`,
- value: {
- [reportAction.reportActionID]: shouldDeleteIOUReport
- ? null
- : {
- pendingAction: null,
- },
- },
- },
];
+ reportActionsOnyxUpdates.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`,
+ value: {
+ [reportAction.reportActionID]: shouldDeleteIOUReport
+ ? null
+ : {
+ pendingAction: null,
+ },
+ },
+ });
if (reportPreviewAction?.reportActionID) {
- onyxUpdates.push({
+ reportActionsOnyxUpdates.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
value: {
@@ -5794,12 +5825,12 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo
}
// added operations to update IOU report and chat report
+ reportActionsOnyxUpdates.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`,
+ value: updatedReportAction,
+ });
onyxUpdates.push(
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`,
- value: updatedReportAction,
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`,
@@ -5850,9 +5881,14 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo
);
}
- Onyx.update(onyxUpdates);
-
- return urlToNavigateBack;
+ // First, update the reportActions to ensure related actions are not displayed.
+ Onyx.update(reportActionsOnyxUpdates).then(() => {
+ Navigation.goBack(urlToNavigateBack);
+ InteractionManager.runAfterInteractions(() => {
+ // After navigation, update the remaining data.
+ Onyx.update(onyxUpdates);
+ });
+ });
}
/**
@@ -6127,7 +6163,7 @@ function deleteTrackExpense(chatReportID: string, transactionID: string, reportA
const urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete(chatReportID, transactionID, reportAction, isSingleTransactionView);
// STEP 1: Get all collections we're updating
- const chatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null;
+ const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null;
if (!ReportUtils.isSelfDM(chatReport)) {
deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView);
return urlToNavigateBack;
@@ -7098,6 +7134,7 @@ function canIOUBePaid(
onlyShowPayElsewhere = false,
chatReportRNVP?: OnyxTypes.ReportNameValuePairs,
invoiceReceiverPolicy?: SearchPolicy,
+ shouldCheckApprovedState = true,
) {
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport);
const reportNameValuePairs = chatReportRNVP ?? ReportUtils.getReportNameValuePairs(chatReport?.reportID);
@@ -7152,11 +7189,22 @@ function canIOUBePaid(
reimbursableSpend !== 0 &&
!isChatReportArchived &&
!isAutoReimbursable &&
- !shouldBeApproved &&
+ (!shouldBeApproved || !shouldCheckApprovedState) &&
!isPayAtEndExpenseReport
);
}
+function canSubmitReport(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy) {
+ const currentUserAccountID = Report.getCurrentUserAccountID();
+ const isOpenExpenseReport = ReportUtils.isOpenExpenseReport(report);
+ const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report);
+ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+
+ // This logic differs from the one in MoneyRequestHeader
+ // We are intentionally doing this for now because Auth violations are not ready and thus not returned by Search results. Additionally, the risk of a customer having either RTER or Broken connection violation is really small in the current cohort.
+ return isOpenExpenseReport && reimbursableSpend !== 0 && (report?.ownerAccountID === currentUserAccountID || isAdmin || report?.managerID === currentUserAccountID);
+}
+
function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry, excludedIOUReportID: string): OnyxEntry {
const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {};
@@ -8137,7 +8185,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string, sea
const updatedViolations = [...transactionViolations, newViolation];
const parentReportActionOptimistic = ReportUtils.getOptimisticDataForParentReportAction(reportID, createdReportActionComment.created, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- const iouReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
+ const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
const optimisticData: OnyxUpdate[] = [
{
@@ -8260,7 +8308,7 @@ function unholdRequest(transactionID: string, reportID: string, searchHash?: num
const createdReportAction = ReportUtils.buildOptimisticUnHoldReportAction();
const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- const iouReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
+ const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
const optimisticData: OnyxUpdate[] = [
{
@@ -8491,7 +8539,7 @@ function mergeDuplicates(params: TransactionMergeParams) {
return total + duplicateTransaction.amount;
}, 0);
- const expenseReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`];
+ const expenseReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`];
const expenseReportOptimisticData: OnyxUpdate = {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`,
@@ -8789,5 +8837,6 @@ export {
getIOUReportActionToApproveOrPay,
getNavigationUrlOnMoneyRequestDelete,
getNavigationUrlAfterTrackExpenseDelete,
+ canSubmitReport,
};
export type {GPSPoint as GpsPoint, IOURequestType};
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 25f50519ff14..f81539d1e921 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -71,8 +71,6 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
import {navigateWhenEnableFeature} from '@libs/PolicyUtils';
-import * as ReportActionsConnection from '@libs/ReportActionsConnection';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover';
@@ -88,6 +86,7 @@ import type {
ReimbursementAccount,
Report,
ReportAction,
+ ReportActions,
Request,
TaxRatesWithDefault,
Transaction,
@@ -173,6 +172,24 @@ Onyx.connect({
callback: (value) => (lastAccessedWorkspacePolicyID = value),
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
+let allReportActions: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ allReportActions = actions;
+ },
+});
+
let sessionEmail = '';
let sessionAccountID = 0;
Onyx.connect({
@@ -309,7 +326,7 @@ function deleteWorkspace(policyID: string, policyName: string) {
: []),
];
- const reportsToArchive = Object.values(ReportConnection.getAllReports() ?? {}).filter(
+ const reportsToArchive = Object.values(allReports ?? {}).filter(
(report) => ReportUtils.isPolicyRelatedReport(report, policyID) && (ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report)),
);
const finallyData: OnyxUpdate[] = [];
@@ -2491,7 +2508,7 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF
});
// We need to move the report preview action from the DM to the workspace chat.
- const parentReport = ReportActionsConnection.getAllReportActions()?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`];
+ const parentReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`];
const parentReportActionID = iouReport.parentReportActionID;
const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReport?.[parentReportActionID] : undefined;
diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts
index 2ee1707c21d1..2ada745ca6cf 100644
--- a/src/libs/actions/Policy/ReportField.ts
+++ b/src/libs/actions/Policy/ReportField.ts
@@ -14,7 +14,6 @@ import type {
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import * as WorkspaceReportFieldUtils from '@libs/WorkspaceReportFieldUtils';
import CONST from '@src/CONST';
@@ -39,6 +38,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
const allPolicies: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,
@@ -179,9 +187,7 @@ function createReportField(policyID: string, {name, type, initialValue}: CreateR
value: type === CONST.REPORT_FIELD_TYPES.LIST ? CONST.REPORT_FIELD_TYPES.LIST : null,
};
- const policyExpenseReports = Object.values(ReportConnection.getAllReports() ?? {}).filter(
- (report) => report?.policyID === policyID && report.type === CONST.REPORT.TYPE.EXPENSE,
- ) as Report[];
+ const policyExpenseReports = Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID && report.type === CONST.REPORT.TYPE.EXPENSE) as Report[];
const optimisticData = [
{
diff --git a/src/libs/actions/PriorityMode.ts b/src/libs/actions/PriorityMode.ts
index 2aca5d9f9de8..02cd3a765e8c 100644
--- a/src/libs/actions/PriorityMode.ts
+++ b/src/libs/actions/PriorityMode.ts
@@ -1,10 +1,11 @@
import debounce from 'lodash/debounce';
import Onyx from 'react-native-onyx';
+import type {OnyxCollection} from 'react-native-onyx';
import Log from '@libs/Log';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report} from '@src/types/onyx';
/**
* This actions file is used to automatically switch a user into #focus mode when they exceed a certain number of reports. We do this primarily for performance reasons.
@@ -68,6 +69,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
function resetHasReadRequiredDataFromStorage() {
// Create a new promise and a new resolve function
isReadyPromise = new Promise((resolve) => {
@@ -77,7 +87,7 @@ function resetHasReadRequiredDataFromStorage() {
}
function checkRequiredData() {
- if (ReportConnection.getAllReports() === undefined || hasTriedFocusMode === undefined || isInFocusMode === undefined || isLoadingReportData) {
+ if (allReports === undefined || hasTriedFocusMode === undefined || isInFocusMode === undefined || isLoadingReportData) {
return;
}
@@ -98,7 +108,6 @@ function tryFocusModeUpdate() {
}
const validReports = [];
- const allReports = ReportConnection.getAllReports();
Object.keys(allReports ?? {}).forEach((key) => {
const report = allReports?.[key];
if (!report) {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 06e339ea1696..7baf66adc5c5 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -81,7 +81,6 @@ import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils';
import processReportIDDeeplink from '@libs/processReportIDDeeplink';
import * as Pusher from '@libs/Pusher/pusher';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {doesReportBelongToWorkspace} from '@libs/ReportUtils';
@@ -106,13 +105,13 @@ import type {
PolicyReportField,
QuickAction,
RecentlyUsedReportFields,
+ Report,
ReportAction,
ReportActionReactions,
ReportUserIsTyping,
} from '@src/types/onyx';
import type {Decision} from '@src/types/onyx/OriginalMessage';
import type {ConnectionName} from '@src/types/onyx/Policy';
-import type Report from '@src/types/onyx/Report';
import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report';
import type {Message, ReportActions} from '@src/types/onyx/ReportAction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -220,6 +219,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
let isNetworkOffline = false;
let networkStatus: NetworkStatus;
Onyx.connect({
@@ -320,7 +328,7 @@ registerPaginationConfig({
resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES,
sortItems: (reportActions, reportID) => {
- const report = ReportUtils.getReport(reportID);
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, canUserPerformWriteAction, true);
},
@@ -536,7 +544,7 @@ function addActions(reportID: string, text = '', file?: FileObject) {
lastReadTime: currentTime,
};
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const shouldUpdateNotificationPrefernece = !isEmptyObject(report) && ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
if (shouldUpdateNotificationPrefernece) {
@@ -714,7 +722,7 @@ function updateGroupChatName(reportID: string, reportName: string) {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
- reportName: ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? null,
+ reportName: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? null,
pendingFields: {
reportName: null,
},
@@ -743,7 +751,7 @@ function updateGroupChatAvatar(reportID: string, file?: File | CustomRNImageMani
},
];
- const fetchedReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const fetchedReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -812,7 +820,7 @@ function openReport(
const optimisticReport = reportActionsExist(reportID)
? {}
: {
- reportName: ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME,
+ reportName: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportName ?? CONST.REPORT.DEFAULT_REPORT_NAME,
};
const optimisticData: OnyxUpdate[] = [
@@ -1062,7 +1070,7 @@ function openReport(
}
}
- parameters.clientLastReadTime = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.lastReadTime ?? '';
+ parameters.clientLastReadTime = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.lastReadTime ?? '';
const paginationConfig = {
resourceID: reportID,
@@ -1173,7 +1181,7 @@ function navigateToAndOpenChildReport(childReportID = '-1', parentReportAction:
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
} else {
const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])];
- const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
+ const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
// Threads from DMs and selfDMs don't have a chatType. All other threads inherit the chatType from their parent
const childReportChatType = parentReport && ReportUtils.isSelfDM(parentReport) ? undefined : parentReport?.chatType;
const newChat = ReportUtils.buildOptimisticChatReport(
@@ -1371,8 +1379,7 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) {
// If no action created date is provided, use the last action's from other user
const actionCreationTime =
- reportActionCreated ||
- (latestReportActionFromOtherUsers?.created ?? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0));
+ reportActionCreated || (latestReportActionFromOtherUsers?.created ?? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0));
// We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date
// For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998'
@@ -1546,7 +1553,7 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) {
lastVisibleActionCreated: '',
};
const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions as ReportActions);
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
if (lastMessageText || lastMessageTranslationKey) {
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, canUserPerformWriteAction, optimisticReportActions as ReportActions);
@@ -1689,7 +1696,7 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar
/** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */
function editReportComment(reportID: string, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) {
const originalReportID = ReportUtils.getOriginalReportID(reportID, originalReportAction);
- const report = ReportUtils.getReport(originalReportID ?? '-1');
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`];
const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
if (!originalReportID || !originalReportAction) {
@@ -1932,7 +1939,7 @@ function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction:
}
} else {
const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction?.actorAccountID)])];
- const parentReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
+ const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
const newChat = ReportUtils.buildOptimisticChatReport(
participantAccountIDs,
ReportActionsUtils.getReportActionText(parentReportAction),
@@ -2208,7 +2215,7 @@ function updateDescription(reportID: string, previousValue: string, newValue: st
const parsedDescription = ReportUtils.getParsedComment(newValue, {reportID});
const optimisticDescriptionUpdatedReportAction = ReportUtils.buildOptimisticRoomDescriptionUpdatedReportAction(parsedDescription);
- const report = ReportUtils.getReport(reportID);
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const optimisticData: OnyxUpdate[] = [
{
@@ -2413,7 +2420,7 @@ function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) {
/** Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. */
function deleteReport(reportID: string, shouldDeleteChildReports = false) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const onyxData: Record = {
[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null,
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: null,
@@ -2577,7 +2584,7 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi
}
// We don't want to send a local notification if the user preference is daily, mute or hidden.
- const notificationPreference = ReportUtils.getReportNotificationPreference(ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]);
+ const notificationPreference = ReportUtils.getReportNotificationPreference(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]);
if (notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS) {
Log.info(`${tag} No notification because user preference is to be notified: ${notificationPreference}`);
return false;
@@ -2595,7 +2602,7 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi
return false;
}
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report || (report && report.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
Log.info(`${tag} No notification because the report does not exist or is pending deleted`, false);
return false;
@@ -2630,7 +2637,7 @@ function showReportActionNotification(reportID: string, reportAction: ReportActi
Log.info('[LocalNotification] Creating notification');
const localReportID = `${ONYXKEYS.COLLECTION.REPORT}${reportID}`;
- const report = ReportConnection.getAllReports()?.[localReportID] ?? null;
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report) {
Log.hmmm("[LocalNotification] couldn't show report action notification because the report wasn't found", {localReportID, reportActionID: reportAction.reportActionID});
return;
@@ -2915,7 +2922,7 @@ function joinRoom(report: OnyxEntry) {
}
function leaveGroupChat(reportID: string) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report) {
Log.warn('Attempting to leave Group Chat that does not existing locally');
return;
@@ -2972,7 +2979,7 @@ function leaveGroupChat(reportID: string) {
/** Leave a report by setting the state to submitted and closed */
function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = false) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report) {
return;
@@ -3076,7 +3083,7 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal
/** Invites people to a room */
function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmailsToAccountIDs) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report) {
return;
}
@@ -3181,7 +3188,7 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails
}
function clearAddRoomMemberError(reportID: string, invitedAccountID: string) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
pendingChatMembers: report?.pendingChatMembers?.filter((pendingChatMember) => pendingChatMember.accountID !== invitedAccountID),
participants: {
@@ -3243,7 +3250,7 @@ function inviteToGroupChat(reportID: string, inviteeEmailsToAccountIDs: InvitedE
* Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
*/
function removeFromRoom(reportID: string, targetAccountIDs: number[]) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!report) {
return;
}
@@ -3526,7 +3533,7 @@ function prepareOnboardingOptimisticData(
// Guides are assigned and tasks are posted in the #admins room for the MANAGE_TEAM onboarding action, except for emails that have a '+'.
const shouldPostTasksInAdminsRoom = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !currentUserEmail?.includes('+');
const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : '';
- const adminsChatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`];
+ const adminsChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`];
const targetChatReport = shouldPostTasksInAdminsRoom ? adminsChatReport : ReportUtils.getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]);
const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {};
const assignedGuideEmail = getPolicy(targetChatPolicyID)?.assignedGuide?.email ?? 'Setup Specialist';
@@ -3847,7 +3854,7 @@ function prepareOnboardingOptimisticData(
lastVisibleActionCreated: '',
hasOutstandingChildTask: false,
};
- const report = ReportUtils.getReport(targetChatReportID);
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`];
const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(targetChatReportID, canUserPerformWriteAction);
if (lastMessageText || lastMessageTranslationKey) {
@@ -3964,7 +3971,7 @@ function prepareOnboardingOptimisticData(
if (engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM) {
const selfDMReportID = ReportUtils.findSelfDMReportID();
- const selfDMReport = ReportConnection.getReport(selfDMReportID ?? '-1');
+ const selfDMReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`];
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`,
@@ -4153,7 +4160,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt
},
};
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`];
const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions);
const reportUpdateDataWithCurrentLastMessage = {
@@ -4228,7 +4235,7 @@ function resolveActionableReportMentionWhisper(
},
};
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`];
const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions);
const reportUpdateDataWithCurrentLastMessage = {
diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts
index 9338527eaccc..d880c1a8fb20 100644
--- a/src/libs/actions/ReportActions.ts
+++ b/src/libs/actions/ReportActions.ts
@@ -1,23 +1,31 @@
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReportActions} from '@src/types/onyx';
+import type * as OnyxTypes from '@src/types/onyx';
import type ReportAction from '@src/types/onyx/ReportAction';
import * as Report from './Report';
type IgnoreDirection = 'parent' | 'child';
-let allReportActions: OnyxCollection;
+let allReportActions: OnyxCollection;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (value) => (allReportActions = value),
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
function clearReportActionErrors(reportID: string, reportAction: ReportAction, keys?: string[]) {
const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
@@ -81,7 +89,7 @@ function clearAllRelatedReportActionErrors(reportID: string, reportAction: Repor
clearReportActionErrors(reportID, reportAction, keys);
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.parentReportID && report?.parentReportActionID && ignore !== 'parent') {
const parentReportAction = ReportActionUtils.getReportAction(report.parentReportID, report.parentReportActionID);
const parentErrorKeys = Object.keys(parentReportAction?.errors ?? {}).filter((err) => errorKeys.includes(err));
diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts
index c08a83088180..50e37ba6afe5 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -5,11 +5,13 @@ import type {FormOnyxValues} from '@components/Form/types';
import type {PaymentData, SearchQueryJSON} from '@components/Search/types';
import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import * as API from '@libs/API';
-import type {ExportSearchItemsToCSVParams} from '@libs/API/parameters';
+import type {ExportSearchItemsToCSVParams, SubmitReportParams} from '@libs/API/parameters';
import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ApiUtils from '@libs/ApiUtils';
import fileDownload from '@libs/fileDownload';
import enhanceParameters from '@libs/Network/enhanceParameters';
+import {rand64} from '@libs/NumberUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils';
import playSound, {SOUNDS} from '@libs/Sound';
@@ -17,7 +19,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm';
import type {LastPaymentMethod, SearchResults} from '@src/types/onyx';
-import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
+import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
import * as Report from './Report';
let currentUserEmail: string;
@@ -64,6 +66,11 @@ function handleActionButtonPress(hash: number, item: TransactionListItemType | R
case CONST.SEARCH.ACTION_TYPES.APPROVE:
approveMoneyRequestOnSearch(hash, [item.reportID], transactionID);
return;
+ case CONST.SEARCH.ACTION_TYPES.SUBMIT: {
+ const policy = (allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] ?? {}) as SearchPolicy;
+ submitMoneyRequestOnSearch(hash, [item], [policy], transactionID);
+ return;
+ }
default:
goToItem();
}
@@ -236,7 +243,7 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com
API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList, comment}, {optimisticData, finallyData});
}
-function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) {
+function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: SearchPolicy[], transactionIDList?: string[]) {
const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -246,43 +253,72 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], trans
? (Object.fromEntries(
transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {isActionLoading: isLoading}]),
) as Partial)
- : (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {isActionLoading: isLoading}])) as Partial),
+ : (Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {isActionLoading: isLoading}])) as Partial),
},
},
];
const optimisticData: OnyxUpdate[] = createActionLoadingData(true);
const finallyData: OnyxUpdate[] = createActionLoadingData(false);
- API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData});
+ const report = (reportList.at(0) ?? {}) as SearchReport;
+ const parameters: SubmitReportParams = {
+ reportID: report.reportID,
+ managerAccountID: PolicyUtils.getSubmitToAccountID(policy.at(0), report) ?? report?.managerID,
+ reportActionID: rand64(),
+ };
+
+ // The SubmitReport command is not 1:1:1 yet, which means creating a separate SubmitMoneyRequestOnSearch command is not feasible until https://github.com/Expensify/Expensify/issues/451223 is done.
+ // In the meantime, we'll call SubmitReport which works for a single expense only, so not bulk actions are possible.
+ API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, finallyData});
+}
+
+function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) {
+ const createOnyxData = (update: Partial | Partial): OnyxUpdate[] => [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
+ value: {
+ data: transactionIDList
+ ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial)
+ : (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, update])) as Partial),
+ },
+ },
+ ];
+ const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true});
+ const failureData: OnyxUpdate[] = createOnyxData({hasError: true});
+ const finallyData: OnyxUpdate[] = createOnyxData({isActionLoading: false});
+
+ API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, failureData, finallyData});
}
function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionIDList?: string[]) {
- const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [
+ const createOnyxData = (update: Partial | Partial): OnyxUpdate[] => [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
value: {
data: transactionIDList
- ? (Object.fromEntries(
- transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {isActionLoading: isLoading}]),
- ) as Partial)
- : (Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, {isActionLoading: isLoading}])) as Partial),
+ ? (Object.fromEntries(transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, update])) as Partial)
+ : (Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, update])) as Partial),
},
},
];
- const optimisticData: OnyxUpdate[] = createActionLoadingData(true);
- const finallyData: OnyxUpdate[] = createActionLoadingData(false);
+ const optimisticData: OnyxUpdate[] = createOnyxData({isActionLoading: true});
+ const failureData: OnyxUpdate[] = createOnyxData({hasError: true});
+ const finallyData: OnyxUpdate[] = createOnyxData({isActionLoading: false});
// eslint-disable-next-line rulesdir/no-api-side-effects-method
- API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, paymentData: JSON.stringify(paymentData)}, {optimisticData, finallyData}).then(
- (response) => {
- if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) {
- return;
- }
- playSound(SOUNDS.SUCCESS);
- },
- );
+ API.makeRequestWithSideEffects(
+ SIDE_EFFECT_REQUEST_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH,
+ {hash, paymentData: JSON.stringify(paymentData)},
+ {optimisticData, failureData, finallyData},
+ ).then((response) => {
+ if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) {
+ return;
+ }
+ playSound(SOUNDS.SUCCESS);
+ });
}
function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) {
@@ -344,14 +380,6 @@ function clearAdvancedFilters() {
Onyx.merge(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, values);
}
-function showSavedSearchRenameTooltip() {
- Onyx.set(ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP, true);
-}
-
-function dismissSavedSearchRenameTooltip() {
- Onyx.set(ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP, false);
-}
-
export {
saveSearch,
search,
@@ -364,9 +392,8 @@ export {
clearAllFilters,
clearAdvancedFilters,
deleteSavedSearch,
- dismissSavedSearchRenameTooltip,
- showSavedSearchRenameTooltip,
payMoneyRequestOnSearch,
approveMoneyRequestOnSearch,
handleActionButtonPress,
+ submitMoneyRequestOnSearch,
};
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index e50eba7e596f..c4aee9fb91a8 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -218,7 +218,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
if (!isAnonymousUser()) {
// In the HybridApp, we want the Old Dot to handle the sign out process
if (NativeModules.HybridAppModule && killHybridApp) {
- NativeModules.HybridAppModule.closeReactNativeApp(true, false);
+ NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
return;
}
// We'll only call signOut if we're not stashing the session and this is not a supportal session,
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index de0f963596d2..df8c0474fbaa 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -12,7 +12,6 @@ import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import CONST from '@src/CONST';
@@ -79,6 +78,15 @@ Onyx.connect({
},
});
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
/**
* Clears out the task info from the store
*/
@@ -128,7 +136,7 @@ function createTaskAndNavigate(
const currentTime = DateUtils.getDBTimeWithSkew();
const lastCommentText = ReportUtils.formatReportLastMessageText(ReportActionsUtils.getReportActionText(optimisticAddCommentReport.reportAction));
- const parentReport = ReportUtils.getReport(parentReportID);
+ const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`];
const optimisticParentReport = {
lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created,
lastMessageText: lastCommentText,
@@ -324,10 +332,6 @@ function createTaskAndNavigate(
API.write(WRITE_COMMANDS.CREATE_TASK, parameters, {optimisticData, successData, failureData});
- ReportConnection.updateReportData(parentReportID, {
- lastReadTime: currentTime,
- });
-
if (!isCreatedUsingMarkdown) {
clearOutTaskInfo();
Navigation.dismissModal(parentReportID);
@@ -923,7 +927,7 @@ function getAssignee(assigneeAccountID: number, personalDetails: OnyxEntry): OnyxEntry, sessionAccountID
}
function clearTaskErrors(reportID: string) {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
// Delete the task preview in the parent report
if (report?.pendingFields?.createChat === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 3cb6e3dc44ba..c8a007458242 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -458,7 +458,7 @@ function abandonReviewDuplicateTransactions() {
}
function clearError(transactionID: string) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null, errorFields: {route: null}});
+ Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null, errorFields: {route: null, waypoints: null, routes: null}});
}
function markAsCash(transactionID: string, transactionThreadReportID: string) {
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index e23422d083ca..4aa8b1e7ec1f 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -1362,14 +1362,6 @@ function dismissTrackTrainingModal() {
});
}
-function dismissWorkspaceTooltip() {
- Onyx.merge(ONYXKEYS.NVP_WORKSPACE_TOOLTIP, {shouldShow: false});
-}
-
-function dismissGBRTooltip() {
- Onyx.merge(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, true);
-}
-
function requestRefund() {
API.write(WRITE_COMMANDS.REQUEST_REFUND, null);
}
@@ -1390,7 +1382,6 @@ export {
closeAccount,
dismissReferralBanner,
dismissTrackTrainingModal,
- dismissWorkspaceTooltip,
resendValidateCode,
requestContactMethodValidateCode,
updateNewsletterSubscription,
@@ -1424,6 +1415,5 @@ export {
addPendingContactMethod,
clearValidateCodeActionError,
subscribeToActiveGuides,
- dismissGBRTooltip,
setIsDebugModeEnabled,
};
diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts
index b306daf444ba..6febdf5f3e60 100644
--- a/src/libs/actions/Welcome/index.ts
+++ b/src/libs/actions/Welcome/index.ts
@@ -145,7 +145,7 @@ function completeHybridAppOnboarding() {
// No matter what the response is, we want to mark the onboarding as completed (user saw the explanation modal)
Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true);
- NativeModules.HybridAppModule.completeOnboarding(true);
+ NativeModules.HybridAppModule.completeOnboarding({status: true});
});
}
@@ -207,20 +207,16 @@ function setSelfTourViewed(shouldUpdateOnyxDataOnlyLocally = false) {
function dismissProductTraining(elementName: string) {
const date = new Date();
- // const optimisticData = [
- // {
- // onyxMethod: Onyx.METHOD.MERGE,
- // key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
- // value: {
- // [elementName]: DateUtils.getDBTime(date.valueOf()),
- // },
- // },
- // ];
- // API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: elementName}, {optimisticData});
-
- Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {
- [elementName]: DateUtils.getDBTime(date.valueOf()),
- });
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
+ value: {
+ [elementName]: DateUtils.getDBTime(date.valueOf()),
+ },
+ },
+ ];
+ API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: elementName}, {optimisticData});
}
export {
diff --git a/src/libs/markAllPolicyReportsAsRead.ts b/src/libs/markAllPolicyReportsAsRead.ts
index 259a5e426d89..e01a351882a6 100644
--- a/src/libs/markAllPolicyReportsAsRead.ts
+++ b/src/libs/markAllPolicyReportsAsRead.ts
@@ -1,13 +1,23 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
import * as ReportActionFile from './actions/Report';
-import * as ReportConnection from './ReportConnection';
import * as ReportUtils from './ReportUtils';
+let allReports: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
export default function markAllPolicyReportsAsRead(policyID: string) {
let delay = 0;
- const allReports = ReportConnection.getAllReports() ?? {};
- Object.keys(allReports).forEach((key: string) => {
- const report: Report | null | undefined = allReports[key];
+ Object.keys(allReports ?? {}).forEach((key: string) => {
+ const report = allReports?.[key];
if (report?.policyID !== policyID || !ReportUtils.isUnread(report)) {
return;
}
diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts
index 20f3d0a86495..8c743e66e79f 100644
--- a/src/libs/migrations/NVPMigration.ts
+++ b/src/libs/migrations/NVPMigration.ts
@@ -9,7 +9,6 @@ import type {OnyxKey} from '@src/ONYXKEYS';
const migrations = {
// eslint-disable-next-line @typescript-eslint/naming-convention
nvp_lastPaymentMethod: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
- isFirstTimeNewExpensifyUser: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
preferredLocale: ONYXKEYS.NVP_PREFERRED_LOCALE,
preferredEmojiSkinTone: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
frequentlyUsedEmojis: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index 6b25681fa8e8..1a082310ff53 100644
--- a/src/pages/EditReportFieldPage.tsx
+++ b/src/pages/EditReportFieldPage.tsx
@@ -40,7 +40,7 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) {
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const {translate} = useLocalize();
const isReportFieldTitle = ReportUtils.isReportFieldOfTypeTitle(reportField);
- const reportFieldsEnabled = (ReportUtils.isPaidGroupPolicyExpenseReport(report) && !!policy?.areReportFieldsEnabled) || isReportFieldTitle;
+ const reportFieldsEnabled = ((ReportUtils.isPaidGroupPolicyExpenseReport(report) || ReportUtils.isInvoiceReport(report)) && !!policy?.areReportFieldsEnabled) || isReportFieldTitle;
if (!reportFieldsEnabled || !reportField || !policyField || !report || isDisabled) {
return (
diff --git a/src/pages/ErrorPage/GenericErrorPage.tsx b/src/pages/ErrorPage/GenericErrorPage.tsx
index 0357cdc0204b..cb70ebf14d62 100644
--- a/src/pages/ErrorPage/GenericErrorPage.tsx
+++ b/src/pages/ErrorPage/GenericErrorPage.tsx
@@ -18,11 +18,12 @@ import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ErrorBodyText from './ErrorBodyText';
-function GenericErrorPage() {
+function GenericErrorPage({error}: {error?: Error}) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
+ const isChunkLoadError = error?.name === CONST.CHUNK_LOAD_ERROR || /Loading chunk [\d]+ failed/.test(error?.message ?? '');
const refreshPage = usePageRefresh();
return (
@@ -60,7 +61,7 @@ function GenericErrorPage() {
success
text={translate('genericErrorPage.refresh')}
style={styles.mr3}
- onPress={refreshPage}
+ onPress={() => refreshPage(isChunkLoadError)}
/>
@@ -309,7 +310,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
)}
- {!isLoading && canJoin && shouldUseNarrowLayout && {joinButton}}
+ {!isParentReportLoading && !isLoading && canJoin && shouldUseNarrowLayout && {joinButton}}
{!isLoading && isChatUsedForOnboarding && shouldUseNarrowLayout && {freeTrialButton}}
);
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 97582f75b7b1..47315b604f0b 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -131,7 +131,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
selector: (parentReportActions) => getParentReportAction(parentReportActions, reportOnyx?.parentReportActionID ?? ''),
});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
- const [workspaceTooltip] = useOnyx(ONYXKEYS.NVP_WORKSPACE_TOOLTIP);
const wasLoadingApp = usePrevious(isLoadingApp);
const finishedLoadingApp = wasLoadingApp && !isLoadingApp;
const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction);
@@ -865,7 +864,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
isComposerFullSize={!!isComposerFullSize}
isEmptyChat={isEmptyChat}
lastReportAction={lastReportAction}
- workspaceTooltip={workspaceTooltip}
/>
) : null}
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 1d7deea43a04..b88a5a0bd33e 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -16,6 +16,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useRestoreInputFocus from '@hooks/useRestoreInputFocus';
import useStyleUtils from '@hooks/useStyleUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -268,6 +269,7 @@ function BaseReportActionContextMenu({
},
{isActive: shouldEnableArrowNavigation && shouldEnableContextMenuEnterShortcut, shouldPreventDefault: false},
);
+ useRestoreInputFocus(isVisible);
const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: MutableRefObject) => {
showContextMenu(
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 705b85d4c3fc..a31137e53c6a 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -22,14 +22,12 @@ import Navigation from '@libs/Navigation/Navigation';
import Parser from '@libs/Parser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import * as TaskUtils from '@libs/TaskUtils';
import * as Download from '@userActions/Download';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
-import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Beta, Download as DownloadOnyx, OnyxInputOrEntry, ReportAction, ReportActionReactions, Transaction, User} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -606,7 +604,7 @@ const ContextMenuActions: ContextMenuAction[] = [
successIcon: Expensicons.Checkmark,
shouldShow: ({type, isProduction}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isProduction,
onPress: (closePopover, {reportID}) => {
- const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const report = ReportUtils.getReport(reportID);
Clipboard.setString(JSON.stringify(report, null, 4));
hideContextMenu(true, ReportActionComposeFocusManager.focus);
},
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 893d2b3060d9..78d3288d05f0 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -12,14 +12,12 @@ import type {FileObject} from '@components/AttachmentModal';
import AttachmentModal from '@components/AttachmentModal';
import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton';
import ExceededCommentLength from '@components/ExceededCommentLength';
-import Icon from '@components/Icon';
-import * as Expensicons from '@components/Icon/Expensicons';
import ImportedStateIndicator from '@components/ImportedStateIndicator';
import type {Mention} from '@components/MentionSuggestions';
import OfflineIndicator from '@components/OfflineIndicator';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {usePersonalDetails} from '@components/OnyxProvider';
-import Text from '@components/Text';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebounce from '@hooks/useDebounce';
@@ -28,7 +26,6 @@ import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitl
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -116,7 +113,6 @@ function ReportActionCompose({
onComposerFocus,
onComposerBlur,
}: ReportActionComposeProps) {
- const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -129,6 +125,11 @@ function ReportActionCompose({
const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT);
+ const {renderProductTrainingTooltip, hideProductTrainingTooltip, shouldShowProductTrainingTooltip} = useProductTrainingContext(
+ CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.WORKSAPCE_CHAT_CREATE,
+ shouldShowEducationalTooltip,
+ );
+
/**
* Updates the Highlight state of the composer
*/
@@ -380,34 +381,6 @@ function ReportActionCompose({
return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM;
}, [styles]);
- const renderWorkspaceChatTooltip = useCallback(
- () => (
-
-
-
- {translate('reportActionCompose.tooltip.title')}
- {translate('reportActionCompose.tooltip.subtitle')}
-
-
- ),
- [
- styles.alignItemsCenter,
- styles.flexRow,
- styles.justifyContentCenter,
- styles.flexWrap,
- styles.textAlignCenter,
- styles.gap1,
- styles.quickActionTooltipTitle,
- styles.quickActionTooltipSubtitle,
- theme.tooltipHighlightText,
- translate,
- ],
- );
-
const validateMaxLength = useCallback(
(value: string) => {
const taskCommentMatch = value?.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION);
@@ -448,10 +421,10 @@ function ReportActionCompose({
contentContainerStyle={isComposerFullSize ? styles.flex1 : {}}
>
- sortedReportActions.filter(
- (reportAction) =>
- (isOffline ||
- ReportActionsUtils.isDeletedParentAction(reportAction) ||
- reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ||
- reportAction.errors) &&
- ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction),
- ),
- [sortedReportActions, isOffline, canUserPerformWriteAction],
- );
const lastAction = sortedVisibleReportActions.at(0);
const sortedVisibleReportActionsObjects: OnyxTypes.ReportActions = useMemo(
() =>
@@ -207,9 +196,7 @@ function ReportActionsList({
);
const prevSortedVisibleReportActionsObjects = usePrevious(sortedVisibleReportActionsObjects);
- const reportLastReadTime = useMemo(() => {
- return ReportConnection.getReport(report.reportID)?.lastReadTime ?? report.lastReadTime ?? '';
- }, [report.reportID, report.lastReadTime]);
+ const reportLastReadTime = report.lastReadTime ?? '';
// In a one-expense report, the report actions from the expense report and transaction thread are combined.
// If the transaction thread has a newer action, it will show an unread marker if we compare it with the expense report lastReadTime.
diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index ee7c929acc7d..363e83e04d7e 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -247,6 +247,19 @@ function ReportActionsView({
// currentReportActionID is needed to trigger batching once the report action has been positioned
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [reportActionID, combinedReportActions, indexOfLinkedAction, currentReportActionID]);
+ const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
+ const visibleReportActions = useMemo(
+ () =>
+ reportActions.filter(
+ (reportAction) =>
+ (isOffline ||
+ ReportActionsUtils.isDeletedParentAction(reportAction) ||
+ reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ||
+ reportAction.errors) &&
+ ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction),
+ ),
+ [reportActions, isOffline, canUserPerformWriteAction],
+ );
const reportActionIDMap = useMemo(() => {
const reportActionIDs = allReportActions.map((action) => action.reportActionID);
@@ -285,7 +298,8 @@ function ReportActionsView({
const hasMoreCached = reportActions.length < combinedReportActions.length;
const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]);
const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]);
- const hasNewestReportAction = reportActions.at(0)?.created === report.lastVisibleActionCreated || reportActions.at(0)?.created === transactionThreadReport?.lastVisibleActionCreated;
+ const hasNewestReportAction =
+ visibleReportActions.at(0)?.created === report.lastVisibleActionCreated || visibleReportActions.at(0)?.created === transactionThreadReport?.lastVisibleActionCreated;
const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]);
useEffect(() => {
@@ -479,6 +493,7 @@ function ReportActionsView({
parentReportActionForTransactionThread={parentReportActionForTransactionThread}
onLayout={recordTimeToMeasureItemLayout}
sortedReportActions={reportActions}
+ sortedVisibleReportActions={visibleReportActions}
mostRecentIOUReportActionID={mostRecentIOUReportActionID}
loadOlderChats={loadOlderChats}
loadNewerChats={loadNewerChats}
diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx
index 9771e357b15f..f46e2a9476b3 100644
--- a/src/pages/home/report/ReportFooter.tsx
+++ b/src/pages/home/report/ReportFooter.tsx
@@ -45,9 +45,6 @@ type ReportFooterProps = {
/** The last report action */
lastReportAction?: OnyxEntry;
- /** Whether to show educational tooltip in workspace chat for first-time user */
- workspaceTooltip: OnyxEntry;
-
/** Whether the chat is empty */
isEmptyChat?: boolean;
@@ -76,7 +73,6 @@ function ReportFooter({
isEmptyChat = true,
isReportReadyForDisplay = true,
isComposerFullSize = false,
- workspaceTooltip,
onComposerBlur,
onComposerFocus,
}: ReportFooterProps) {
@@ -118,7 +114,7 @@ function ReportFooter({
const isSystemChat = ReportUtils.isSystemChat(report);
const isAdminsOnlyPostingRoom = ReportUtils.isAdminsOnlyPostingRoom(report);
const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy);
- const shouldShowEducationalTooltip = !!workspaceTooltip?.shouldShow && !isUserPolicyAdmin;
+ const shouldShowEducationalTooltip = ReportUtils.isPolicyExpenseChat(report) && !!report.isOwnPolicyExpenseChat && !isUserPolicyAdmin;
const allPersonalDetails = usePersonalDetails();
@@ -238,7 +234,6 @@ export default memo(
prevProps.isEmptyChat === nextProps.isEmptyChat &&
prevProps.lastReportAction === nextProps.lastReportAction &&
prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay &&
- prevProps.workspaceTooltip?.shouldShow === nextProps.workspaceTooltip?.shouldShow &&
lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
lodashIsEqual(prevProps.policy?.employeeList, nextProps.policy?.employeeList) &&
lodashIsEqual(prevProps.policy?.role, nextProps.policy?.role),
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
index d36003960fe4..6d74a910f455 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
@@ -11,7 +11,7 @@ import FloatingActionButton from '@components/FloatingActionButton';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import PopoverMenu from '@components/PopoverMenu';
-import Text from '@components/Text';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -111,6 +111,27 @@ const getQuickActionIcon = (action: QuickActionName): React.FC => {
}
};
+const getIouType = (action: QuickActionName) => {
+ switch (action) {
+ case CONST.QUICK_ACTIONS.REQUEST_MANUAL:
+ case CONST.QUICK_ACTIONS.REQUEST_SCAN:
+ case CONST.QUICK_ACTIONS.REQUEST_DISTANCE:
+ return CONST.IOU.TYPE.SUBMIT;
+ case CONST.QUICK_ACTIONS.SPLIT_MANUAL:
+ case CONST.QUICK_ACTIONS.SPLIT_SCAN:
+ case CONST.QUICK_ACTIONS.SPLIT_DISTANCE:
+ return CONST.IOU.TYPE.SPLIT;
+ case CONST.QUICK_ACTIONS.TRACK_DISTANCE:
+ case CONST.QUICK_ACTIONS.TRACK_MANUAL:
+ case CONST.QUICK_ACTIONS.TRACK_SCAN:
+ return CONST.IOU.TYPE.TRACK;
+ case CONST.QUICK_ACTIONS.SEND_MONEY:
+ return CONST.IOU.TYPE.PAY;
+ default:
+ return undefined;
+ }
+};
+
const getQuickActionTitle = (action: QuickActionName): TranslationPaths => {
switch (action) {
case CONST.QUICK_ACTIONS.REQUEST_MANUAL:
@@ -166,7 +187,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}, [activePolicy, activePolicyID, session?.accountID, allReports]);
const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector)});
- const [hasSeenTrackTraining] = useOnyx(ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING);
const [isCreateMenuActive, setIsCreateMenuActive] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
@@ -177,7 +197,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
const prevIsFocused = usePrevious(isFocused);
const {isOffline} = useNetwork();
- const {canUseSpotnanaTravel, canUseCombinedTrackSubmit} = usePermissions();
+ const {canUseSpotnanaTravel} = usePermissions();
const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]);
const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport, reportNameValuePairs));
const {environment} = useEnvironment();
@@ -186,6 +206,12 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasSeenTourSelector,
});
+
+ const {renderProductTrainingTooltip, hideProductTrainingTooltip, shouldShowProductTrainingTooltip} = useProductTrainingContext(
+ CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.QUICK_ACTION_BUTTON,
+ isCreateMenuActive && (!shouldUseNarrowLayout || isFocused),
+ );
+
/**
* There are scenarios where users who have not yet had their group workspace-chats in NewDot (isPolicyExpenseChatEnabled). In those scenarios, things can get confusing if they try to submit/track expenses. To address this, we block them from Creating, Tracking, Submitting expenses from NewDot if they are:
* 1. on at least one group policy
@@ -212,16 +238,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [personalDetails, session?.accountID, quickActionReport, quickActionPolicy, policyChatForActivePolicy]);
- const renderQuickActionTooltip = useCallback(
- () => (
-
- {translate('quickAction.tooltip.title')}
- {translate('quickAction.tooltip.subtitle')}
-
- ),
- [styles.quickActionTooltipTitle, styles.quickActionTooltipSubtitle, translate],
- );
-
const quickActionTitle = useMemo(() => {
if (isEmptyObject(quickActionReport)) {
return '';
@@ -351,66 +367,11 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}
};
- // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), [isLoading, isCreateMenuActive]);
-
const expenseMenuItems = useMemo((): PopoverMenuItem[] => {
- if (canUseCombinedTrackSubmit) {
- return [
- {
- icon: getIconForAction(CONST.IOU.TYPE.CREATE),
- text: translate('iou.createExpense'),
- shouldCallAfterModalHide: shouldRedirectToExpensifyClassic,
- onSelected: () =>
- interceptAnonymousUser(() => {
- if (shouldRedirectToExpensifyClassic) {
- setModalVisible(true);
- return;
- }
- IOU.startMoneyRequest(
- CONST.IOU.TYPE.CREATE,
- // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
- // for all of the routes in the creation flow.
- ReportUtils.generateReportID(),
- );
- }),
- },
- ];
- }
-
return [
- ...(selfDMReportID
- ? [
- {
- icon: getIconForAction(CONST.IOU.TYPE.TRACK),
- text: translate('iou.trackExpense'),
- shouldCallAfterModalHide: shouldRedirectToExpensifyClassic,
- onSelected: () => {
- if (shouldRedirectToExpensifyClassic) {
- setModalVisible(true);
- return;
- }
- interceptAnonymousUser(() => {
- IOU.startMoneyRequest(
- CONST.IOU.TYPE.TRACK,
- // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID.
- // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow.
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
- );
- });
- if (!hasSeenTrackTraining && !isOffline) {
- setTimeout(() => {
- Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL);
- }, CONST.ANIMATED_TRANSITION);
- }
- },
- },
- ]
- : []),
{
- icon: getIconForAction(CONST.IOU.TYPE.REQUEST),
- text: translate('iou.submitExpense'),
+ icon: getIconForAction(CONST.IOU.TYPE.CREATE),
+ text: translate('iou.createExpense'),
shouldCallAfterModalHide: shouldRedirectToExpensifyClassic,
onSelected: () =>
interceptAnonymousUser(() => {
@@ -418,9 +379,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
setModalVisible(true);
return;
}
-
IOU.startMoneyRequest(
- CONST.IOU.TYPE.SUBMIT,
+ CONST.IOU.TYPE.CREATE,
// When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
// for all of the routes in the creation flow.
ReportUtils.generateReportID(),
@@ -428,7 +388,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}),
},
];
- }, [canUseCombinedTrackSubmit, translate, selfDMReportID, hasSeenTrackTraining, isOffline, shouldRedirectToExpensifyClassic]);
+ }, [translate, shouldRedirectToExpensifyClassic]);
const quickActionMenuItems = useMemo(() => {
// Define common properties in baseQuickAction
@@ -444,11 +404,17 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
},
tooltipShiftHorizontal: styles.popoverMenuItem.paddingHorizontal,
tooltipShiftVertical: styles.popoverMenuItem.paddingVertical / 2,
- renderTooltipContent: renderQuickActionTooltip,
+ renderTooltipContent: renderProductTrainingTooltip,
tooltipWrapperStyle: styles.quickActionTooltipWrapper,
+ onHideTooltip: hideProductTrainingTooltip,
+ shouldRenderTooltip: shouldShowProductTrainingTooltip,
};
if (quickAction?.action) {
+ const iouType = getIouType(quickAction?.action);
+ if (!!iouType && !ReportUtils.canCreateRequest(quickActionReport, quickActionPolicy, iouType)) {
+ return [];
+ }
return [
{
...baseQuickAction,
@@ -457,7 +423,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
description: !hideQABSubtitle ? ReportUtils.getReportName(quickActionReport) ?? translate('quickAction.updateDestination') : '',
onSelected: () => interceptAnonymousUser(() => navigateToQuickAction()),
shouldShowSubscriptRightAvatar: ReportUtils.isPolicyExpenseChat(quickActionReport),
- shouldRenderTooltip: quickAction.isFirstQuickAction,
},
];
}
@@ -476,7 +441,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}, true);
}),
shouldShowSubscriptRightAvatar: true,
- shouldRenderTooltip: false,
},
];
}
@@ -488,16 +452,18 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
styles.popoverMenuItem.paddingHorizontal,
styles.popoverMenuItem.paddingVertical,
styles.quickActionTooltipWrapper,
- renderQuickActionTooltip,
+ renderProductTrainingTooltip,
+ hideProductTrainingTooltip,
quickAction?.action,
- quickAction?.isFirstQuickAction,
policyChatForActivePolicy,
quickActionTitle,
hideQABSubtitle,
quickActionReport,
+ shouldShowProductTrainingTooltip,
navigateToQuickAction,
selectOption,
isValidReport,
+ quickActionPolicy,
]);
const viewTourTaskReportID = introSelected?.viewTour;
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx
index 9e18a74246e9..461ed6298c84 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx
@@ -465,6 +465,7 @@ function IOURequestStepDistance({
waypoints,
...(hasRouteChanged ? {routes: transaction?.routes} : {}),
policy,
+ transactionBackup,
});
transactionWasSaved.current = true;
navigateBack();
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index a40b14eae4c9..cb5929e54415 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -243,7 +243,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
...(NativeModules.HybridAppModule
? {
action: () => {
- NativeModules.HybridAppModule.closeReactNativeApp(false, true);
+ NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true});
setInitialURL(undefined);
},
}
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
index 4870591bcad5..e94042b25e68 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -272,7 +272,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) {
setIsValidateCodeActionModalVisible(false);
}}
sendValidateCode={() => User.requestContactMethodValidateCode(contactMethod)}
- descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod})}
+ descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod: formattedContactMethod})}
/>
{!isValidateCodeActionModalVisible && getMenuItems()}
diff --git a/src/pages/settings/Profile/ProfileAvatar.tsx b/src/pages/settings/Profile/ProfileAvatar.tsx
index a80db51580ba..74b88f419e1d 100644
--- a/src/pages/settings/Profile/ProfileAvatar.tsx
+++ b/src/pages/settings/Profile/ProfileAvatar.tsx
@@ -1,7 +1,7 @@
import React, {useEffect} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import AttachmentModal from '@components/AttachmentModal';
+import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {AuthScreensParamList} from '@libs/Navigation/types';
@@ -11,17 +11,13 @@ import * as ValidationUtils from '@libs/ValidationUtils';
import * as PersonalDetails from '@userActions/PersonalDetails';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
-import type {PersonalDetailsList, PersonalDetailsMetadata} from '@src/types/onyx';
-type ProfileAvatarOnyxProps = {
- personalDetails: OnyxEntry;
- personalDetailsMetadata: OnyxEntry>;
- isLoadingApp: OnyxEntry;
-};
+type ProfileAvatarProps = PlatformStackScreenProps;
-type ProfileAvatarProps = ProfileAvatarOnyxProps & PlatformStackScreenProps;
-
-function ProfileAvatar({route, personalDetails, personalDetailsMetadata, isLoadingApp = true}: ProfileAvatarProps) {
+function ProfileAvatar({route}: ProfileAvatarProps) {
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
+ const [personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_METADATA);
+ const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {initialValue: true});
const personalDetail = personalDetails?.[route.params.accountID];
const avatarURL = personalDetail?.avatar ?? '';
const accountID = Number(route.params.accountID ?? '-1');
@@ -37,7 +33,7 @@ function ProfileAvatar({route, personalDetails, personalDetailsMetadata, isLoadi
return (
{
@@ -52,14 +48,4 @@ function ProfileAvatar({route, personalDetails, personalDetailsMetadata, isLoadi
ProfileAvatar.displayName = 'ProfileAvatar';
-export default withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- personalDetailsMetadata: {
- key: ONYXKEYS.PERSONAL_DETAILS_METADATA,
- },
- isLoadingApp: {
- key: ONYXKEYS.IS_LOADING_APP,
- },
-})(ProfileAvatar);
+export default ProfileAvatar;
diff --git a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx
index 39062f8a9830..2add4009bb56 100644
--- a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx
+++ b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx
@@ -76,7 +76,7 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) {
title={translate('delegate.role', {role})}
description={translate('delegate.accessLevel')}
helperText={translate('delegate.roleDescription', {role})}
- onPress={() => Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(login, role))}
+ onPress={() => Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(login, role), CONST.NAVIGATION.ACTION_TYPE.PUSH)}
shouldShowRightIcon
/>
({
@@ -74,7 +74,6 @@ function UpdateDelegateRolePage({route}: UpdateDelegateRolePageProps) {
}
requestValidationCode();
- setCurrentRole(option.value);
Navigation.navigate(ROUTES.SETTINGS_UPDATE_DELEGATE_ROLE_MAGIC_CODE.getRoute(login, option.value));
}}
sections={[{data: roleOptions}]}
diff --git a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx
index 3af05e1ef1cd..a624244bafeb 100644
--- a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx
+++ b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx
@@ -24,10 +24,11 @@ function SubscriptionSettings() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
const preferredCurrency = usePreferredCurrency();
const possibleCostSavings = useSubscriptionPossibleCostSavings();
- const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.delegatedAccess?.delegate});
+ const isActingAsDelegate = !!account?.delegatedAccess?.delegate;
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
const autoRenewalDate = formatSubscriptionEndDate(privateSubscription?.endDate);
@@ -41,7 +42,11 @@ function SubscriptionSettings() {
Subscription.updateSubscriptionAutoRenew(true);
return;
}
- Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY);
+ if (account?.hasPurchases) {
+ Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY);
+ } else {
+ Subscription.updateSubscriptionAutoRenew(false);
+ }
};
const handleAutoIncreaseToggle = () => {
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index d279cba2ec65..4ddf2235992b 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -359,7 +359,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
// We are checking if the user can access the route.
// If user can't access the route, we are dismissing any modals that are open when the NotFound view is shown
- const canAccessRoute = activeRoute && menuItems.some((item) => item.routeName === activeRoute);
+ const canAccessRoute = activeRoute && (menuItems.some((item) => item.routeName === activeRoute) || activeRoute === SCREENS.WORKSPACE.INITIAL);
useEffect(() => {
if (!shouldShowNotFoundPage && canAccessRoute) {
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 231550dc0454..00fb559263b7 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -32,6 +32,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as FormActions from '@libs/actions/FormActions';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -110,7 +111,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
if (!approverAccountID) {
return translate('workspace.people.removeMembersPrompt', {
count: selectedEmployees.length,
- memberName: PersonalDetailsUtils.getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? '',
+ memberName: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedEmployees, currentUserAccountID).at(0)?.displayName ?? ''),
});
}
return translate('workspace.people.removeMembersWarningPrompt', {
diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx
index 58c4564cc842..7d6e73009a9c 100644
--- a/src/pages/workspace/WorkspaceProfileSharePage.tsx
+++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx
@@ -1,12 +1,14 @@
import React, {useMemo, useRef} from 'react';
import {View} from 'react-native';
import type {ImageSourcePropType} from 'react-native';
+import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
import ContextMenuItem from '@components/ContextMenuItem';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
import {useSession} from '@components/OnyxProvider';
-import QRShare from '@components/QRShare';
-import type {QRShareHandle} from '@components/QRShare/types';
+import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
+import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
@@ -19,6 +21,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Clipboard from '@libs/Clipboard';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
+import shouldAllowDownloadQRCode from '@libs/shouldAllowDownloadQRCode';
import * as Url from '@libs/Url';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -31,7 +34,7 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {environmentURL} = useEnvironment();
- const qrCodeRef = useRef(null);
+ const qrCodeRef = useRef(null);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const session = useSession();
@@ -96,21 +99,14 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
- {/*
- Right now QR code download button is not shown anymore
- This is a temporary measure because right now it's broken because of the Fabric update.
- We need to wait for react-native v0.74 to be released so react-native-view-shot gets fixed.
-
- Please see https://github.com/Expensify/App/issues/40110 to see if it can be re-enabled.
- */}
-
@@ -126,6 +122,18 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
shouldLimitWidth={false}
wrapperStyle={themeStyles.sectionMenuItemTopDescription}
/>
+ {/* Remove this once https://github.com/Expensify/App/issues/19834 is done.
+ We shouldn't introduce platform specific code in our codebase.
+ This is a temporary solution while Web is not supported for the QR code download feature */}
+ {shouldAllowDownloadQRCode && (
+
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 56ba8a5440b8..7d9306795be7 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -84,7 +84,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const currentConnectionName = PolicyUtils.getCurrentConnectionName(policy);
const isQuickSettingsFlow = !!backTo;
- const canSelectMultiple = shouldUseNarrowLayout ? selectionMode?.isEnabled : true;
+ const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true;
const fetchCategories = useCallback(() => {
Category.openPolicyCategoriesPage(policyId);
@@ -182,7 +182,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const options: Array>> = [];
const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0;
- if (shouldUseNarrowLayout ? canSelectMultiple : selectedCategoriesArray.length > 0) {
+ if (isSmallScreenWidth ? canSelectMultiple : selectedCategoriesArray.length > 0) {
if (!isThereAnyAccountingConnection) {
options.push({
icon: Expensicons.Trashcan,
@@ -408,7 +408,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
{hasVisibleCategories && !isLoading && (
item && toggleCategory(item)}
sections={[{data: categoryList, isDisabled: false}]}
onCheckboxPress={toggleCategory}
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
index 98c4e93d8d4c..bd5edaa43779 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
@@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx';
import CategorySelectorModal from '@components/CategorySelector/CategorySelectorModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import SelectionList from '@components/SelectionList';
import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
@@ -99,52 +100,54 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet
title={translate('common.settings')}
onBackButtonPress={() => Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(policyID, backTo) : undefined)}
/>
- Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')}
- shouldPlaceSubtitleBelowSwitch
- />
-
-
- {!!currentPolicy && (sections.at(0)?.data?.length ?? 0) > 0 && (
-
- {translate('workspace.categories.defaultSpendCategories')}
- {translate('workspace.categories.spendCategoriesDescription')}
-
- }
- sections={sections}
- ListItem={SpendCategorySelectorListItem}
- onSelectRow={(item) => {
- if (!item.groupID || !item.categoryID) {
- return;
+
+ Policy.clearPolicyErrorField(policy?.id ?? '-1', 'requiresCategory')}
+ shouldPlaceSubtitleBelowSwitch
+ />
+
+
+ {!!currentPolicy && (sections.at(0)?.data?.length ?? 0) > 0 && (
+
+ {translate('workspace.categories.defaultSpendCategories')}
+ {translate('workspace.categories.spendCategoriesDescription')}
+
}
- setIsSelectorModalVisible(true);
- setCategoryID(item.categoryID);
- setGroupID(item.groupID);
- }}
- />
- )}
- {!!categoryID && !!groupID && (
- setIsSelectorModalVisible(false)}
- onCategorySelected={setNewCategory}
- label={groupID[0].toUpperCase() + groupID.slice(1)}
- />
- )}
-
+ sections={sections}
+ ListItem={SpendCategorySelectorListItem}
+ onSelectRow={(item) => {
+ if (!item.groupID || !item.categoryID) {
+ return;
+ }
+ setIsSelectorModalVisible(true);
+ setCategoryID(item.categoryID);
+ setGroupID(item.groupID);
+ }}
+ />
+ )}
+
+
+ {!!categoryID && !!groupID && (
+ setIsSelectorModalVisible(false)}
+ onCategorySelected={setNewCategory}
+ label={groupID[0].toUpperCase() + groupID.slice(1)}
+ />
+ )}
);
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
index 6d667d786d5d..85a5d2372ee9 100644
--- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -23,6 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton';
import Navigation from '@navigation/Navigation';
@@ -58,7 +59,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
const styles = useThemeStyles();
const {isOffline} = useNetwork();
- const {translate} = useLocalize();
+ const {formatPhoneNumber, translate} = useLocalize();
const StyleUtils = useStyleUtils();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [cards] = useOnyx(`${ONYXKEYS.CARD_LIST}`);
@@ -75,13 +76,13 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
const prevMember = usePrevious(member);
const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
const fallbackIcon = details.fallbackIcon ?? '';
- const displayName = details.displayName ?? '';
+ const displayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details));
const isSelectedMemberOwner = policy?.owner === details.login;
const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID;
const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN;
const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login;
const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails);
- const policyOwnerDisplayName = ownerDetails.displayName ?? policy?.owner ?? '';
+ const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? '';
const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0;
const paymentAccountID = cardSettings?.paymentBankAccountID ?? 0;
diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx
index d45e27905c28..029f8e78271f 100644
--- a/src/pages/workspace/upgrade/UpgradeIntro.tsx
+++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx
@@ -75,6 +75,7 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi