diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 8f12229707ff..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:
@@ -508,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 b670e9b9cdb3..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/**']
@@ -88,37 +87,39 @@ jobs:
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: |
@@ -126,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:
@@ -144,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
@@ -156,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
@@ -205,53 +197,43 @@ jobs:
env:
DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer
runs-on: macos-13-xlarge
- defaults:
- run:
- working-directory: Mobile-Expensify/react-native
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
+ - name: Setup Node
id: setup-node
- with:
- node-version-file: 'Mobile-Expensify/react-native/.nvmrc'
- cache: npm
- cache-dependency-path: 'Mobile-Expensify/react-native'
-
+ 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: Install node modules
- run: |
- npm install
- cd .. && npm install
-
- name: Setup Ruby
uses: ruby/setup-ruby@v1.190.0
with:
bundler-cache: true
- working-directory: 'Mobile-Expensify/react-native'
- name: Install New Expensify Gems
run: bundle install
@@ -260,12 +242,12 @@ jobs:
uses: actions/cache@v4
id: pods-cache
with:
- path: ios/Pods
- key: ${{ runner.os }}-pods-cache-${{ hashFiles('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('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
+ 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
@@ -273,7 +255,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 5
- command: cd Mobile-Expensify/iOS && bundle exec pod install
+ command: npm run pod-install
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
diff --git a/Mobile-Expensify b/Mobile-Expensify
index c0cf9cfe6fd7..af549932c171 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit c0cf9cfe6fd7c760a2f8621cf0473f0694bec10f
+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 23f777896f61..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 1009007500
- versionName "9.0.75-0"
+ 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/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/fastlane/Fastfile b/fastlane/Fastfile
index 880a12023056..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',
@@ -408,7 +408,7 @@ platform :ios do
desc "Build an iOS HybridApp Adhoc build"
lane :build_adhoc_hybrid do
- ENV["ENVFILE"]="../.env.adhoc.hybridapp"
+ ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp"
setupIOSSigningCertificate()
@@ -425,7 +425,7 @@ platform :ios do
)
build_app(
- workspace: "../iOS/Expensify.xcworkspace",
+ workspace: "Mobile-Expensify/iOS/Expensify.xcworkspace",
scheme: "Expensify",
output_name: "Expensify.ipa",
export_method: "app-store",
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 2c05d4b504bc..74d34f52214b 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.75.0
+ 9.0.75.6
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index bdba334ae098..c594f105f833 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.75.0
+ 9.0.75.6
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 70d8151a90cc..2b8181d88d5b 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.75
CFBundleVersion
- 9.0.75.0
+ 9.0.75.6
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 86f1d38ac1e5..9c0683631269 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -2034,8 +2034,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
@@ -3296,7 +3315,7 @@ SPEC CHECKSUMS:
react-native-quick-sqlite: 7c793c9f5834e756b336257a8d8b8239b7ceb451
react-native-release-profiler: 131ec5e4145d900b2be2a8d6641e2ce0dd784259
react-native-safe-area-context: 458f6b948437afcb59198016b26bbd02ff9c3b47
- react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688
+ react-native-view-shot: 6bafd491eb295b5834e05c469a37ecbd796d5b22
react-native-webview: ad29375839c9aa0409ce8e8693291b42bdc067a4
React-nativeconfig: aeed6e2a8ac02b2df54476afcc7c663416c12bf7
React-NativeModulesApple: c5b7813da94136f50ef084fa1ac077332dcfc658
diff --git a/package-lock.json b/package-lock.json
index b70f9fc1d07b..5703d2fca1ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.75-0",
+ "version": "9.0.75-6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.75-0",
+ "version": "9.0.75-6",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -97,7 +97,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",
@@ -116,7 +116,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",
@@ -32358,9 +32358,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",
@@ -32666,7 +32666,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 d4b1757782ea..f297e01263a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.75-0",
+ "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.",
@@ -160,7 +160,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",
@@ -179,7 +179,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/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 15554719ca9d..4fcc1cada6ff 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -344,6 +344,8 @@ 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,
@@ -6405,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..e19aa71cce52 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',
@@ -464,6 +452,9 @@ const ONYXKEYS = {
/** The user's Concierge reportID */
CONCIERGE_REPORT_ID: 'conciergeReportID',
+ /** The user's session that will be preserved when using imported state */
+ PRESERVED_USER_SESSION: 'preservedUserSession',
+
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
@@ -876,7 +867,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 +1007,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,9 +1015,9 @@ 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.PRESERVED_USER_SESSION]: OnyxTypes.Session;
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx
index 12189d22dba0..6be2b43c09d7 100644
--- a/src/components/AmountTextInput.tsx
+++ b/src/components/AmountTextInput.tsx
@@ -39,7 +39,7 @@ type AmountTextInputProps = {
/** Hide the focus styles on TextInput */
hideFocusedState?: boolean;
-} & Pick;
+} & Pick;
function AmountTextInput(
{
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/ImportOnyxState/index.native.tsx b/src/components/ImportOnyxState/index.native.tsx
index 2258da4c8f6c..bdd805241c55 100644
--- a/src/components/ImportOnyxState/index.native.tsx
+++ b/src/components/ImportOnyxState/index.native.tsx
@@ -1,11 +1,12 @@
import React, {useState} from 'react';
import ReactNativeBlobUtil from 'react-native-blob-util';
-import Onyx from 'react-native-onyx';
+import Onyx, {useOnyx} from 'react-native-onyx';
import type {FileObject} from '@components/AttachmentModal';
-import {KEYS_TO_PRESERVE, setIsUsingImportedState} from '@libs/actions/App';
+import {KEYS_TO_PRESERVE, setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App';
import {setShouldForceOffline} from '@libs/actions/Network';
import Navigation from '@libs/Navigation/Navigation';
import type {OnyxValues} from '@src/ONYXKEYS';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import BaseImportOnyxState from './BaseImportOnyxState';
import type ImportOnyxStateProps from './types';
@@ -45,8 +46,9 @@ function applyStateInChunks(state: OnyxValues) {
return promise;
}
-export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxStateProps) {
+export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) {
const [isErrorModalVisible, setIsErrorModalVisible] = useState(false);
+ const [session] = useOnyx(ONYXKEYS.SESSION);
const handleFileRead = (file: FileObject) => {
if (!file.uri) {
@@ -57,6 +59,8 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta
readOnyxFile(file.uri)
.then((fileContent: string) => {
const transformedState = cleanAndTransformState(fileContent);
+ const currentUserSessionCopy = {...session};
+ setPreservedUserSession(currentUserSessionCopy);
setShouldForceOffline(true);
Onyx.clear(KEYS_TO_PRESERVE).then(() => {
applyStateInChunks(transformedState).then(() => {
@@ -67,14 +71,7 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta
})
.catch(() => {
setIsErrorModalVisible(true);
- })
- .finally(() => {
- setIsLoading(false);
});
-
- if (isLoading) {
- setIsLoading(false);
- }
};
return (
diff --git a/src/components/ImportOnyxState/index.tsx b/src/components/ImportOnyxState/index.tsx
index 8add2d9172fd..2f9a2b70b65b 100644
--- a/src/components/ImportOnyxState/index.tsx
+++ b/src/components/ImportOnyxState/index.tsx
@@ -1,17 +1,19 @@
import React, {useState} from 'react';
-import Onyx from 'react-native-onyx';
+import Onyx, {useOnyx} from 'react-native-onyx';
import type {FileObject} from '@components/AttachmentModal';
-import {KEYS_TO_PRESERVE, setIsUsingImportedState} from '@libs/actions/App';
+import {KEYS_TO_PRESERVE, setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App';
import {setShouldForceOffline} from '@libs/actions/Network';
import Navigation from '@libs/Navigation/Navigation';
import type {OnyxValues} from '@src/ONYXKEYS';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import BaseImportOnyxState from './BaseImportOnyxState';
import type ImportOnyxStateProps from './types';
import {cleanAndTransformState} from './utils';
-export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxStateProps) {
+export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) {
const [isErrorModalVisible, setIsErrorModalVisible] = useState(false);
+ const [session] = useOnyx(ONYXKEYS.SESSION);
const handleFileRead = (file: FileObject) => {
if (!file.uri) {
@@ -27,26 +29,20 @@ export default function ImportOnyxState({setIsLoading, isLoading}: ImportOnyxSta
.then((text) => {
const fileContent = text;
const transformedState = cleanAndTransformState(fileContent);
+ const currentUserSessionCopy = {...session};
+ setPreservedUserSession(currentUserSessionCopy);
setShouldForceOffline(true);
Onyx.clear(KEYS_TO_PRESERVE).then(() => {
- Onyx.multiSet(transformedState)
- .then(() => {
- setIsUsingImportedState(true);
- Navigation.navigate(ROUTES.HOME);
- })
- .finally(() => {
- setIsLoading(false);
- });
+ Onyx.multiSet(transformedState).then(() => {
+ setIsUsingImportedState(true);
+ Navigation.navigate(ROUTES.HOME);
+ });
});
})
.catch(() => {
setIsErrorModalVisible(true);
setIsLoading(false);
});
-
- if (isLoading) {
- setIsLoading(false);
- }
};
return (
diff --git a/src/components/ImportOnyxState/types.ts b/src/components/ImportOnyxState/types.ts
index 8e504c493529..2b4b56a3b20c 100644
--- a/src/components/ImportOnyxState/types.ts
+++ b/src/components/ImportOnyxState/types.ts
@@ -1,5 +1,4 @@
type ImportOnyxStateProps = {
- isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
};
diff --git a/src/components/ImportOnyxState/utils.ts b/src/components/ImportOnyxState/utils.ts
index a5f24fa80714..94779868384d 100644
--- a/src/components/ImportOnyxState/utils.ts
+++ b/src/components/ImportOnyxState/utils.ts
@@ -3,7 +3,7 @@ import type {UnknownRecord} from 'type-fest';
import ONYXKEYS from '@src/ONYXKEYS';
// List of Onyx keys from the .txt file we want to keep for the local override
-const keysToOmit = [ONYXKEYS.ACTIVE_CLIENTS, ONYXKEYS.FREQUENTLY_USED_EMOJIS, ONYXKEYS.NETWORK, ONYXKEYS.CREDENTIALS, ONYXKEYS.SESSION, ONYXKEYS.PREFERRED_THEME];
+const keysToOmit = [ONYXKEYS.ACTIVE_CLIENTS, ONYXKEYS.FREQUENTLY_USED_EMOJIS, ONYXKEYS.NETWORK, ONYXKEYS.CREDENTIALS, ONYXKEYS.PREFERRED_THEME];
function isRecord(value: unknown): value is Record {
return typeof value === 'object' && !Array.isArray(value) && value !== null;
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 25d790e3d391..8340e7d6a976 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/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx
index 717659c16fd3..9ef33900bb00 100644
--- a/src/components/MoneyRequestAmountInput.tsx
+++ b/src/components/MoneyRequestAmountInput.tsx
@@ -12,7 +12,6 @@ import CONST from '@src/CONST';
import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused';
import type {BaseTextInputRef} from './TextInput/BaseTextInput/types';
import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol';
-import type {TextInputWithCurrencySymbolProps} from './TextInputWithCurrencySymbol/types';
type CurrentMoney = {amount: string; currency: string};
@@ -92,7 +91,7 @@ type MoneyRequestAmountInputProps = {
/** The width of inner content */
contentWidth?: number;
-} & Pick;
+};
type Selection = {
start: number;
@@ -127,7 +126,6 @@ function MoneyRequestAmountInput(
hideFocusedState = true,
shouldKeepUserInput = false,
autoGrow = true,
- autoGrowExtraSpace,
contentWidth,
...props
}: MoneyRequestAmountInputProps,
@@ -291,7 +289,6 @@ function MoneyRequestAmountInput(
return (
{
- if (!errors.includes(formError)) {
- return;
- }
-
- setFormError('');
- },
- [formError, setFormError],
- );
-
const shouldDisplayFieldError: boolean = useMemo(() => {
if (!isEditingSplitBill) {
return false;
@@ -314,34 +303,6 @@ function MoneyRequestConfirmationList({
return false;
};
- const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0);
-
- useEffect(() => {
- // We want this effect to run only when the transaction is moving from Self DM to a workspace chat
- if (!isDistanceRequest || !isMovingTransactionFromTrackExpense || !isPolicyExpenseChat) {
- return;
- }
-
- const errorKey = 'iou.error.invalidRate';
- const policyRates = DistanceRequestUtils.getMileageRates(policy);
-
- // If the selected rate belongs to the policy, clear the error
- if (Object.keys(policyRates).includes(customUnitRateID)) {
- clearFormErrors([errorKey]);
- return;
- }
-
- // If there is a distance rate in the policy that matches the rate and unit of the currently selected mileage rate, select it automatically
- const matchingRate = Object.values(policyRates).find((policyRate) => policyRate.rate === mileageRate.rate && policyRate.unit === mileageRate.unit);
- if (matchingRate?.customUnitRateID) {
- IOU.setCustomUnitRateID(transactionID, matchingRate.customUnitRateID);
- return;
- }
-
- // If none of the above conditions are met, display the rate error
- setFormError(errorKey);
- }, [isDistanceRequest, isPolicyExpenseChat, transactionID, mileageRate, customUnitRateID, policy, isMovingTransactionFromTrackExpense, setFormError, clearFormErrors]);
-
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -352,11 +313,12 @@ function MoneyRequestConfirmationList({
return;
}
// reset the form error whenever the screen gains or loses focus
- clearFormErrors(['iou.error.genericSmartscanFailureMessage', 'iou.receiptScanningFailed']);
+ setFormError('');
// 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(() => {
@@ -507,8 +469,8 @@ function MoneyRequestConfirmationList({
return;
}
- clearFormErrors(['iou.error.invalidSplit', 'iou.error.invalidSplitParticipants', 'iou.error.invalidSplitYourself']);
- }, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate, clearFormErrors]);
+ setFormError('');
+ }, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate]);
useEffect(() => {
if (!isTypeSplit || !transaction?.splitShares) {
@@ -675,9 +637,7 @@ function MoneyRequestConfirmationList({
}, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]);
useEffect(() => {
- if (!isDistanceRequest || (isMovingTransactionFromTrackExpense && !isPolicyExpenseChat)) {
- // We don't want to recalculate the distance merchant when moving a transaction from Track Expense to a 1:1 chat, because the distance rate will be the same default P2P rate.
- // When moving to a policy chat (e.g. sharing with an accountant), we should recalculate the distance merchant with the policy's rate.
+ if (!isDistanceRequest || isMovingTransactionFromTrackExpense) {
return;
}
@@ -700,7 +660,6 @@ function MoneyRequestConfirmationList({
translate,
toLocaleDigit,
isDistanceRequest,
- isPolicyExpenseChat,
transaction,
transactionID,
action,
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index c391564a6d3d..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);
@@ -259,7 +257,6 @@ function MoneyRequestConfirmationListFooter({
const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
// Determine if the merchant error should be displayed
const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty;
- const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate';
// The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
const shouldShowReceiptEmptyState = iouType === CONST.IOU.TYPE.SUBMIT && PolicyUtils.isPaidGroupPolicy(policy);
const {
@@ -370,7 +367,6 @@ function MoneyRequestConfirmationListFooter({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
- brickRoadIndicator={shouldDisplayDistanceRateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
disabled={didConfirm}
interactive={!!rate && !isReadOnly && isPolicyExpenseChat}
/>
@@ -536,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/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 1c2a552db476..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,
};
@@ -729,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/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 c7e7f587769c..2608e4e2de8c 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -87,16 +87,19 @@ function ActionCell({
);
}
- if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) {
+ if (action === CONST.SEARCH.ACTION_TYPES.VIEW || action === CONST.SEARCH.ACTION_TYPES.REVIEW || shouldUseViewAction) {
return isLargeScreenWidth ? (
) : null;
}
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.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index d81713691fac..cdfcb22f1f2c 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -49,7 +49,6 @@ function BaseTextInput(
autoFocus = false,
disableKeyboard = false,
autoGrow = false,
- autoGrowExtraSpace = 0,
autoGrowHeight = false,
maxAutoGrowHeight,
hideFocusedState = false,
@@ -251,8 +250,7 @@ function BaseTextInput(
const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([
styles.textInputContainer,
textInputContainerStyles,
- !!contentWidth && StyleUtils.getWidthStyle(textInputWidth),
- autoGrow && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace),
+ (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth),
!hideFocusedState && isFocused && styles.borderColorFocus,
(!!hasError || !!errorText) && styles.borderColorDanger,
autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined},
@@ -443,10 +441,14 @@ function BaseTextInput(
)}
{/*
Text input component doesn't support auto grow by default.
+ We're using a hidden text input to achieve that.
This text view is used to calculate width or height of the input value given textStyle in this component.
This Text component is intentionally positioned out of the screen.
*/}
{(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && (
+ // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value
+ // https://github.com/Expensify/App/issues/8158
+ // https://github.com/Expensify/App/issues/26628
(null);
const isLabelActive = useRef(initialActiveLabel);
+ const didScrollToEndRef = useRef(false);
// AutoFocus which only works on mount:
useEffect(() => {
@@ -251,8 +252,7 @@ function BaseTextInput(
const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([
styles.textInputContainer,
textInputContainerStyles,
- !!contentWidth && StyleUtils.getWidthStyle(textInputWidth),
- autoGrow && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace),
+ (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth),
!hideFocusedState && isFocused && styles.borderColorFocus,
(!!hasError || !!errorText) && styles.borderColorDanger,
autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined},
@@ -422,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 && (
;
+} & Pick;
type TextInputWithCurrencySymbolProps = Omit & {
onSelectionChange?: (start: number, end: number) => void;
diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index 6199a36abdca..ebffbf1b54b6 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -1,17 +1,30 @@
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
import Parser from '@libs/Parser';
+import CONST from '@src/CONST';
import type UseHtmlPaste from './types';
-const insertByCommand = (text: string) => {
- 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/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 d565893f2fa0..ceccfc61057e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -98,7 +98,6 @@ import type {
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
MissingPropertyParams,
- MovedFromSelfDMParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
@@ -229,6 +228,7 @@ const translations = {
optional: 'Optional',
new: 'New',
search: 'Search',
+ reports: 'Reports',
find: 'Find',
searchWithThreeDots: 'Search...',
next: 'Next',
@@ -647,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',
@@ -837,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',
@@ -980,7 +972,6 @@ const translations = {
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`,
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`,
threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
- movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `moved expense from self DM to ${workspaceName ?? `chat with ${reportName}`}`,
tagSelection: 'Select a tag to better organize your spend.',
categorySelection: 'Select a category to better organize your spend.',
error: {
@@ -1010,7 +1001,6 @@ const translations = {
splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.',
invalidMerchant: 'Please enter a correct merchant.',
atLeastOneAttendee: 'At least one attendee must be selected',
- invalidRate: 'Rate not valid for this workspace. Please select an available rate from the workspace.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
enableWallet: 'Enable wallet',
@@ -4442,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',
@@ -4456,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) {
@@ -4556,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',
@@ -5458,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 0b59f8a9044c..6b20790415b3 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -97,7 +97,6 @@ import type {
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
MissingPropertyParams,
- MovedFromSelfDMParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
@@ -220,6 +219,7 @@ const translations = {
new: 'Nuevo',
center: 'Centrar',
search: 'Buscar',
+ reports: 'Informes',
find: 'Encontrar',
searchWithThreeDots: 'Buscar...',
select: 'Seleccionar',
@@ -639,10 +639,6 @@ const translations = {
emoji: 'Emoji',
collapse: 'Colapsar',
expand: 'Expandir',
- tooltip: {
- title: '¡Empecemos!',
- subtitle: ' Presenta tu primer gasto',
- },
},
reportActionContextMenu: {
copyToClipboard: 'Copiar al portapapeles',
@@ -832,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',
@@ -978,7 +970,6 @@ const translations = {
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`,
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`,
threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
- movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `movió el gasto desde su propio mensaje directo a ${workspaceName ?? `un chat con ${reportName}`}`,
tagSelection: 'Selecciona una etiqueta para organizar mejor tus gastos.',
categorySelection: 'Selecciona una categoría para organizar mejor tus gastos.',
error: {
@@ -1008,7 +999,6 @@ const translations = {
splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.',
invalidMerchant: 'Por favor, introduce un comerciante correcto.',
atLeastOneAttendee: 'Debe seleccionarse al menos un asistente',
- invalidRate: 'Tasa no válida para este espacio de trabajo. Por favor, selecciona una tasa disponible en el espacio de trabajo.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`,
enableWallet: 'Habilitar billetera',
@@ -4491,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',
@@ -4505,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) {
@@ -4605,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',
@@ -5978,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/languages/params.ts b/src/languages/params.ts
index 9ac9cd585f4a..3088b99e753b 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -165,8 +165,6 @@ type ThreadRequestReportNameParams = {formattedAmount: string; comment: string};
type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string};
-type MovedFromSelfDMParams = {workspaceName?: string; reportName?: string};
-
type SizeExceededParams = {maxUploadSizeInMB: number};
type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number};
@@ -669,7 +667,7 @@ export type {
LoggedInAsParams,
ManagerApprovedAmountParams,
ManagerApprovedParams,
- MovedFromSelfDMParams,
+ SignUpNewFaceCodeParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
@@ -703,7 +701,6 @@ export type {
SetTheRequestParams,
SettleExpensifyCardParams,
SettledAfterAddedBankAccountParams,
- SignUpNewFaceCodeParams,
SizeExceededParams,
SplitAmountParams,
StepCounterParams,
diff --git a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
index 7b322189838a..78eb0adecc5e 100644
--- a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
@@ -20,12 +20,6 @@ type CategorizeTrackedExpenseParams = {
taxCode: string;
taxAmount: number;
billable?: boolean;
- waypoints?: string;
- customUnitRateID?: string;
- policyExpenseChatReportID?: string;
- policyExpenseCreatedReportActionID?: string;
- adminsChatReportID?: string;
- adminsCreatedReportActionID?: string;
};
export default CategorizeTrackedExpenseParams;
diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
index d44a7af5a444..cee4bc40d9ac 100644
--- a/src/libs/API/parameters/ShareTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
@@ -20,12 +20,6 @@ type ShareTrackedExpenseParams = {
taxCode: string;
taxAmount: number;
billable?: boolean;
- waypoints?: string;
- customUnitRateID?: string;
- policyExpenseChatReportID?: string;
- policyExpenseCreatedReportActionID?: string;
- adminsChatReportID?: string;
- adminsCreatedReportActionID?: string;
};
export default ShareTrackedExpenseParams;
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 24998db0e5cd..c3c12600f882 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -1,4 +1,3 @@
-import isEmpty from 'lodash/isEmpty';
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
@@ -10,8 +9,6 @@ import * as Localize from './Localize';
import Log from './Log';
import * as PolicyUtils from './PolicyUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
-// eslint-disable-next-line import/no-cycle
-import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicyName, getRootParentReport, isPolicyExpenseChat} from './ReportUtils';
import * as TransactionUtils from './TransactionUtils';
let allPolicyTags: OnyxCollection = {};
@@ -135,20 +132,6 @@ function getForDistanceRequest(newMerchant: string, oldMerchant: string, newAmou
});
}
-function getForExpenseMovedFromSelfDM(destinationReportID: string) {
- const destinationReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`];
- const rootParentReport = getRootParentReport(destinationReport);
-
- // The "Move report" flow only supports moving expenses to a policy expense chat or a 1:1 DM.
- const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName(rootParentReport) : buildReportNameFromParticipantNames({report: rootParentReport});
- const policyName = getPolicyName(rootParentReport, true);
-
- return Localize.translateLocal('iou.movedFromSelfDM', {
- reportName,
- workspaceName: !isEmpty(policyName) ? policyName : undefined,
- });
-}
-
/**
* Get the report action message when expense has been modified.
*
@@ -159,14 +142,9 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
if (!ReportActionsUtils.isModifiedExpenseAction(reportAction)) {
return '';
}
-
const reportActionOriginalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
const policyID = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';
- if (reportActionOriginalMessage?.movedToReportID) {
- return getForExpenseMovedFromSelfDM(reportActionOriginalMessage.movedToReportID);
- }
-
const removalFragments: string[] = [];
const setFragments: string[] = [];
const changeFragments: 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/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 a30de7b97198..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';
@@ -73,7 +75,6 @@ import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import Log from './Log';
import {isEmailPublicDomain} from './LoginUtils';
-// eslint-disable-next-line import/no-cycle
import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import linkingConfig from './Navigation/linkingConfig';
import Navigation from './Navigation/Navigation';
@@ -85,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 = {
@@ -628,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,
@@ -745,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}`];
}
@@ -762,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}`];
}
/**
@@ -779,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);
}
/**
@@ -880,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;
}
@@ -945,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;
}
@@ -992,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;
}
@@ -1014,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;
}
@@ -1077,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));
}
/**
@@ -1207,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);
}
@@ -1293,7 +1321,6 @@ function isConciergeChatReport(report: OnyxInputOrEntry): boolean {
}
function findSelfDMReportID(): string | undefined {
- const allReports = ReportConnection.getAllReports();
if (!allReports) {
return;
}
@@ -1414,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) {
@@ -1484,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));
}
/**
@@ -1630,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;
@@ -1643,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;
@@ -1666,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);
}
@@ -1674,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);
}
@@ -1903,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;
}
@@ -2161,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;
@@ -2349,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,
};
@@ -2574,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;
@@ -2595,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;
@@ -2849,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;
@@ -3227,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))) {
@@ -3511,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) {
@@ -3811,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}`]);
@@ -3903,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);
@@ -3916,21 +3944,6 @@ const reportNameCache = new Map): string => `${report?.reportID}-${report?.lastVisibleActionCreated}-${report?.reportName}`;
-/**
- * Get the title for a report using only participant names. This may be used for 1:1 DMs and other non-categorized chats.
- */
-function buildReportNameFromParticipantNames({report, personalDetails}: {report: OnyxEntry; personalDetails?: Partial}) {
- const participantsWithoutCurrentUser: number[] = [];
- Object.keys(report?.participants ?? {}).forEach((accountID) => {
- const accID = Number(accountID);
- if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) {
- participantsWithoutCurrentUser.push(accID);
- }
- });
- const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
- return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', ');
-}
-
/**
* Get the title for a report.
*/
@@ -4094,7 +4107,16 @@ function getReportName(
}
// Not a room or PolicyExpenseChat, generate title from first 5 other participants
- formattedName = buildReportNameFromParticipantNames({report, personalDetails});
+ const participantsWithoutCurrentUser: number[] = [];
+ Object.keys(report?.participants ?? {}).forEach((accountID) => {
+ const accID = Number(accountID);
+ if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) {
+ participantsWithoutCurrentUser.push(accID);
+ }
+ });
+ const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
+ const participantNames = participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', ');
+ formattedName = participantNames;
if (reportID) {
reportNameCache.set(cacheKey, {lastVisibleActionCreated: report?.lastVisibleActionCreated ?? '', reportName: formattedName});
@@ -4710,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);
@@ -6296,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;
}
@@ -6384,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),
);
@@ -6697,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 ?? {});
@@ -6722,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;
@@ -6741,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;
@@ -6752,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);
}
/**
@@ -6764,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));
}
/**
@@ -6917,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;
}
@@ -7245,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);
}
@@ -7269,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;
}
@@ -7299,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));
}
/**
@@ -7349,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);
}
@@ -7665,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;
}
@@ -8060,7 +8082,7 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxInputOrEntry report && report?.[reportFieldToCompare] === tripRoomReportID)
.map((report) => report?.reportID);
return tripTransactionReportIDs.flatMap((reportID) => reportsTransactions[reportID ?? ''] ?? []);
@@ -8313,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));
}
@@ -8336,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);
}
/**
@@ -8378,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);
}
/**
@@ -8485,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) {
@@ -8530,7 +8551,6 @@ export {
buildOptimisticWorkspaceChats,
buildOptimisticCardAssignedReportAction,
buildParticipantsFromAccountIDs,
- buildReportNameFromParticipantNames,
buildTransactionThread,
canAccessReport,
isReportNotFound,
@@ -8618,7 +8638,6 @@ export {
getPersonalDetailsForAccountID,
getPolicyDescriptionText,
getPolicyExpenseChat,
- getPolicyExpenseChatName,
getPolicyName,
getPolicyType,
getReimbursementDeQueuedActionMessage,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 86f7aaaecce9..b5ae73bed2e4 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -257,6 +257,17 @@ 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];
+ // Tracked and unreported expenses don't have a report, so we return early.
+ if (!report) {
+ return CONST.SEARCH.ACTION_TYPES.VIEW;
+ }
+
+ // 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;
}
@@ -265,8 +276,8 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr
return CONST.SEARCH.ACTION_TYPES.DONE;
}
- // We don't need to run the logic if this is not an iou/expense report, so let's shortcircuit the logic for performance reasons
- if (!ReportUtils.isMoneyRequestReport(report)) {
+ // We don't need to run the logic if this is not a transaction or iou/expense report, so let's shortcircuit the logic for performance reasons
+ if (!ReportUtils.isMoneyRequestReport(report) || (isTransaction && !data[key].isFromOneTransactionReport)) {
return CONST.SEARCH.ACTION_TYPES.VIEW;
}
@@ -295,11 +306,13 @@ 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;
}
- if (IOU.canSubmitReport(report, policy)) {
+ // 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;
}
@@ -589,4 +602,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/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/App.ts b/src/libs/actions/App.ts
index 61ce04655ae5..931f9e226995 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -89,6 +89,14 @@ Onyx.connect({
},
});
+let preservedUserSession: OnyxTypes.Session | undefined;
+Onyx.connect({
+ key: ONYXKEYS.PRESERVED_USER_SESSION,
+ callback: (value) => {
+ preservedUserSession = value;
+ },
+});
+
const KEYS_TO_PRESERVE: OnyxKey[] = [
ONYXKEYS.ACCOUNT,
ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
@@ -102,6 +110,7 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [
ONYXKEYS.PREFERRED_THEME,
ONYXKEYS.NVP_PREFERRED_LOCALE,
ONYXKEYS.CREDENTIALS,
+ ONYXKEYS.PRESERVED_USER_SESSION,
];
Onyx.connect({
@@ -524,6 +533,10 @@ function setIsUsingImportedState(usingImportedState: boolean) {
Onyx.set(ONYXKEYS.IS_USING_IMPORTED_STATE, usingImportedState);
}
+function setPreservedUserSession(session: OnyxTypes.Session) {
+ Onyx.set(ONYXKEYS.PRESERVED_USER_SESSION, session);
+}
+
function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) {
// The value of isUsingImportedState will be lost once Onyx is cleared, so we need to store it
const isStateImported = isUsingImportedState;
@@ -538,6 +551,11 @@ function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) {
Navigation.navigate(ROUTES.HOME);
}
+ if (preservedUserSession) {
+ Onyx.set(ONYXKEYS.SESSION, preservedUserSession);
+ Onyx.set(ONYXKEYS.PRESERVED_USER_SESSION, null);
+ }
+
// Requests in a sequential queue should be called even if the Onyx state is reset, so we do not lose any pending data.
// However, the OpenApp request must be called before any other request in a queue to ensure data consistency.
// To do that, sequential queue is cleared together with other keys, and then it's restored once the OpenApp request is resolved.
@@ -574,5 +592,6 @@ export {
updateLastRoute,
setIsUsingImportedState,
clearOnyxAndResetApp,
+ setPreservedUserSession,
KEYS_TO_PRESERVE,
};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 6b45283421ea..c094873379b9 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -8,7 +8,6 @@ import ReceiptGeneric from '@assets/images/receipt-generic.png';
import * as API from '@libs/API';
import type {
ApproveMoneyRequestParams,
- CategorizeTrackedExpenseParams as CategorizeTrackedExpenseApiParams,
CompleteSplitBillParams,
CreateDistanceRequestParams,
CreateWorkspaceParams,
@@ -22,7 +21,6 @@ import type {
SendInvoiceParams,
SendMoneyParams,
SetNameValuePairParams,
- ShareTrackedExpenseParams as ShareTrackedExpenseApiParams,
SplitBillParams,
StartSplitBillParams,
SubmitReportParams,
@@ -50,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';
@@ -127,8 +124,6 @@ type CategorizeTrackedExpenseTransactionParams = {
tag?: string;
billable?: boolean;
receipt?: Receipt;
- waypoints?: string;
- customUnitRateID?: string;
};
type CategorizeTrackedExpensePolicyParams = {
policyID: string;
@@ -352,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({
@@ -591,31 +595,7 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename:
* Set custom unit rateID for the transaction draft
*/
function setCustomUnitRateID(transactionID: string, customUnitRateID: string) {
- const isFakeP2PRate = customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID;
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
- comment: {
- customUnit: {
- customUnitRateID,
- ...(!isFakeP2PRate && {defaultP2PRate: null}),
- },
- },
- });
-}
-
-/**
- * Revert custom unit of the draft transaction to the original transaction's value
- */
-function resetDraftTransactionsCustomUnit(transactionID: string) {
- const originalTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- if (!originalTransaction) {
- return;
- }
-
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
- comment: {
- customUnit: originalTransaction.comment?.customUnit ?? {},
- },
- });
+ Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID}}});
}
/** Set the distance rate of a new transaction */
@@ -686,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({
@@ -1499,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(
@@ -1862,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}`];
@@ -2221,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) {
@@ -2455,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;
@@ -2701,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}`];
@@ -3044,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}`];
@@ -3214,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;
@@ -3255,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;
@@ -3346,6 +3320,7 @@ type UpdateMoneyRequestDistanceParams = {
policy?: OnyxEntry;
policyTagList?: OnyxEntry;
policyCategories?: OnyxEntry;
+ transactionBackup: OnyxEntry;
};
/** Updates the waypoints of a distance expense */
@@ -3357,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;
@@ -3380,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);
}
@@ -3411,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;
@@ -3440,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;
@@ -3614,8 +3620,8 @@ function categorizeTrackedExpense(trackedExpenseParams: CategorizeTrackedExpense
optimisticData?.push(...moveTransactionOptimisticData);
successData?.push(...moveTransactionSuccessData);
failureData?.push(...moveTransactionFailureData);
-
- const parameters: CategorizeTrackedExpenseApiParams = {
+ const parameters = {
+ onyxData,
...reportInformation,
...policyParams,
...transactionParams,
@@ -3658,8 +3664,6 @@ function shareTrackedExpense(
taxAmount = 0,
billable?: boolean,
receipt?: Receipt,
- waypoints?: string,
- customUnitRateID?: string,
createdWorkspaceParams?: CreateWorkspaceParams,
) {
const {optimisticData, successData, failureData} = onyxData ?? {};
@@ -3683,7 +3687,7 @@ function shareTrackedExpense(
successData?.push(...moveTransactionSuccessData);
failureData?.push(...moveTransactionFailureData);
- const parameters: ShareTrackedExpenseApiParams = {
+ const parameters = {
policyID,
transactionID,
moneyRequestPreviewReportActionID,
@@ -3703,8 +3707,6 @@ function shareTrackedExpense(
taxAmount,
billable,
receipt,
- waypoints,
- customUnitRateID,
policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID,
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
@@ -3942,7 +3944,6 @@ function trackExpense(
// Pass an open receipt so the distance expense will show a map with the route optimistically
const trackedReceipt = validWaypoints ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN} : receipt;
- const sanitizedWaypoints = validWaypoints ? JSON.stringify(sanitizeRecentWaypoints(validWaypoints)) : undefined;
const {
createdWorkspaceParams,
@@ -3997,7 +3998,7 @@ function trackExpense(
if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) {
return;
}
- const transactionParams: CategorizeTrackedExpenseTransactionParams = {
+ const transactionParams = {
transactionID: transaction?.transactionID ?? '-1',
amount,
currency,
@@ -4010,14 +4011,12 @@ function trackExpense(
tag,
billable,
receipt: trackedReceipt,
- waypoints: sanitizedWaypoints,
- customUnitRateID,
};
- const policyParams: CategorizeTrackedExpensePolicyParams = {
+ const policyParams = {
policyID: chatReport?.policyID ?? '-1',
isDraftPolicy,
};
- const reportInformation: CategorizeTrackedExpenseReportInformation = {
+ const reportInformation = {
moneyRequestPreviewReportActionID: iouAction?.reportActionID ?? '-1',
moneyRequestReportID: iouReport?.reportID ?? '-1',
moneyRequestCreatedReportActionID: createdIOUReportActionID ?? '-1',
@@ -4027,7 +4026,7 @@ function trackExpense(
transactionThreadReportID: transactionThreadReportID ?? '-1',
reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '-1',
};
- const trackedExpenseParams: CategorizeTrackedExpenseParams = {
+ const trackedExpenseParams = {
onyxData,
reportInformation,
transactionParams,
@@ -4065,8 +4064,6 @@ function trackExpense(
taxAmount,
billable,
trackedReceipt,
- sanitizedWaypoints,
- customUnitRateID,
createdWorkspaceParams,
);
break;
@@ -4096,7 +4093,7 @@ function trackExpense(
receiptGpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined,
transactionThreadReportID: transactionThreadReportID ?? '-1',
createdReportActionIDForThread: createdReportActionIDForThread ?? '-1',
- waypoints: sanitizedWaypoints,
+ waypoints: validWaypoints ? JSON.stringify(sanitizeRecentWaypoints(validWaypoints)) : undefined,
customUnitRateID,
};
if (actionableWhisperReportActionIDParam) {
@@ -4124,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) {
@@ -4423,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) {
@@ -5188,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}`];
@@ -5545,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;
@@ -5566,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}`];
@@ -5731,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)) {
@@ -6170,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;
@@ -7141,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);
@@ -7195,7 +7189,7 @@ function canIOUBePaid(
reimbursableSpend !== 0 &&
!isChatReportArchived &&
!isAutoReimbursable &&
- !shouldBeApproved &&
+ (!shouldBeApproved || !shouldCheckApprovedState) &&
!isPayAtEndExpenseReport
);
}
@@ -8191,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[] = [
{
@@ -8314,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[] = [
{
@@ -8545,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}`,
@@ -8792,7 +8786,6 @@ export {
replaceReceipt,
requestMoney,
resetSplitShares,
- resetDraftTransactionsCustomUnit,
savePreferredPaymentMethod,
sendInvoice,
sendMoneyElsewhere,
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 65e5cfe62c63..50e37ba6afe5 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -273,52 +273,52 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po
}
function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], 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(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {isActionLoading: isLoading}])) as Partial),
+ ? (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[] = 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});
- API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData});
+ 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[]) {
@@ -380,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,
@@ -400,8 +392,6 @@ export {
clearAllFilters,
clearAdvancedFilters,
deleteSavedSearch,
- dismissSavedSearchRenameTooltip,
- showSavedSearchRenameTooltip,
payMoneyRequestOnSearch,
approveMoneyRequestOnSearch,
handleActionButtonPress,
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..f91ab98f7b1d 100644
--- a/src/libs/actions/Welcome/index.ts
+++ b/src/libs/actions/Welcome/index.ts
@@ -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/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 8d0d69a09988..f0308301e142 100755
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -124,7 +124,7 @@ function ProfilePage({route}: ProfilePageProps) {
return {accountID: optimisticAccountID, login: loginParams, displayName: loginParams};
}, [personalDetails, accountID, loginParams, isValidAccountID]);
- const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, undefined, undefined, isCurrentUser);
+ const displayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details, undefined, undefined, isCurrentUser));
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const fallbackIcon = details?.fallbackIcon ?? '';
const login = details?.login ?? '';
diff --git a/src/pages/ReportParticipantDetailsPage.tsx b/src/pages/ReportParticipantDetailsPage.tsx
index 342449ae2bef..bfc4506818d1 100644
--- a/src/pages/ReportParticipantDetailsPage.tsx
+++ b/src/pages/ReportParticipantDetailsPage.tsx
@@ -1,7 +1,6 @@
import React, {useCallback} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import Avatar from '@components/Avatar';
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
@@ -26,24 +25,18 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
+import type {PersonalDetails} from '@src/types/onyx';
import NotFoundPage from './ErrorPage/NotFoundPage';
import withReportOrNotFound from './home/report/withReportOrNotFound';
import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound';
-type ReportParticipantDetailsOnyxProps = {
- /** Personal details of all users */
- personalDetails: OnyxEntry;
-};
+type ReportParticipantDetailsPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps;
-type ReportParticipantDetailsPageProps = WithReportOrNotFoundProps &
- PlatformStackScreenProps &
- ReportParticipantDetailsOnyxProps;
-
-function ReportParticipantDetails({personalDetails, report, route}: ReportParticipantDetailsPageProps) {
+function ReportParticipantDetails({report, route}: ReportParticipantDetailsPageProps) {
const styles = useThemeStyles();
- const {translate} = useLocalize();
+ const {formatPhoneNumber, translate} = useLocalize();
const StyleUtils = useStyleUtils();
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false);
@@ -54,7 +47,7 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic
const member = report?.participants?.[accountID];
const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
const fallbackIcon = details.fallbackIcon ?? '';
- const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details);
+ const displayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details));
const isCurrentUserAdmin = ReportUtils.isGroupChatAdmin(report, currentUserPersonalDetails?.accountID);
const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID;
const removeUser = useCallback(() => {
@@ -149,10 +142,4 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic
ReportParticipantDetails.displayName = 'ReportParticipantDetails';
-export default withReportOrNotFound()(
- withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- })(ReportParticipantDetails),
-);
+export default withReportOrNotFound()(ReportParticipantDetails);
diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx
index 31931aa45544..4c63cc4b4492 100755
--- a/src/pages/ReportParticipantsPage.tsx
+++ b/src/pages/ReportParticipantsPage.tsx
@@ -391,7 +391,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
onCancel={() => setRemoveMembersConfirmModalVisible(false)}
prompt={translate('workspace.people.removeMembersPrompt', {
count: selectedMembers.length,
- memberName: PersonalDetailsUtils.getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? '',
+ memberName: formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? ''),
})}
confirmText={translate('common.remove')}
cancelText={translate('common.cancel')}
diff --git a/src/pages/RoomMemberDetailsPage.tsx b/src/pages/RoomMemberDetailsPage.tsx
index 7ec586fae9a4..e10dd09b206a 100644
--- a/src/pages/RoomMemberDetailsPage.tsx
+++ b/src/pages/RoomMemberDetailsPage.tsx
@@ -17,6 +17,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as Report from '@libs/actions/Report';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {RoomMembersNavigatorParamList} from '@libs/Navigation/types';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
@@ -33,7 +34,7 @@ type RoomMemberDetailsPagePageProps = WithReportOrNotFoundProps & PlatformStackS
function RoomMemberDetailsPage({report, route}: RoomMemberDetailsPagePageProps) {
const styles = useThemeStyles();
- const {translate} = useLocalize();
+ const {formatPhoneNumber, translate} = useLocalize();
const StyleUtils = useStyleUtils();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
@@ -47,7 +48,7 @@ function RoomMemberDetailsPage({report, route}: RoomMemberDetailsPagePageProps)
const member = report?.participants?.[accountID];
const details = personalDetails?.[accountID] ?? ({} as PersonalDetails);
const fallbackIcon = details.fallbackIcon ?? '';
- const displayName = details.displayName ?? '';
+ const displayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details));
const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID;
const isSelectedMemberOwner = accountID === report.ownerAccountID;
const shouldDisableRemoveUser = (ReportUtils.isPolicyExpenseChat(report) && PolicyUtils.isUserPolicyAdmin(policy, details.login)) || isSelectedMemberCurrentUser || isSelectedMemberOwner;
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 16c10fb69116..0f29f1d00ba9 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -373,7 +373,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
onCancel={() => setRemoveMembersConfirmModalVisible(false)}
prompt={translate('roomMembersPage.removeMembersPrompt', {
count: selectedMembers.length,
- memberName: PersonalDetailsUtils.getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? '',
+ memberName: formatPhoneNumber(PersonalDetailsUtils.getPersonalDetailsByIDs(selectedMembers, currentUserAccountID).at(0)?.displayName ?? ''),
})}
confirmText={translate('common.remove')}
cancelText={translate('common.cancel')}
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 02eca4b9fbbc..10c8401b98aa 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -439,11 +439,6 @@ function AdvancedSearchFilters() {
return;
}
- // We only want to show the tooltip once, the NVP will not be set if the user has not saved a search yet
- if (!savedSearches) {
- SearchActions.showSavedSearchRenameTooltip();
- }
-
SearchActions.saveSearch({
queryJSON,
});
diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx
index bfe378409af7..2b4f1a2dc561 100644
--- a/src/pages/Search/SearchPageBottomTab.tsx
+++ b/src/pages/Search/SearchPageBottomTab.tsx
@@ -108,7 +108,7 @@ function SearchPageBottomTab() {
diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx
index 5c93a3877ff6..4a3c0881b5a6 100644
--- a/src/pages/Search/SearchTypeMenu.tsx
+++ b/src/pages/Search/SearchTypeMenu.tsx
@@ -8,6 +8,7 @@ import MenuItem from '@components/MenuItem';
import MenuItemList from '@components/MenuItemList';
import type {MenuItemWithLink} from '@components/MenuItemList';
import {usePersonalDetails} from '@components/OnyxProvider';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import ScrollView from '@components/ScrollView';
import type {SearchQueryJSON} from '@components/Search/types';
@@ -62,7 +63,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
const {singleExecution} = useSingleExecution();
const {translate} = useLocalize();
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES);
- const [shouldShowSavedSearchRenameTooltip] = useOnyx(ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP);
+ const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.RENAME_SAVED_SEARCH);
const {showDeleteModal, DeleteConfirmModal} = useDeleteSavedSearch();
const [session] = useOnyx(ONYXKEYS.SESSION);
@@ -118,65 +119,69 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
[showDeleteModal],
);
- const createSavedSearchMenuItem = (item: SaveSearchItem, key: string, isNarrow: boolean, index: number) => {
- let title = item.name;
- if (title === item.query) {
- const jsonQuery = SearchQueryUtils.buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON);
- title = SearchQueryUtils.buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates);
- }
-
- const baseMenuItem: SavedSearchMenuItem = {
- key,
- title,
- hash: key,
- query: item.query,
- shouldShowRightComponent: true,
- focused: Number(key) === hash,
- onPress: () => {
- SearchActions.clearAllFilters();
- Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: item?.query ?? '', name: item?.name}));
- },
- rightComponent: (
-
- ),
- styles: [styles.alignItemsCenter],
- pendingAction: item.pendingAction,
- disabled: item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
- shouldIconUseAutoWidthStyle: true,
- };
+ const createSavedSearchMenuItem = useCallback(
+ (item: SaveSearchItem, key: string, isNarrow: boolean, index: number) => {
+ let title = item.name;
+ if (title === item.query) {
+ const jsonQuery = SearchQueryUtils.buildSearchQueryJSON(item.query) ?? ({} as SearchQueryJSON);
+ title = SearchQueryUtils.buildUserReadableQueryString(jsonQuery, personalDetails, reports, taxRates);
+ }
- if (!isNarrow) {
- return {
- ...baseMenuItem,
- shouldRenderTooltip: index === 0 && shouldShowSavedSearchRenameTooltip === true,
- tooltipAnchorAlignment: {
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
- },
- tooltipShiftHorizontal: -32,
- tooltipShiftVertical: 15,
- tooltipWrapperStyle: [styles.bgPaleGreen, styles.mh4, styles.pv2],
- onHideTooltip: SearchActions.dismissSavedSearchRenameTooltip,
- renderTooltipContent: () => {
- return (
-
-
- {translate('search.saveSearchTooltipText')}
-
- );
+ const baseMenuItem: SavedSearchMenuItem = {
+ key,
+ title,
+ hash: key,
+ query: item.query,
+ shouldShowRightComponent: true,
+ focused: Number(key) === hash,
+ onPress: () => {
+ SearchActions.clearAllFilters();
+ Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: item?.query ?? '', name: item?.name}));
},
+ rightComponent: (
+
+ ),
+ styles: [styles.alignItemsCenter],
+ pendingAction: item.pendingAction,
+ disabled: item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ shouldIconUseAutoWidthStyle: true,
};
- }
- return baseMenuItem;
- };
+ if (!isNarrow) {
+ return {
+ ...baseMenuItem,
+ shouldRenderTooltip: index === 0 && shouldShowProductTrainingTooltip,
+ tooltipAnchorAlignment: {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ },
+ tooltipShiftHorizontal: -32,
+ tooltipShiftVertical: 15,
+ tooltipWrapperStyle: [styles.bgPaleGreen, styles.mh4, styles.pv2],
+ onHideTooltip: hideProductTrainingTooltip,
+ renderTooltipContent: renderProductTrainingTooltip,
+ };
+ }
+ return baseMenuItem;
+ },
+ [
+ hash,
+ getOverflowMenu,
+ styles.alignItemsCenter,
+ styles.bgPaleGreen,
+ styles.mh4,
+ styles.pv2,
+ personalDetails,
+ reports,
+ taxRates,
+ shouldShowProductTrainingTooltip,
+ hideProductTrainingTooltip,
+ renderProductTrainingTooltip,
+ ],
+ );
const route = useRoute();
const scrollViewRef = useRef(null);
@@ -201,12 +206,12 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
scrollViewRef.current.scrollTo({y: scrollOffset, animated: false});
}, [getScrollOffset, route]);
- const savedSearchesMenuItems = () => {
+ const savedSearchesMenuItems = useCallback(() => {
if (!savedSearches) {
return [];
}
return Object.entries(savedSearches).map(([key, item], index) => createSavedSearchMenuItem(item, key, shouldUseNarrowLayout, index));
- };
+ }, [createSavedSearchMenuItem, savedSearches, shouldUseNarrowLayout]);
const renderSavedSearchesSection = useCallback(
(menuItems: MenuItemWithLink[]) => (
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index 18f6c5960b5e..403f8c2c1e5f 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -8,8 +8,8 @@ import ContextMenuItem from '@components/ContextMenuItem';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
-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 useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
@@ -21,6 +21,7 @@ import Clipboard from '@libs/Clipboard';
import Navigation from '@libs/Navigation/Navigation';
import type {BackToParams} from '@libs/Navigation/types';
import * as ReportUtils from '@libs/ReportUtils';
+import shouldAllowDownloadQRCode from '@libs/shouldAllowDownloadQRCode';
import * as Url from '@libs/Url';
import * as UserUtils from '@libs/UserUtils';
import CONST from '@src/CONST';
@@ -59,7 +60,8 @@ function ShareCodePage({report, policy, backTo}: ShareCodePageProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {environmentURL} = useEnvironment();
- const qrCodeRef = useRef(null);
+ const qrCodeRef = useRef(null);
+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const isReport = !!report?.reportID;
@@ -112,24 +114,17 @@ function ShareCodePage({report, policy, backTo}: ShareCodePageProps) {
/>
- {/*
- 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.
- */}
-
@@ -143,6 +138,18 @@ function ShareCodePage({report, policy, backTo}: ShareCodePageProps) {
onPress={() => Clipboard.setString(url)}
shouldLimitWidth={false}
/>
+ {/* Remove this platform specific condition 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 && (
+
{shouldDisplaySearchRouter && }
@@ -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 : {}}
>
{
- 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/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 2a1d085ddf3c..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';
@@ -187,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);
@@ -198,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();
@@ -207,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
@@ -233,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 '';
@@ -372,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(() => {
@@ -439,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(),
@@ -449,7 +388,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}),
},
];
- }, [canUseCombinedTrackSubmit, translate, selfDMReportID, hasSeenTrackTraining, isOffline, shouldRedirectToExpensifyClassic]);
+ }, [translate, shouldRedirectToExpensifyClassic]);
const quickActionMenuItems = useMemo(() => {
// Define common properties in baseQuickAction
@@ -465,8 +404,10 @@ 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) {
@@ -482,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,
},
];
}
@@ -501,7 +441,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
}, true);
}),
shouldShowSubscriptRightAvatar: true,
- shouldRenderTooltip: false,
},
];
}
@@ -513,13 +452,14 @@ 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,
diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx
index 3b4f66c32738..ba406c3ddef6 100644
--- a/src/pages/iou/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/MoneyRequestAmountForm.tsx
@@ -19,7 +19,6 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
-import variables from '@styles/variables';
import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
@@ -260,7 +259,6 @@ function MoneyRequestAmountForm(
>
() => {
- if (!isMovingTransactionFromTrackExpense) {
- return;
- }
-
- IOU.resetDraftTransactionsCustomUnit(transactionID);
- },
- [isMovingTransactionFromTrackExpense, transactionID],
- );
-
useEffect(() => {
const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat);
if (policyExpenseChat?.policyID && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
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/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
index df0504a25c01..fb2484ea414f 100644
--- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx
+++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
@@ -44,7 +44,6 @@ function IOURequestStepParticipants({
const numberOfParticipants = useRef(participants?.length ?? 0);
const iouRequestType = TransactionUtils.getRequestType(transaction);
const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT;
- const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action);
const headerTitle = useMemo(() => {
if (action === CONST.IOU.ACTION.CATEGORIZE) {
return translate('iou.categorize');
@@ -76,27 +75,23 @@ function IOURequestStepParticipants({
// the image ceases to exist. The best way for the user to recover from this is to start over from the start of the expense process.
// skip this in case user is moving the transaction as the receipt path will be valid in that case
useEffect(() => {
- if (isMovingTransactionFromTrackExpense) {
+ if (IOUUtils.isMovingTransactionFromTrackExpense(action)) {
return;
}
IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename ?? '', receiptPath ?? '', () => {}, iouRequestType, iouType, transactionID, reportID, receiptType ?? '');
- }, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID, isMovingTransactionFromTrackExpense]);
+ }, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID, action]);
const addParticipant = useCallback(
(val: Participant[]) => {
HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS);
const firstParticipantReportID = val.at(0)?.reportID ?? '';
+ const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID);
const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID);
numberOfParticipants.current = val.length;
- IOU.setMoneyRequestParticipants(transactionID, val);
- if (!isMovingTransactionFromTrackExpense) {
- // When moving the transaction, keep the original rate and let the user manually change it to the one they want from the workspace.
- // Otherwise, select the default one automatically.
- const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID);
- IOU.setCustomUnitRateID(transactionID, rateID);
- }
+ IOU.setMoneyRequestParticipants(transactionID, val);
+ IOU.setCustomUnitRateID(transactionID, rateID);
// When multiple participants are selected, the reportID is generated at the end of the confirmation step.
// So we are resetting selectedReportID ref to the reportID coming from params.
@@ -108,7 +103,7 @@ function IOURequestStepParticipants({
// When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step.
selectedReportID.current = firstParticipantReportID || reportID;
},
- [iouType, reportID, transactionID, isMovingTransactionFromTrackExpense],
+ [iouType, reportID, transactionID],
);
const goToNextStep = useCallback(() => {
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/UpdateDelegateRole/UpdateDelegateRolePage.tsx b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx
index b43c970ef5ff..097b1ce679af 100644
--- a/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx
+++ b/src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useState} from 'react';
+import React, {useEffect} from 'react';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
@@ -22,7 +22,7 @@ type UpdateDelegateRolePageProps = PlatformStackScreenProps ({
@@ -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/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx
index bd0ce596c733..defc5eb941ac 100644
--- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx
+++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx
@@ -144,10 +144,7 @@ function TroubleshootPage() {
/>
-
+
(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 && (
+ qrCodeRef.current?.download?.()}
+ wrapperStyle={themeStyles.sectionMenuItemTopDescription}
+ />
+ )}
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