diff --git a/.eslintrc.js b/.eslintrc.js
index f852c970f85c..281f8269804e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,12 +1,13 @@
const restrictedImportPaths = [
{
name: 'react-native',
- importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable'],
+ importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text'],
message: [
'',
"For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.",
"For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.",
"For 'StatusBar', please use 'src/libs/StatusBar' instead.",
+ "For 'Text', please use '@components/Text' instead.",
].join('\n'),
},
{
diff --git a/README.md b/README.md
index 3b9010695760..f6629af8604d 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ In order to have more consistent builds, we use a strict `node` and `npm` versio
## Configuring HTTPS
The webpack development server now uses https. If you're using a mac, you can simply run `npm run setup-https`.
-If you're using another operating system, you will need to ensure `mkcert` is installed, and then follow the instructions in the repository to generate certificates valid for `new.expesify.com.dev` and `localhost`. The certificate should be named `certificate.pem` and the key should be named `key.pem`. They should be placed in `config/webpack`.
+If you're using another operating system, you will need to ensure `mkcert` is installed, and then follow the instructions in the repository to generate certificates valid for `dev.new.expensify.com` and `localhost`. The certificate should be named `certificate.pem` and the key should be named `key.pem`. They should be placed in `config/webpack`.
## Running the web app 🕸
* To run the **development web app**: `npm run web`
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 83bf303b0f0c..8bf0bf928e4e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001042406
- versionName "1.4.24-6"
+ versionCode 1001042701
+ versionName "1.4.27-1"
}
flavorDimensions "default"
diff --git a/android/app/src/main/res/drawable/ic_launcher_monochrome.png b/android/app/src/main/res/drawable/ic_launcher_monochrome.png
index b1a286b6f8dd..0af99b087923 100644
Binary files a/android/app/src/main/res/drawable/ic_launcher_monochrome.png and b/android/app/src/main/res/drawable/ic_launcher_monochrome.png differ
diff --git a/assets/animations/Coin.lottie b/assets/animations/Coin.lottie
new file mode 100644
index 000000000000..e426f7efdc3c
Binary files /dev/null and b/assets/animations/Coin.lottie differ
diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg
new file mode 100644
index 000000000000..047a43073b3c
--- /dev/null
+++ b/assets/images/chatbubble-add.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg
new file mode 100644
index 000000000000..9da789510276
--- /dev/null
+++ b/assets/images/chatbubble-unread.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/assets/images/expensify-logo--adhoc.svg b/assets/images/expensify-logo--adhoc.svg
index 273002deca9b..52b381dc4b78 100644
--- a/assets/images/expensify-logo--adhoc.svg
+++ b/assets/images/expensify-logo--adhoc.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/expensify-logo--dev.svg b/assets/images/expensify-logo--dev.svg
index e8e3fb5033d9..2c9ae142e283 100644
--- a/assets/images/expensify-logo--dev.svg
+++ b/assets/images/expensify-logo--dev.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/expensify-logo--staging.svg b/assets/images/expensify-logo--staging.svg
index 78dcc1581f99..a1e7482c133b 100644
--- a/assets/images/expensify-logo--staging.svg
+++ b/assets/images/expensify-logo--staging.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/home-background--mobile-new.svg b/assets/images/home-background--mobile-new.svg
index 0da937cae059..d81f2a18cc78 100644
--- a/assets/images/home-background--mobile-new.svg
+++ b/assets/images/home-background--mobile-new.svg
@@ -1,8835 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/new-expensify.svg b/assets/images/new-expensify.svg
index 38276ecd9385..7bfef1fd38b4 100644
--- a/assets/images/new-expensify.svg
+++ b/assets/images/new-expensify.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/product-illustrations/payment-hands.svg b/assets/images/product-illustrations/payment-hands.svg
index bf76b528ee76..2dbebd24994b 100644
--- a/assets/images/product-illustrations/payment-hands.svg
+++ b/assets/images/product-illustrations/payment-hands.svg
@@ -1 +1,140 @@
-
\ No newline at end of file
+
diff --git a/assets/images/product-illustrations/telescope.svg b/assets/images/product-illustrations/telescope.svg
index 95617c801789..1830dff0fe3c 100644
--- a/assets/images/product-illustrations/telescope.svg
+++ b/assets/images/product-illustrations/telescope.svg
@@ -1,79 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/signIn/google-logo.svg b/assets/images/signIn/google-logo.svg
index 4fbdc804a0a2..169ea34b23ee 100644
--- a/assets/images/signIn/google-logo.svg
+++ b/assets/images/signIn/google-logo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__bigrocket.svg b/assets/images/simple-illustrations/simple-illustration__bigrocket.svg
index 1afd5f66b6ea..64d6dc2200f0 100644
--- a/assets/images/simple-illustrations/simple-illustration__bigrocket.svg
+++ b/assets/images/simple-illustrations/simple-illustration__bigrocket.svg
@@ -1,100 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg
index 829d3ee2e3fe..ab9d3ae4db70 100644
--- a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg
+++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg
@@ -1,22 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__handcard.svg b/assets/images/simple-illustrations/simple-illustration__handcard.svg
index 7419b33d425c..a49e0ee5b77f 100644
--- a/assets/images/simple-illustrations/simple-illustration__handcard.svg
+++ b/assets/images/simple-illustrations/simple-illustration__handcard.svg
@@ -1,41 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg b/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg
index 471b978bb97e..5b5e12a99a9b 100644
--- a/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg
+++ b/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg
@@ -1,98 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__hourglass.svg b/assets/images/simple-illustrations/simple-illustration__hourglass.svg
index 539e1e45b795..683e74a657e8 100644
--- a/assets/images/simple-illustrations/simple-illustration__hourglass.svg
+++ b/assets/images/simple-illustrations/simple-illustration__hourglass.svg
@@ -1,56 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__mailbox.svg b/assets/images/simple-illustrations/simple-illustration__mailbox.svg
index 81b1f508fb52..7af7c71e24f3 100644
--- a/assets/images/simple-illustrations/simple-illustration__mailbox.svg
+++ b/assets/images/simple-illustrations/simple-illustration__mailbox.svg
@@ -1,71 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__smallrocket.svg b/assets/images/simple-illustrations/simple-illustration__smallrocket.svg
index 0f8f166c849f..388bb968a762 100644
--- a/assets/images/simple-illustrations/simple-illustration__smallrocket.svg
+++ b/assets/images/simple-illustrations/simple-illustration__smallrocket.svg
@@ -1,45 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__trashcan.svg b/assets/images/simple-illustrations/simple-illustration__trashcan.svg
index 4e66efa0a67e..66cc9ee27550 100644
--- a/assets/images/simple-illustrations/simple-illustration__trashcan.svg
+++ b/assets/images/simple-illustrations/simple-illustration__trashcan.svg
@@ -1,52 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/thumbs-up.svg b/assets/images/thumbs-up.svg
index ef81c88fc854..3e2a4a5125b6 100644
--- a/assets/images/thumbs-up.svg
+++ b/assets/images/thumbs-up.svg
@@ -1,8 +1 @@
-
-
+
\ No newline at end of file
diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md
index 6e02cae677bb..9eb16099f535 100644
--- a/contributingGuides/CONTRIBUTING.md
+++ b/contributingGuides/CONTRIBUTING.md
@@ -5,7 +5,7 @@ Welcome! Thanks for checking out the New Expensify app and taking the time to co
If you would like to become an Expensify contributor, the first step is to read this document in its **entirety**. The second step is to review the README guidelines [here](https://github.com/Expensify/App/blob/main/README.md) to understand our coding philosophy and for a general overview of the code repository (i.e. how to run the app locally, testing, storage, our app philosophy, etc). Please read both documents before asking questions, as it may be covered within the documentation.
#### Test Accounts
-You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar.
+You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. Do not use Expensify employee or customer accounts for testing.
**Notes**:
diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md
deleted file mode 100644
index e6d8f2fedb73..000000000000
--- a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-title: Free Trial
-description: Learn more about your free trial with Expensify.
----
-
-# Overview
-New customers can take advantage of a seven-day Free Trial on a group Workspace. This trial period allows you to fully explore Expensify's features and capabilities before deciding on a subscription.
-During the trial, your organization will have complete access to all the features and functionality offered by the Collect or Control workspace plan. This post provides a step-by-step guide on how to begin, oversee, and successfully conclude your organization's Expensify Free Trial.
-
-# How to start a Free Trial
-1. Sign up for a new Expensify account at expensify.com.
-2. After you've signed up for a new Expensify account, you will see a task on your Home page asking if you are using Expensify for your business or as an individual.
- a. **Note**: If you select “Individual”, Expensify is free for individuals for up to 25 SmartScans per month. Selecting Individual will **not** start a Free Trial. More details on individual subscriptions can be found [here](https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription).
-3. Select the Business option.
-4. Select which Expensify features you'd like to set up for your organization.
-5. Congratulations, your seven-day Free Trial has started!
-
-Once you've made these selections, we'll automatically enroll you in a Free Trial and create a Group Workspace, which will trigger new tasks on your Home page to walk you through how to configure Expensify for your organization. If you have any questions about a specific task or need assistance setting up your company, you can speak with your designated Setup Specialist by clicking “Support” on the left-hand navigation menu and selecting their name. This will allow you to message your Setup Specialist, and request a call if you need.
-
-# How to unlock additional Free Trial weeks
-When you begin a Free Trial, you'll have an initial seven-day period before you need to provide your billing information to continue using Expensify. Luckily, Expensify offers the option to extend your Free Trial by an additional five weeks!
-
-To access these extra free weeks, all you need to do is complete the tasks on your Home page marked with the "Free Week!" banner. Each task completed in this category will automatically add seven more days to your trial. You can easily keep track of the remaining days of your Free Trial by checking the top right-hand corner of your Expensify Home page.
-
-# How to make the most of your Free Trial
-- Complete all of the "Free Week!" tasks right away. These tasks are crucial for establishing your organization's Workspace, and finishing them will give you a clear idea of how much time you have left in your Free Trial.
-
-- Every Free Trial has dedicated access to a Setup Specialist who can help you set up your account to your preferences. We highly recommend booking a call with your dedicated Setup Specialist as soon as you start your Free Trial. If you ever need assistance with a setup task, your tasks also include demo videos.
-
-- Invite a few employees to join Expensify as early as possible during the Free Trial. Bringing employees on board and having them submit expenses will allow you to fully experience how all of the features and functionalities of Expensify work together to save time. We provide excellent resources to help employees get started with Expensify.
-
-- Establish a connection between Expensify and your accounting system from the outset. By doing this early, you can start testing Expensify comprehensively from end to end.
-
-{% include faq-begin.md %}
-## What happens when my Free Trial ends?
-If you’ve already added a billing card to Expensify, you will automatically start your organization’s Expensify subscription after your Free Trial ends. At the beginning of the following month, we'll bill the card you have on file for your subscription, adjusting the charge to exclude the Free Trial period.
-If your Free Trial concludes without a billing card on file, you will see a notification on your Home page saying, 'Your Free Trial has expired.'
-If you still have outstanding 'Free Week!' tasks, completing them will extend your Free Trial by additional days.
-If you continue without adding a billing card, you will be granted a five-day grace period after the following billing cycle before all Group Workspace functionality is disabled. To continue using Expensify's Group Workspace features, you will need to input your billing card information and initiate a subscription.
-
-## How can I downgrade my account after my Free Trial ends?
-If you’d like to downgrade to an individual account after your Free Trial has ended, you will need to delete any Group Workspace that you have created. This action will remove the Workspaces, subscription, and any amount owed. You can do this in one of two ways from the Expensify web app:
-- Select the “Downgrade” option on the billing card task on your Home page.
-- Go to **Settings > Workspaces > [Workspace name]**, then click the gear button next to the Workspace and select Delete.
-
-{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md
index 73b6c9106e4e..fb84e3484598 100644
--- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md
+++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md
@@ -65,7 +65,7 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s
### How This Works
1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account.
2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement).
-3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual credit card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories.
+3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories.
### Example
- We have card transactions for the day totaling $100, so we create the following journal entry upon sync:
diff --git a/docs/assets/images/send-money.svg b/docs/assets/images/send-money.svg
index e858f0d5c327..7abce818f09e 100644
--- a/docs/assets/images/send-money.svg
+++ b/docs/assets/images/send-money.svg
@@ -1,25 +1 @@
-
+
\ No newline at end of file
diff --git a/docs/assets/images/subscription-annual.svg b/docs/assets/images/subscription-annual.svg
index a4b99a43b16e..f74ce086b2c7 100644
--- a/docs/assets/images/subscription-annual.svg
+++ b/docs/assets/images/subscription-annual.svg
@@ -1,23 +1 @@
-
+
\ No newline at end of file
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index e676d0b9ddb7..73ddb84be0e3 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.24
+ 1.4.27
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.24.6
+ 1.4.27.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 381e28ad74c9..b697d413129d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.24
+ 1.4.27
CFBundleSignature
????
CFBundleVersion
- 1.4.24.6
+ 1.4.27.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 6a2ab9126cc5..0a7aab285d15 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -3,9 +3,9 @@
CFBundleShortVersionString
- 1.4.24
+ 1.4.27
CFBundleVersion
- 1.4.24.6
+ 1.4.27.1
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index acc8720dafce..f433c4f1e1e2 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1176,11 +1176,11 @@ PODS:
- React-Core
- react-native-key-command (1.0.6):
- React-Core
- - react-native-netinfo (11.1.0):
+ - react-native-netinfo (11.2.1):
- React-Core
- react-native-pager-view (6.2.2):
- React-Core
- - react-native-pdf (6.7.4):
+ - react-native-pdf (6.7.3):
- React-Core
- react-native-performance (5.1.0):
- React-Core
@@ -1909,9 +1909,9 @@ SPEC CHECKSUMS:
react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56
react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b
react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5
- react-native-netinfo: 3aa5637c18834966e0c932de8ae1ae56fea20a97
+ react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d
react-native-pager-view: 02a5c4962530f7efc10dd51ee9cdabeff5e6c631
- react-native-pdf: 79aa75e39a80c1d45ffe58aa500f3cf08f267a2e
+ react-native-pdf: b4ca3d37a9a86d9165287741c8b2ef4d8940c00e
react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886
react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1
react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4
@@ -1967,7 +1967,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7
- Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047
+ Yoga: 13c8ef87792450193e117976337b8527b49e8c03
PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2
diff --git a/package-lock.json b/package-lock.json
index a2d27747fc52..c3a48044479a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.24-6",
+ "version": "1.4.27-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.24-6",
+ "version": "1.4.27-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -27,7 +27,7 @@
"@react-native-camera-roll/camera-roll": "5.4.0",
"@react-native-clipboard/clipboard": "^1.12.1",
"@react-native-community/geolocation": "^3.0.6",
- "@react-native-community/netinfo": "11.1.0",
+ "@react-native-community/netinfo": "11.2.1",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
"@react-native-firebase/crashlytics": "^12.3.0",
@@ -51,7 +51,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
"expo": "^50.0.0-preview.7",
"expo-image": "1.10.1",
"fbjs": "^3.0.2",
@@ -96,7 +96,7 @@
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.118",
"react-native-pager-view": "6.2.2",
- "react-native-pdf": "^6.7.4",
+ "react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
"react-native-permissions": "^3.9.3",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5",
@@ -9608,9 +9608,9 @@
}
},
"node_modules/@react-native-community/netinfo": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz",
- "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==",
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz",
+ "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==",
"peerDependencies": {
"react-native": ">=0.59"
}
@@ -26153,10 +26153,9 @@
}
},
"node_modules/clipboard": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
- "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
- "license": "MIT",
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+ "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
@@ -28303,8 +28302,7 @@
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
- "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
- "license": "MIT"
+ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/delegates": {
"version": "1.0.0",
@@ -31594,37 +31592,26 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
- "integrity": "sha512-H7UrLgWIr8mCoPc1oxbeYW2RwLzUWI6jdjbV6cRnrlp8cDW3IyZISF+BQSPFDj7bMhNAbczQPtEOE1gld21Cvg==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
+ "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
- "clipboard": "2.0.4",
- "html-entities": "^2.3.3",
+ "clipboard": "2.0.11",
+ "html-entities": "^2.4.0",
"jquery": "3.6.0",
"localforage": "^1.10.0",
"lodash": "4.17.21",
- "prop-types": "15.7.2",
+ "prop-types": "15.8.1",
"react": "16.12.0",
"react-dom": "16.12.0",
- "semver": "^7.3.5",
+ "semver": "^7.5.2",
"simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
- "string.prototype.replaceall": "^1.0.6",
+ "string.prototype.replaceall": "^1.0.8",
"ua-parser-js": "^1.0.35",
"underscore": "1.13.6"
}
},
- "node_modules/expensify-common/node_modules/prop-types": {
- "version": "15.7.2",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
- "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.8.1"
- }
- },
"node_modules/expensify-common/node_modules/react": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
@@ -33539,7 +33526,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
- "license": "MIT",
"dependencies": {
"delegate": "^3.1.2"
}
@@ -34284,9 +34270,19 @@
}
},
"node_modules/html-entities": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz",
- "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg=="
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz",
+ "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ]
},
"node_modules/html-escaper": {
"version": "2.0.2",
@@ -47083,9 +47079,9 @@
}
},
"node_modules/react-native-pdf": {
- "version": "6.7.4",
- "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.4.tgz",
- "integrity": "sha512-sBeNcsrTRnLjmiU9Wx7Uk0K2kPSQtKIIG+FECdrEG16TOdtmQ3iqqEwt0dmy0pJegpg07uES5BXqiKsKkRUIFw==",
+ "version": "6.7.3",
+ "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz",
+ "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==",
"dependencies": {
"crypto-js": "4.2.0",
"deprecated-react-native-prop-types": "^2.3.0"
@@ -49759,8 +49755,7 @@
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
- "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
- "license": "MIT"
+ "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
},
"node_modules/select-hose": {
"version": "2.0.0",
@@ -51347,14 +51342,15 @@
}
},
"node_modules/string.prototype.replaceall": {
- "version": "1.0.6",
- "license": "MIT",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz",
+ "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "has-symbols": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
"is-regex": "^1.1.4"
},
"funding": {
@@ -62634,9 +62630,9 @@
"requires": {}
},
"@react-native-community/netinfo": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz",
- "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==",
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz",
+ "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==",
"requires": {}
},
"@react-native-firebase/analytics": {
@@ -74710,9 +74706,9 @@
"dev": true
},
"clipboard": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
- "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+ "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
@@ -78671,36 +78667,26 @@
}
},
"expensify-common": {
- "version": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
- "integrity": "sha512-H7UrLgWIr8mCoPc1oxbeYW2RwLzUWI6jdjbV6cRnrlp8cDW3IyZISF+BQSPFDj7bMhNAbczQPtEOE1gld21Cvg==",
- "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
+ "version": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
+ "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==",
+ "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
"requires": {
"classnames": "2.3.1",
- "clipboard": "2.0.4",
- "html-entities": "^2.3.3",
+ "clipboard": "2.0.11",
+ "html-entities": "^2.4.0",
"jquery": "3.6.0",
"localforage": "^1.10.0",
"lodash": "4.17.21",
- "prop-types": "15.7.2",
+ "prop-types": "15.8.1",
"react": "16.12.0",
"react-dom": "16.12.0",
- "semver": "^7.3.5",
+ "semver": "^7.5.2",
"simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
- "string.prototype.replaceall": "^1.0.6",
+ "string.prototype.replaceall": "^1.0.8",
"ua-parser-js": "^1.0.35",
"underscore": "1.13.6"
},
"dependencies": {
- "prop-types": {
- "version": "15.7.2",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
- "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
- "requires": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.8.1"
- }
- },
"react": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
@@ -80625,9 +80611,9 @@
}
},
"html-entities": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz",
- "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg=="
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz",
+ "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ=="
},
"html-escaper": {
"version": "2.0.2",
@@ -89732,9 +89718,9 @@
"requires": {}
},
"react-native-pdf": {
- "version": "6.7.4",
- "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.4.tgz",
- "integrity": "sha512-sBeNcsrTRnLjmiU9Wx7Uk0K2kPSQtKIIG+FECdrEG16TOdtmQ3iqqEwt0dmy0pJegpg07uES5BXqiKsKkRUIFw==",
+ "version": "6.7.3",
+ "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz",
+ "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==",
"requires": {
"crypto-js": "4.2.0",
"deprecated-react-native-prop-types": "^2.3.0"
@@ -92752,13 +92738,15 @@
}
},
"string.prototype.replaceall": {
- "version": "1.0.6",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz",
+ "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==",
"requires": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "has-symbols": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
"is-regex": "^1.1.4"
}
},
diff --git a/package.json b/package.json
index 6ba8790c98c9..da993f136512 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.24-6",
+ "version": "1.4.27-1",
"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.",
@@ -13,10 +13,10 @@
"postinstall": "scripts/postInstall.sh",
"clean": "npx react-native clean-project-auto",
"android": "scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev",
- "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --configuration=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"",
+ "ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"",
"pod-install": "cd ios && bundle exec pod install",
- "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --configuration=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"",
- "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --configuration=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"",
+ "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"",
+ "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"",
"start": "npx react-native start",
"web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server",
"web-proxy": "node web/proxy.js",
@@ -75,7 +75,7 @@
"@react-native-camera-roll/camera-roll": "5.4.0",
"@react-native-clipboard/clipboard": "^1.12.1",
"@react-native-community/geolocation": "^3.0.6",
- "@react-native-community/netinfo": "11.1.0",
+ "@react-native-community/netinfo": "11.2.1",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
"@react-native-firebase/crashlytics": "^12.3.0",
@@ -99,7 +99,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
"expo": "^50.0.0-preview.7",
"expo-image": "1.10.1",
"fbjs": "^3.0.2",
@@ -144,7 +144,7 @@
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.118",
"react-native-pager-view": "6.2.2",
- "react-native-pdf": "^6.7.4",
+ "react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
"react-native-permissions": "^3.9.3",
"react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5",
diff --git a/patches/react-native-blob-util+0.17.3.patch b/patches/react-native-blob-util+0.17.3.patch
new file mode 100644
index 000000000000..2ade175a7b30
--- /dev/null
+++ b/patches/react-native-blob-util+0.17.3.patch
@@ -0,0 +1,17 @@
+diff --git a/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java b/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java
+index 4b41402..4f07fc6 100644
+--- a/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java
++++ b/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java
+@@ -279,7 +279,11 @@ public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnabl
+ DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE);
+ downloadManagerId = dm.enqueue(req);
+ androidDownloadManagerTaskTable.put(taskId, Long.valueOf(downloadManagerId));
+- appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
++ if(Build.VERSION.SDK_INT >= 34 ){
++ appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED);
++ }else{
++ appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
++ }
+ future = scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
diff --git a/src/CONST.ts b/src/CONST.ts
index b1a6b6895de7..e3cce4b613af 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -479,7 +479,9 @@ const CONST = {
ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/',
// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
- EXPENSIFY_INBOX_URL: 'https://www.expensify.com/inbox',
+ OLDDOT_URLS: {
+ INBOX: 'inbox',
+ },
SIGN_IN_FORM_WIDTH: 300,
@@ -527,6 +529,7 @@ const CONST = {
TASKCOMPLETED: 'TASKCOMPLETED',
TASKEDITED: 'TASKEDITED',
TASKREOPENED: 'TASKREOPENED',
+ ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER',
POLICYCHANGELOG: {
ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE',
ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET',
@@ -600,6 +603,10 @@ const CONST = {
},
THREAD_DISABLED: ['CREATED'],
},
+ ACTIONABLE_MENTION_WHISPER_RESOLUTION: {
+ INVITE: 'invited',
+ NOTHING: 'nothing',
+ },
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
@@ -630,18 +637,13 @@ const CONST = {
ANNOUNCE: '#announce',
ADMINS: '#admins',
},
- STATE: {
- OPEN: 'OPEN',
- SUBMITTED: 'SUBMITTED',
- PROCESSING: 'PROCESSING',
- },
STATE_NUM: {
OPEN: 0,
- PROCESSING: 1,
- SUBMITTED: 2,
+ SUBMITTED: 1,
+ APPROVED: 2,
BILLING: 3,
},
- STATUS: {
+ STATUS_NUM: {
OPEN: 0,
SUBMITTED: 1,
CLOSED: 2,
@@ -1288,10 +1290,15 @@ const CONST = {
TRIP: 'trip',
MANUAL: 'manual',
},
+ AUTO_REPORTING_OFFSET: {
+ LAST_BUSINESS_DAY_OF_MONTH: 'lastBusinessDayOfMonth',
+ LAST_DAY_OF_MONTH: 'lastDayOfMonth',
+ },
ROOM_PREFIX: '#',
CUSTOM_UNIT_RATE_BASE_OFFSET: 100,
OWNER_EMAIL_FAKE: '_FAKE_',
OWNER_ACCOUNT_ID_FAKE: 0,
+ ID_FAKE: '_FAKE_',
},
CUSTOM_UNITS: {
@@ -1440,6 +1447,8 @@ const CONST = {
INVISIBLE_CHARACTERS_GROUPS: /[\p{C}\p{Z}]/gu,
OTHER_INVISIBLE_CHARACTERS: /[\u3164]/g,
+
+ REPORT_FIELD_TITLE: /{report:([a-zA-Z]+)}/g,
},
PRONOUNS: {
@@ -2725,7 +2734,7 @@ const CONST = {
EXPECTED_OUTPUT: 'FCFA 123,457',
},
- PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg'],
+ PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg', 'docs/index.html'],
// Test tool menu parameters
TEST_TOOL: {
@@ -3115,6 +3124,8 @@ const CONST = {
EMAIL: 'EMAIL',
REPORT: 'REPORT',
},
+
+ MINI_CONTEXT_MENU_MAX_ITEMS: 4,
} as const;
export default CONST;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 7538a16d1a2c..37003a09a0cd 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -286,10 +286,6 @@ const ROUTES = {
route: ':iouType/new/merchant/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const,
},
- MONEY_REQUEST_WAYPOINT: {
- route: ':iouType/new/waypoint/:waypointIndex',
- getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}` as const,
- },
MONEY_REQUEST_RECEIPT: {
route: ':iouType/new/receipt/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
@@ -298,10 +294,6 @@ const ROUTES = {
route: ':iouType/new/address/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const,
},
- MONEY_REQUEST_EDIT_WAYPOINT: {
- route: 'r/:threadReportID/edit/distance/:transactionID/waypoint/:waypointIndex',
- getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}` as const,
- },
MONEY_REQUEST_DISTANCE_TAB: {
route: ':iouType/new/:reportID?/distance',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const,
@@ -378,9 +370,9 @@ const ROUTES = {
getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_WAYPOINT: {
- route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') =>
- getUrlWithBackToParam(`create/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo),
+ route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') =>
+ getUrlWithBackToParam(`${action}/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo),
},
// This URL is used as a redirect to one of the create tabs below. This is so that we can message users with a link
// straight to those flows without needing to have optimistic transaction and report IDs.
diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.tsx
similarity index 72%
rename from src/components/AddressSearch/CurrentLocationButton.js
rename to src/components/AddressSearch/CurrentLocationButton.tsx
index 06541565f567..11bd0a64eba5 100644
--- a/src/components/AddressSearch/CurrentLocationButton.js
+++ b/src/components/AddressSearch/CurrentLocationButton.tsx
@@ -1,29 +1,16 @@
-import PropTypes from 'prop-types';
import React from 'react';
-import {Text} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import getButtonState from '@libs/getButtonState';
import colors from '@styles/theme/colors';
+import type {CurrentLocationButtonProps} from './types';
-const propTypes = {
- /** Callback that runs when location button is clicked */
- onPress: PropTypes.func,
-
- /** Boolean to indicate if the button is clickable */
- isDisabled: PropTypes.bool,
-};
-
-const defaultProps = {
- isDisabled: false,
- onPress: () => {},
-};
-
-function CurrentLocationButton({onPress, isDisabled}) {
+function CurrentLocationButton({onPress, isDisabled = false}: CurrentLocationButtonProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
@@ -32,7 +19,7 @@ function CurrentLocationButton({onPress, isDisabled}) {
onPress?.()}
accessibilityLabel={translate('location.useCurrent')}
disabled={isDisabled}
onMouseDown={(e) => e.preventDefault()}
@@ -48,7 +35,5 @@ function CurrentLocationButton({onPress, isDisabled}) {
}
CurrentLocationButton.displayName = 'CurrentLocationButton';
-CurrentLocationButton.propTypes = propTypes;
-CurrentLocationButton.defaultProps = defaultProps;
export default CurrentLocationButton;
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.tsx
similarity index 63%
rename from src/components/AddressSearch/index.js
rename to src/components/AddressSearch/index.tsx
index 357f5af8cb58..89e87eeebe54 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.tsx
@@ -1,184 +1,79 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native';
+import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
+import {ActivityIndicator, Keyboard, LogBox, ScrollView, View} from 'react-native';
+import type {LayoutChangeEvent} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
-import _ from 'underscore';
+import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import LocationErrorMessage from '@components/LocationErrorMessage';
-import networkPropTypes from '@components/networkPropTypes';
-import {withNetwork} from '@components/OnyxProvider';
+import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ApiUtils from '@libs/ApiUtils';
-import compose from '@libs/compose';
import getCurrentPosition from '@libs/getCurrentPosition';
+import type {GeolocationErrorCodeType} from '@libs/getCurrentPosition/getCurrentPosition.types';
import * as GooglePlacesUtils from '@libs/GooglePlacesUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import CurrentLocationButton from './CurrentLocationButton';
import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer';
+import type {AddressSearchProps, RenamedInputKeysProps} from './types';
// The error that's being thrown below will be ignored until we fork the
// react-native-google-places-autocomplete repo and replace the
// VirtualizedList component with a VirtualizedList-backed instead
LogBox.ignoreLogs(['VirtualizedLists should never be nested']);
-const propTypes = {
- /** The ID used to uniquely identify the input in a Form */
- inputID: PropTypes.string,
-
- /** Saves a draft of the input value when used in a form */
- shouldSaveDraft: PropTypes.bool,
-
- /** Callback that is called when the text input is blurred */
- onBlur: PropTypes.func,
-
- /** Error text to display */
- errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]),
-
- /** Hint text to display */
- hint: PropTypes.string,
-
- /** The label to display for the field */
- label: PropTypes.string.isRequired,
-
- /** The value to set the field to initially */
- value: PropTypes.string,
-
- /** The value to set the field to initially */
- defaultValue: PropTypes.string,
-
- /** A callback function when the value of this field has changed */
- onInputChange: PropTypes.func.isRequired,
-
- /** A callback function when an address has been auto-selected */
- onPress: PropTypes.func,
-
- /** Customize the TextInput container */
- // eslint-disable-next-line react/forbid-prop-types
- containerStyles: PropTypes.arrayOf(PropTypes.object),
-
- /** Should address search be limited to results in the USA */
- isLimitedToUSA: PropTypes.bool,
-
- /** Shows a current location button in suggestion list */
- canUseCurrentLocation: PropTypes.bool,
-
- /** A list of predefined places that can be shown when the user isn't searching for something */
- predefinedPlaces: PropTypes.arrayOf(
- PropTypes.shape({
- /** A description of the location (usually the address) */
- description: PropTypes.string,
-
- /** The name of the location */
- name: PropTypes.string,
-
- /** Data required by the google auto complete plugin to know where to put the markers on the map */
- geometry: PropTypes.shape({
- /** Data about the location */
- location: PropTypes.shape({
- /** Lattitude of the location */
- lat: PropTypes.number,
-
- /** Longitude of the location */
- lng: PropTypes.number,
- }),
- }),
- }),
- ),
-
- /** A map of inputID key names */
- renamedInputKeys: PropTypes.shape({
- street: PropTypes.string,
- street2: PropTypes.string,
- city: PropTypes.string,
- state: PropTypes.string,
- lat: PropTypes.string,
- lng: PropTypes.string,
- zipCode: PropTypes.string,
- }),
-
- /** Maximum number of characters allowed in search input */
- maxInputLength: PropTypes.number,
-
- /** The result types to return from the Google Places Autocomplete request */
- resultTypes: PropTypes.string,
-
- /** Information about the network */
- network: networkPropTypes.isRequired,
-
- /** Location bias for querying search results. */
- locationBias: PropTypes.string,
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- inputID: undefined,
- shouldSaveDraft: false,
- onBlur: () => {},
- onPress: () => {},
- errorText: '',
- hint: '',
- value: undefined,
- defaultValue: undefined,
- containerStyles: [],
- isLimitedToUSA: false,
- canUseCurrentLocation: false,
- renamedInputKeys: {
- street: 'addressStreet',
- street2: 'addressStreet2',
- city: 'addressCity',
- state: 'addressState',
- zipCode: 'addressZipCode',
- lat: 'addressLat',
- lng: 'addressLng',
- },
- maxInputLength: undefined,
- predefinedPlaces: [],
- resultTypes: 'address',
- locationBias: undefined,
-};
-
-function AddressSearch({
- canUseCurrentLocation,
- containerStyles,
- defaultValue,
- errorText,
- hint,
- innerRef,
- inputID,
- isLimitedToUSA,
- label,
- maxInputLength,
- network,
- onBlur,
- onInputChange,
- onPress,
- predefinedPlaces,
- preferredLocale,
- renamedInputKeys,
- resultTypes,
- shouldSaveDraft,
- translate,
- value,
- locationBias,
-}) {
+function AddressSearch(
+ {
+ canUseCurrentLocation = false,
+ containerStyles,
+ defaultValue,
+ errorText = '',
+ hint = '',
+ inputID,
+ isLimitedToUSA = false,
+ label,
+ maxInputLength,
+ onBlur,
+ onInputChange,
+ onPress,
+ predefinedPlaces = [],
+ preferredLocale,
+ renamedInputKeys = {
+ street: 'addressStreet',
+ street2: 'addressStreet2',
+ city: 'addressCity',
+ state: 'addressState',
+ zipCode: 'addressZipCode',
+ lat: 'addressLat',
+ lng: 'addressLng',
+ },
+ resultTypes = 'address',
+ shouldSaveDraft = false,
+ value,
+ locationBias,
+ }: AddressSearchProps,
+ ref: ForwardedRef,
+) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
const [displayListViewBorder, setDisplayListViewBorder] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [isFocused, setIsFocused] = useState(false);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [searchValue, setSearchValue] = useState(value || defaultValue || '');
- const [locationErrorCode, setLocationErrorCode] = useState(null);
+ const [locationErrorCode, setLocationErrorCode] = useState(null);
const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false);
const shouldTriggerGeolocationCallbacks = useRef(true);
- const containerRef = useRef();
+ const containerRef = useRef(null);
const query = useMemo(
() => ({
language: preferredLocale,
@@ -189,18 +84,18 @@ function AddressSearch({
[preferredLocale, resultTypes, isLimitedToUSA, locationBias],
);
const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
- const saveLocationDetails = (autocompleteData, details) => {
- const addressComponents = details.address_components;
+ const saveLocationDetails = (autocompleteData: GooglePlaceData, details: GooglePlaceDetail | null) => {
+ const addressComponents = details?.address_components;
if (!addressComponents) {
// When there are details, but no address_components, this indicates that some predefined options have been passed
// to this component which don't match the usual properties coming from auto-complete. In that case, only a limited
// amount of data massaging needs to happen for what the parent expects to get from this function.
- if (_.size(details)) {
- onPress({
- address: autocompleteData.description || lodashGet(details, 'description', ''),
- lat: lodashGet(details, 'geometry.location.lat', 0),
- lng: lodashGet(details, 'geometry.location.lng', 0),
- name: lodashGet(details, 'name'),
+ if (details) {
+ onPress?.({
+ address: autocompleteData.description ?? '',
+ lat: details.geometry.location.lat ?? 0,
+ lng: details.geometry.location.lng ?? 0,
+ name: details.name,
});
}
return;
@@ -219,14 +114,19 @@ function AddressSearch({
administrative_area_level_2: stateFallback,
country: countryPrimary,
} = GooglePlacesUtils.getAddressComponents(addressComponents, {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
street_number: 'long_name',
route: 'long_name',
subpremise: 'long_name',
locality: 'long_name',
sublocality: 'long_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
postal_town: 'long_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
postal_code: 'long_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_1: 'short_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_2: 'long_name',
country: 'short_name',
});
@@ -234,6 +134,7 @@ function AddressSearch({
// The state's iso code (short_name) is needed for the StatePicker component but we also
// need the state's full name (long_name) when we render the state in a TextInput.
const {administrative_area_level_1: longStateName} = GooglePlacesUtils.getAddressComponents(addressComponents, {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_1: 'long_name',
});
@@ -243,15 +144,16 @@ function AddressSearch({
country: countryFallbackLongName = '',
state: stateAutoCompleteFallback = '',
city: cityAutocompleteFallback = '',
- } = GooglePlacesUtils.getPlaceAutocompleteTerms(autocompleteData.terms);
+ } = GooglePlacesUtils.getPlaceAutocompleteTerms(autocompleteData?.terms ?? []);
- const countryFallback = _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryFallbackLongName);
+ const countryFallback = Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFallbackLongName);
- const country = countryPrimary || countryFallback;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const country = countryPrimary || countryFallback || '';
const values = {
street: `${streetNumber} ${streetName}`.trim(),
- name: lodashGet(details, 'name', ''),
+ name: details.name ?? '',
// Autocomplete returns any additional valid address fragments (e.g. Apt #) as subpremise.
street2: subpremise,
// Make sure country is updated first, since city and state will be reset if the country changes
@@ -264,9 +166,9 @@ function AddressSearch({
city: locality || postalTown || sublocality || cityAutocompleteFallback,
zipCode,
- lat: lodashGet(details, 'geometry.location.lat', 0),
- lng: lodashGet(details, 'geometry.location.lng', 0),
- address: autocompleteData.description || lodashGet(details, 'formatted_address', ''),
+ lat: details.geometry.location.lat ?? 0,
+ lng: details.geometry.location.lng ?? 0,
+ address: autocompleteData.description || details.formatted_address || '',
};
// If the address is not in the US, use the full length state name since we're displaying the address's
@@ -282,7 +184,7 @@ function AddressSearch({
}
// Set the state to be the same as the city in case the state is empty.
- if (_.isEmpty(values.state)) {
+ if (!values.state) {
values.state = values.city;
}
@@ -290,8 +192,8 @@ function AddressSearch({
// We are setting up a fallback to ensure "values.street" is populated with a relevant value
if (!values.street && details.adr_address) {
const streetAddressRegex = /([^<]*)<\/span>/;
- const adr_address = details.adr_address.match(streetAddressRegex);
- const streetAddressFallback = lodashGet(adr_address, [1], null);
+ const adrAddress = details.adr_address.match(streetAddressRegex);
+ const streetAddressFallback = adrAddress ? adrAddress?.[1] : null;
if (streetAddressFallback) {
values.street = streetAddressFallback;
}
@@ -299,28 +201,28 @@ function AddressSearch({
// Not all pages define the Address Line 2 field, so in that case we append any additional address details
// (e.g. Apt #) to Address Line 1
- if (subpremise && typeof renamedInputKeys.street2 === 'undefined') {
+ if (subpremise && typeof renamedInputKeys?.street2 === 'undefined') {
values.street += `, ${subpremise}`;
}
- const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country);
+ const isValidCountryCode = !!Object.keys(CONST.ALL_COUNTRIES).find((foundCountry) => foundCountry === country);
if (isValidCountryCode) {
values.country = country;
}
if (inputID) {
- _.each(values, (inputValue, key) => {
- const inputKey = lodashGet(renamedInputKeys, key, key);
+ Object.entries(values).forEach(([key, inputValue]) => {
+ const inputKey = renamedInputKeys?.[key as keyof RenamedInputKeysProps] ?? key;
if (!inputKey) {
return;
}
- onInputChange(inputValue, inputKey);
+ onInputChange?.(inputValue, inputKey);
});
} else {
- onInputChange(values);
+ onInputChange?.(values);
}
- onPress(values);
+ onPress?.(values);
};
/** Gets the user's current location and registers success/error callbacks */
@@ -351,7 +253,7 @@ function AddressSearch({
address: CONST.YOUR_LOCATION_TEXT,
name: CONST.YOUR_LOCATION_TEXT,
};
- onPress(location);
+ onPress?.(location);
},
(errorData) => {
if (!shouldTriggerGeolocationCallbacks.current) {
@@ -368,19 +270,22 @@ function AddressSearch({
);
};
- const renderHeaderComponent = () =>
- predefinedPlaces.length > 0 && (
- <>
- {/* This will show current location button in list if there are some recent destinations */}
- {shouldShowCurrentLocationButton && (
-
- )}
- {!value && {translate('common.recentDestinations')}}
- >
- );
+ const renderHeaderComponent = () => (
+ <>
+ {predefinedPlaces.length > 0 && (
+ <>
+ {/* This will show current location button in list if there are some recent destinations */}
+ {shouldShowCurrentLocationButton && (
+
+ )}
+ {!value && {translate('common.recentDestinations')}}
+ >
+ )}
+ >
+ );
// eslint-disable-next-line arrow-body-style
useEffect(() => {
@@ -392,10 +297,8 @@ function AddressSearch({
const listEmptyComponent = useCallback(
() =>
- network.isOffline || !isTyping ? null : (
- {translate('common.noResultsFound')}
- ),
- [network.isOffline, isTyping, styles, translate],
+ !!isOffline || !isTyping ? null : {translate('common.noResultsFound')},
+ [isOffline, isTyping, styles, translate],
);
const listLoader = useCallback(
@@ -464,27 +367,15 @@ function AddressSearch({
query={query}
requestUrl={{
useOnPlatform: 'all',
- url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
+ url: isOffline ? '' : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
}}
textInputProps={{
InputComp: TextInput,
- ref: (node) => {
- if (!innerRef) {
- return;
- }
-
- if (_.isFunction(innerRef)) {
- innerRef(node);
- return;
- }
-
- // eslint-disable-next-line no-param-reassign
- innerRef.current = node;
- },
+ ref,
label,
containerStyles,
errorText,
- hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint,
+ hint: displayListViewBorder || (predefinedPlaces?.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint,
value,
defaultValue,
inputID,
@@ -498,20 +389,19 @@ function AddressSearch({
setIsFocused(false);
setIsTyping(false);
}
- onBlur();
+ onBlur?.();
},
autoComplete: 'off',
- onInputChange: (text) => {
+ onInputChange: (text: string) => {
setSearchValue(text);
setIsTyping(true);
if (inputID) {
- onInputChange(text);
+ onInputChange?.(text);
} else {
onInputChange({street: text});
}
-
// If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
- if (_.isEmpty(text) && _.isEmpty(predefinedPlaces)) {
+ if (!text && !predefinedPlaces.length) {
setDisplayListViewBorder(false);
}
},
@@ -530,22 +420,21 @@ function AddressSearch({
isRowScrollable={false}
listHoverColor={theme.border}
listUnderlayColor={theme.buttonPressedBG}
- onLayout={(event) => {
+ onLayout={(event: LayoutChangeEvent) => {
// We use the height of the element to determine if we should hide the border of the listView dropdown
// to prevent a lingering border when there are no address suggestions.
setDisplayListViewBorder(event.nativeEvent.layout.height > variables.googleEmptyListViewHeight);
}}
inbetweenCompo={
// We want to show the current location button even if there are no recent destinations
- predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
+ predefinedPlaces?.length === 0 &&
+ shouldShowCurrentLocationButton && (
- ) : (
- <>>
)
}
placeholder=""
@@ -561,18 +450,6 @@ function AddressSearch({
);
}
-AddressSearch.propTypes = propTypes;
-AddressSearch.defaultProps = defaultProps;
-AddressSearch.displayName = 'AddressSearch';
-
-const AddressSearchWithRef = React.forwardRef((props, ref) => (
-
-));
-
-AddressSearchWithRef.displayName = 'AddressSearchWithRef';
+AddressSearch.displayName = 'AddressSearchWithRef';
-export default compose(withNetwork(), withLocalize)(AddressSearchWithRef);
+export default forwardRef(AddressSearch);
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.js
deleted file mode 100644
index 18bfc10a8dcb..000000000000
--- a/src/components/AddressSearch/isCurrentTargetInsideContainer.js
+++ /dev/null
@@ -1,8 +0,0 @@
-function isCurrentTargetInsideContainer(event, containerRef) {
- // The related target check is required here
- // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false
- // it will make the auto complete component re-render before onPress is called making selecting an option not working.
- return containerRef.current && event.target && containerRef.current.contains(event.relatedTarget);
-}
-
-export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js
deleted file mode 100644
index dbf0004b08d9..000000000000
--- a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js
+++ /dev/null
@@ -1,6 +0,0 @@
-function isCurrentTargetInsideContainer() {
- // The related target check is not required here because in native there is no race condition rendering like on the web
- return false;
-}
-
-export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts
new file mode 100644
index 000000000000..b53b9e3ddec0
--- /dev/null
+++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts
@@ -0,0 +1,6 @@
+import type {IsCurrentTargetInsideContainerType} from './types';
+
+// The related target check is not required here because in native there is no race condition rendering like on the web
+const isCurrentTargetInsideContainer: IsCurrentTargetInsideContainerType = () => false;
+
+export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.ts b/src/components/AddressSearch/isCurrentTargetInsideContainer.ts
new file mode 100644
index 000000000000..a50eb747b400
--- /dev/null
+++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.ts
@@ -0,0 +1,14 @@
+import type {IsCurrentTargetInsideContainerType} from './types';
+
+const isCurrentTargetInsideContainer: IsCurrentTargetInsideContainerType = (event, containerRef) => {
+ // The related target check is required here
+ // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false
+ // it will make the auto complete component re-render before onPress is called making selecting an option not working.
+ if (!containerRef.current || !event.target || !('relatedTarget' in event) || !('contains' in containerRef.current)) {
+ return false;
+ }
+
+ return !!containerRef.current.contains(event.relatedTarget as Node);
+};
+
+export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts
new file mode 100644
index 000000000000..8016f1b2ea39
--- /dev/null
+++ b/src/components/AddressSearch/types.ts
@@ -0,0 +1,96 @@
+import type {RefObject} from 'react';
+import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native';
+import type {Place} from 'react-native-google-places-autocomplete';
+import type Locale from '@src/types/onyx/Locale';
+
+type CurrentLocationButtonProps = {
+ /** Callback that is called when the button is clicked */
+ onPress?: () => void;
+
+ /** Boolean to indicate if the button is clickable */
+ isDisabled?: boolean;
+};
+
+type RenamedInputKeysProps = {
+ street: string;
+ street2: string;
+ city: string;
+ state: string;
+ lat: string;
+ lng: string;
+ zipCode: string;
+};
+
+type OnPressProps = {
+ address: string;
+ lat: number;
+ lng: number;
+ name: string;
+};
+
+type StreetValue = {
+ street: string;
+};
+
+type AddressSearchProps = {
+ /** The ID used to uniquely identify the input in a Form */
+ inputID?: string;
+
+ /** Saves a draft of the input value when used in a form */
+ shouldSaveDraft?: boolean;
+
+ /** Callback that is called when the text input is blurred */
+ onBlur?: () => void;
+
+ /** Error text to display */
+ errorText?: string;
+
+ /** Hint text to display */
+ hint?: string;
+
+ /** The label to display for the field */
+ label: string;
+
+ /** The value to set the field to initially */
+ value?: string;
+
+ /** The value to set the field to initially */
+ defaultValue?: string;
+
+ /** A callback function when the value of this field has changed */
+ onInputChange: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void;
+
+ /** A callback function when an address has been auto-selected */
+ onPress?: (props: OnPressProps) => void;
+
+ /** Customize the TextInput container */
+ containerStyles?: StyleProp;
+
+ /** Should address search be limited to results in the USA */
+ isLimitedToUSA?: boolean;
+
+ /** Shows a current location button in suggestion list */
+ canUseCurrentLocation?: boolean;
+
+ /** A list of predefined places that can be shown when the user isn't searching for something */
+ predefinedPlaces?: Place[];
+
+ /** A map of inputID key names */
+ renamedInputKeys: RenamedInputKeysProps;
+
+ /** Maximum number of characters allowed in search input */
+ maxInputLength?: number;
+
+ /** The result types to return from the Google Places Autocomplete request */
+ resultTypes?: string;
+
+ /** Location bias for querying search results. */
+ locationBias?: string;
+
+ /** The user's preferred locale e.g. 'en', 'es-ES' */
+ preferredLocale?: Locale;
+};
+
+type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean;
+
+export type {CurrentLocationButtonProps, AddressSearchProps, RenamedInputKeysProps, IsCurrentTargetInsideContainerType};
diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
similarity index 52%
rename from src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
rename to src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
index 6161ba140726..df8a0a30b129 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
@@ -1,6 +1,6 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import AttachmentView from '@components/Attachments/AttachmentView';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext';
@@ -10,59 +10,52 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as Download from '@userActions/Download';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import {defaultProps as anchorForAttachmentsOnlyDefaultProps, propTypes as anchorForAttachmentsOnlyPropTypes} from './anchorForAttachmentsOnlyPropTypes';
-
-const propTypes = {
- /** Press in handler for the link */
- onPressIn: PropTypes.func,
-
- /** Press out handler for the link */
- onPressOut: PropTypes.func,
+import type {Download as OnyxDownload} from '@src/types/onyx';
+import type AnchorForAttachmentsOnlyProps from './types';
+type BaseAnchorForAttachmentsOnlyOnyxProps = {
/** If a file download is happening */
- download: PropTypes.shape({
- isDownloading: PropTypes.bool.isRequired,
- }),
-
- ...anchorForAttachmentsOnlyPropTypes,
+ download: OnyxEntry;
};
-const defaultProps = {
- onPressIn: undefined,
- onPressOut: undefined,
- download: {isDownloading: false},
- ...anchorForAttachmentsOnlyDefaultProps,
-};
+type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps &
+ BaseAnchorForAttachmentsOnlyOnyxProps & {
+ /** Press in handler for the link */
+ onPressIn?: () => void;
+
+ /** Press out handler for the link */
+ onPressOut?: () => void;
+ };
-function BaseAnchorForAttachmentsOnly(props) {
- const sourceURL = props.source;
- const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL);
- const sourceID = (sourceURL.match(CONST.REGEX.ATTACHMENT_ID) || [])[1];
- const fileName = props.displayName;
+function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', download, onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) {
+ const sourceURLWithAuth = addEncryptedAuthTokenToURL(source);
+ const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1];
- const isDownloading = props.download && props.download.isDownloading;
+ const isDownloading = download?.isDownloading ?? false;
return (
{({anchor, report, action, checkIfContextMenuActive}) => (
{
if (isDownloading) {
return;
}
Download.setDownload(sourceID, true);
- fileDownload(sourceURLWithAuth, fileName).then(() => Download.setDownload(sourceID, false));
+ fileDownload(sourceURLWithAuth, displayName).then(() => Download.setDownload(sourceID, false));
}}
- onPressIn={props.onPressIn}
- onPressOut={props.onPressOut}
+ onPressIn={onPressIn}
+ onPressOut={onPressOut}
+ // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript.
onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
- accessibilityLabel={fileName}
+ accessibilityLabel={displayName}
role={CONST.ROLE.BUTTON}
>
@@ -73,13 +66,11 @@ function BaseAnchorForAttachmentsOnly(props) {
}
BaseAnchorForAttachmentsOnly.displayName = 'BaseAnchorForAttachmentsOnly';
-BaseAnchorForAttachmentsOnly.propTypes = propTypes;
-BaseAnchorForAttachmentsOnly.defaultProps = defaultProps;
-export default withOnyx({
+export default withOnyx({
download: {
key: ({source}) => {
- const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) || [])[1];
+ const sourceID = (source?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1];
return `${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`;
},
},
diff --git a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js b/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js
deleted file mode 100644
index 9452e615d31c..000000000000
--- a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types';
-import stylePropTypes from '@styles/stylePropTypes';
-
-const propTypes = {
- /** The URL of the attachment */
- source: PropTypes.string,
-
- /** Filename for attachments. */
- displayName: PropTypes.string,
-
- /** Any additional styles to apply */
- style: stylePropTypes,
-};
-
-const defaultProps = {
- source: '',
- style: {},
- displayName: '',
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/AnchorForAttachmentsOnly/index.native.js b/src/components/AnchorForAttachmentsOnly/index.native.tsx
similarity index 62%
rename from src/components/AnchorForAttachmentsOnly/index.native.js
rename to src/components/AnchorForAttachmentsOnly/index.native.tsx
index 3277d51ec058..2e0e94bc0b88 100644
--- a/src/components/AnchorForAttachmentsOnly/index.native.js
+++ b/src/components/AnchorForAttachmentsOnly/index.native.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes';
import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly';
+import type AnchorForAttachmentsOnlyProps from './types';
-function AnchorForAttachmentsOnly(props) {
+function AnchorForAttachmentsOnly(props: AnchorForAttachmentsOnlyProps) {
const styles = useThemeStyles();
return (
;
+};
+
+export default AnchorForAttachmentsOnlyProps;
diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
index bb3792f59d9f..99a0ee3bf683 100644
--- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
+++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
@@ -1,5 +1,6 @@
import Str from 'expensify-common/lib/str';
import React, {useEffect, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
import {StyleSheet} from 'react-native';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx
index ad79e316baf3..04e8a5f8d55b 100644
--- a/src/components/AnonymousReportFooter.tsx
+++ b/src/components/AnonymousReportFooter.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx/lib/types';
import useLocalize from '@hooks/useLocalize';
@@ -9,6 +9,7 @@ import type {PersonalDetails, Report} from '@src/types/onyx';
import AvatarWithDisplayName from './AvatarWithDisplayName';
import Button from './Button';
import ExpensifyWordmark from './ExpensifyWordmark';
+import Text from './Text';
type AnonymousReportFooterProps = {
/** The report currently being looked at */
diff --git a/src/components/AutoEmailLink.js b/src/components/AutoEmailLink.tsx
similarity index 68%
rename from src/components/AutoEmailLink.js
rename to src/components/AutoEmailLink.tsx
index af581525ab69..e1a9bdd2794b 100644
--- a/src/components/AutoEmailLink.js
+++ b/src/components/AutoEmailLink.tsx
@@ -1,18 +1,13 @@
import {CONST} from 'expensify-common/lib/CONST';
-import PropTypes from 'prop-types';
import React from 'react';
-import _ from 'underscore';
+import type {StyleProp, TextStyle} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import Text from './Text';
import TextLink from './TextLink';
-const propTypes = {
- text: PropTypes.string.isRequired,
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-};
-
-const defaultProps = {
- style: [],
+type AutoEmailLinkProps = {
+ text: string;
+ style?: StyleProp;
};
/*
@@ -21,14 +16,15 @@ const defaultProps = {
* - Else just render it inside `Text` component
*/
-function AutoEmailLink(props) {
+function AutoEmailLink({text, style}: AutoEmailLinkProps) {
const styles = useThemeStyles();
return (
-
- {_.map(props.text.split(CONST.REG_EXP.EXTRACT_EMAIL), (str, index) => {
+
+ {text.split(CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => {
if (CONST.REG_EXP.EMAIL.test(str)) {
return (
{str}
@@ -52,6 +49,5 @@ function AutoEmailLink(props) {
}
AutoEmailLink.displayName = 'AutoEmailLink';
-AutoEmailLink.propTypes = propTypes;
-AutoEmailLink.defaultProps = defaultProps;
+
export default AutoEmailLink;
diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.js
index 92cbe3a4da04..f69fe7eb5ecb 100644
--- a/src/components/AvatarCropModal/ImageCropView.js
+++ b/src/components/AvatarCropModal/ImageCropView.js
@@ -6,7 +6,6 @@ import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import useStyleUtils from '@hooks/useStyleUtils';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
import gestureHandlerPropTypes from './gestureHandlerPropTypes';
@@ -51,7 +50,6 @@ const defaultProps = {
};
function ImageCropView(props) {
- const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize);
@@ -90,7 +88,8 @@ function ImageCropView(props) {
diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx
index 5993e60861f5..807029addf5e 100644
--- a/src/components/BlockingViews/FullPageNotFoundView.tsx
+++ b/src/components/BlockingViews/FullPageNotFoundView.tsx
@@ -33,10 +33,10 @@ type FullPageNotFoundViewProps = {
linkKey?: TranslationPaths;
/** Method to trigger when pressing the back button of the header */
- onBackButtonPress: () => void;
+ onBackButtonPress?: () => void;
/** Function to call when pressing the navigation link */
- onLinkPress: () => void;
+ onLinkPress?: () => void;
};
// eslint-disable-next-line rulesdir/no-negated-variables
diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.tsx
similarity index 56%
rename from src/components/ButtonWithDropdownMenu.js
rename to src/components/ButtonWithDropdownMenu.tsx
index 4d3ec8796a31..466c68229a32 100644
--- a/src/components/ButtonWithDropdownMenu.js
+++ b/src/components/ButtonWithDropdownMenu.tsx
@@ -1,89 +1,89 @@
-import PropTypes from 'prop-types';
+import type {RefObject} from 'react';
import React, {useEffect, useRef, useState} from 'react';
+import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import _ from 'underscore';
+import type {ValueOf} from 'type-fest';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import type {AnchorPosition} from '@styles/index';
import CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
-import sourcePropTypes from './Image/sourcePropTypes';
+import type {AnchorAlignment} from './Popover/types';
import PopoverMenu from './PopoverMenu';
-const propTypes = {
+type DropdownOption = {
+ value: string;
+ text: string;
+ icon: IconAsset;
+ iconWidth?: number;
+ iconHeight?: number;
+ iconDescription?: string;
+};
+
+type ButtonWithDropdownMenuProps = {
/** Text to display for the menu header */
- menuHeaderText: PropTypes.string,
+ menuHeaderText?: string;
/** Callback to execute when the main button is pressed */
- onPress: PropTypes.func.isRequired,
+ onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: string) => void;
/** Call the onPress function on main button when Enter key is pressed */
- pressOnEnter: PropTypes.bool,
+ pressOnEnter?: boolean;
/** Whether we should show a loading state for the main button */
- isLoading: PropTypes.bool,
+ isLoading?: boolean;
/** The size of button size */
- buttonSize: PropTypes.oneOf(_.values(CONST.DROPDOWN_BUTTON_SIZE)),
+ buttonSize: ValueOf;
/** Should the confirmation button be disabled? */
- isDisabled: PropTypes.bool,
+ isDisabled?: boolean;
/** Additional styles to add to the component */
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+ style?: StyleProp;
/** Menu options to display */
/** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */
- options: PropTypes.arrayOf(
- PropTypes.shape({
- value: PropTypes.string.isRequired,
- text: PropTypes.string.isRequired,
- icon: sourcePropTypes,
- iconWidth: PropTypes.number,
- iconHeight: PropTypes.number,
- iconDescription: PropTypes.string,
- }),
- ).isRequired,
+ options: DropdownOption[];
/** The anchor alignment of the popover menu */
- anchorAlignment: PropTypes.shape({
- horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
- vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
- }),
+ anchorAlignment?: AnchorAlignment;
/* ref for the button */
- buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+ buttonRef: RefObject;
};
-const defaultProps = {
- isLoading: false,
- isDisabled: false,
- pressOnEnter: false,
- menuHeaderText: '',
- style: [],
- buttonSize: CONST.DROPDOWN_BUTTON_SIZE.MEDIUM,
- anchorAlignment: {
+function ButtonWithDropdownMenu({
+ isLoading = false,
+ isDisabled = false,
+ pressOnEnter = false,
+ menuHeaderText = '',
+ style,
+ buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM,
+ anchorAlignment = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP
},
- buttonRef: () => {},
-};
-
-function ButtonWithDropdownMenu(props) {
+ buttonRef,
+ onPress,
+ options,
+}: ButtonWithDropdownMenuProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedItemIndex, setSelectedItemIndex] = useState(0);
const [isMenuVisible, setIsMenuVisible] = useState(false);
- const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null);
+ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null);
const {windowWidth, windowHeight} = useWindowDimensions();
- const caretButton = useRef(null);
- const selectedItem = props.options[selectedItemIndex] || _.first(props.options);
- const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(props.buttonSize);
- const isButtonSizeLarge = props.buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE;
+ const caretButton = useRef(null);
+ const selectedItem = options[selectedItemIndex] || options[0];
+ const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize);
+ const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE;
useEffect(() => {
if (!caretButton.current) {
@@ -92,29 +92,31 @@ function ButtonWithDropdownMenu(props) {
if (!isMenuVisible) {
return;
}
- caretButton.current.measureInWindow((x, y, w, h) => {
- setPopoverAnchorPosition({
- horizontal: x + w,
- vertical:
- props.anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP
- ? y + h + CONST.MODAL.POPOVER_MENU_PADDING // if vertical anchorAlignment is TOP, menu will open below the button and we need to add the height of button and padding
- : y - CONST.MODAL.POPOVER_MENU_PADDING, // if it is BOTTOM, menu will open above the button so NO need to add height but DO subtract padding
+ if ('measureInWindow' in caretButton.current) {
+ caretButton.current.measureInWindow((x, y, w, h) => {
+ setPopoverAnchorPosition({
+ horizontal: x + w,
+ vertical:
+ anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP
+ ? y + h + CONST.MODAL.POPOVER_MENU_PADDING // if vertical anchorAlignment is TOP, menu will open below the button and we need to add the height of button and padding
+ : y - CONST.MODAL.POPOVER_MENU_PADDING, // if it is BOTTOM, menu will open above the button so NO need to add height but DO subtract padding
+ });
});
- });
- }, [windowWidth, windowHeight, isMenuVisible, props.anchorAlignment.vertical]);
+ }
+ }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]);
return (
- {props.options.length > 1 ? (
-
+ {options.length > 1 ? (
+
);
}
-export default MoneyRequestReferralProgramCTA;
+export default ReferralProgramCTA;
diff --git a/src/components/ReportActionItem/ActionableItemButtons.tsx b/src/components/ReportActionItem/ActionableItemButtons.tsx
new file mode 100644
index 000000000000..d1f169d2f409
--- /dev/null
+++ b/src/components/ReportActionItem/ActionableItemButtons.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {TranslationPaths} from '@src/languages/types';
+
+type ActionableItem = {
+ isPrimary?: boolean;
+ key: string;
+ onPress: () => void;
+ text: TranslationPaths;
+};
+
+type ActionableItemButtonsProps = {
+ items: ActionableItem[];
+};
+
+function ActionableItemButtons(props: ActionableItemButtonsProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ return (
+
+ {props.items?.map((item) => (
+
+ ))}
+
+ );
+}
+
+ActionableItemButtons.displayName = 'ActionableItemButtton';
+
+export default ActionableItemButtons;
+export type {ActionableItem};
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 16ea27b17f42..4fcca3e518a5 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -1,11 +1,14 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
import SpacerView from '@components/SpacerView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -14,22 +17,26 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import variables from '@styles/variables';
-import type {Report} from '@src/types/onyx';
+import type {PolicyReportField, Report} from '@src/types/onyx';
type MoneyReportViewProps = {
/** The report currently being looked at */
report: Report;
+ /** Policy report fields */
+ policyReportFields: PolicyReportField[];
+
/** Whether we should display the horizontal rule below the component */
shouldShowHorizontalRule: boolean;
};
-function MoneyReportView({report, shouldShowHorizontalRule}: MoneyReportViewProps) {
+function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
+ const {canUseReportFields} = usePermissions();
const isSettled = ReportUtils.isSettled(report.reportID);
const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report);
@@ -46,10 +53,41 @@ function MoneyReportView({report, shouldShowHorizontalRule}: MoneyReportViewProp
StyleUtils.getColorStyle(theme.textSupporting),
];
+ const sortedPolicyReportFields = useMemo(
+ () => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight),
+ [policyReportFields],
+ );
+
return (
+ {canUseReportFields &&
+ sortedPolicyReportFields.map((reportField) => {
+ const title = ReportUtils.getReportFieldTitle(report, reportField);
+ return (
+
+ {}}
+ shouldShowRightIcon
+ disabled={false}
+ wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
+ shouldGreyOutWhenDisabled={false}
+ numberOfLinesTitle={0}
+ interactive
+ shouldStackHorizontally={false}
+ onSecondaryInteraction={() => {}}
+ hoverAndPressStyle={false}
+ titleWithTooltips={[]}
+ />
+
+ );
+ })}
{
if (isDistanceRequest) {
- return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : translate('common.tbd');
+ return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency) : translate('common.tbd');
}
if (isScanning) {
@@ -328,7 +331,8 @@ function MoneyRequestPreview(props) {
{!isCurrentUserManager && props.shouldShowPendingConversionMessage && (
{translate('iou.pendingConversionMessage')}
)}
- {(shouldShowDescription || shouldShowMerchant) && {merchantOrDescription}}
+ {shouldShowDescription && }
+ {shouldShowMerchant && {merchantOrDescription}}
{props.isBillSplit && !_.isEmpty(participantAccountIDs) && requestAmount > 0 && (
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 036b64af1e4b..86affbcac114 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -313,8 +313,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEditMerchant}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))}
- brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
- error={hasErrors && isEmptyMerchant ? translate('common.error.enterMerchant') : ''}
+ brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant && isPolicyExpenseChat) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ error={hasErrors && isPolicyExpenseChat && isEmptyMerchant ? translate('common.error.enterMerchant') : ''}
/>
{canUseViolations && }
@@ -432,7 +432,7 @@ export default compose(
return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
- transactionViolation: {
+ transactionViolations: {
key: ({report}) => {
const parentReportAction = ReportActionsUtils.getParentReportAction(report);
const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0);
diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index abc7e3954200..8483b7a481f2 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -13,6 +13,7 @@ import refPropTypes from '@components/refPropTypes';
import SettlementButton from '@components/SettlementButton';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
+import transactionPropTypes from '@components/transactionPropTypes';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
@@ -22,7 +23,6 @@ import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Navigation from '@libs/Navigation/Navigation';
-import onyxSubscribe from '@libs/onyxSubscribe';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -105,6 +105,9 @@ const propTypes = {
/** Whether a message is a whisper */
isWhisper: PropTypes.bool,
+ /** All the transactions, used to update ReportPreview label and status */
+ transactions: PropTypes.objectOf(transactionPropTypes),
+
...withLocalizePropTypes,
};
@@ -121,6 +124,7 @@ const defaultProps = {
policy: {
isHarvestingEnabled: false,
},
+ transactions: {},
};
function ReportPreview(props) {
@@ -128,10 +132,17 @@ function ReportPreview(props) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const [hasMissingSmartscanFields, sethasMissingSmartscanFields] = useState(false);
- const [areAllRequestsBeingSmartScanned, setAreAllRequestsBeingSmartScanned] = useState(false);
- const [hasOnlyDistanceRequests, setHasOnlyDistanceRequests] = useState(false);
- const [hasNonReimbursableTransactions, setHasNonReimbursableTransactions] = useState(false);
+ const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo(
+ () => ({
+ hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(props.iouReportID),
+ areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action),
+ hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID),
+ hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(props.iouReportID),
+ }),
+ // When transactions get updated these status may have changed, so that is a case where we also want to run this.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [props.transactions, props.iouReportID, props.action],
+ );
const managerID = props.iouReport.managerID || 0;
const isCurrentUserManager = managerID === lodashGet(props.session, 'accountID');
@@ -155,6 +166,9 @@ function ReportPreview(props) {
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction));
let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null;
+ if (TransactionUtils.isPartialMerchant(formattedMerchant)) {
+ formattedMerchant = null;
+ }
const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null));
if (hasPendingWaypoints) {
formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd'));
@@ -162,7 +176,7 @@ function ReportPreview(props) {
const previewSubtitle =
formattedMerchant ||
props.translate('iou.requestCount', {
- count: numberOfRequests,
+ count: numberOfRequests - numberOfScanningReceipts,
scanningReceipts: numberOfScanningReceipts,
});
@@ -218,28 +232,6 @@ function ReportPreview(props) {
const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport);
- useEffect(() => {
- const unsubscribeOnyxTransaction = onyxSubscribe({
- key: ONYXKEYS.COLLECTION.TRANSACTION,
- waitForCollectionCallback: true,
- callback: (allTransactions) => {
- if (_.isEmpty(allTransactions)) {
- return;
- }
-
- sethasMissingSmartscanFields(ReportUtils.hasMissingSmartscanFields(props.iouReportID));
- setAreAllRequestsBeingSmartScanned(ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action));
- setHasOnlyDistanceRequests(ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID));
- setHasNonReimbursableTransactions(ReportUtils.hasNonReimbursableTransactions(props.iouReportID));
- },
- });
-
- return () => {
- unsubscribeOnyxTransaction();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(props.chatReport);
const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(props.policy, 'role') === CONST.POLICY.ROLE.ADMIN;
const isPayer = isPaidGroupPolicy
@@ -305,7 +297,7 @@ function ReportPreview(props) {
)}
- {!isScanning && (numberOfRequests > 1 || hasReceipts) && (
+ {!isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant)) && (
{previewSubtitle || moneyRequestComment}
@@ -370,5 +362,8 @@ export default compose(
session: {
key: ONYXKEYS.SESSION,
},
+ transactions: {
+ key: ONYXKEYS.COLLECTION.TRANSACTION,
+ },
}),
)(ReportPreview);
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index fbc58a381318..cbd166d79d3a 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -1,5 +1,7 @@
import Str from 'expensify-common/lib/str';
import React from 'react';
+// eslint-disable-next-line no-restricted-imports
+import type {Text as RNText} from 'react-native';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
@@ -32,7 +34,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
type PolicyRole = {
/** The role of current user */
- role: string;
+ role: Task.PolicyValue | undefined;
};
type TaskPreviewOnyxProps = {
@@ -63,7 +65,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps &
chatReportID: string;
/** Popover context menu anchor, used for showing context menu */
- contextMenuAnchor: Element;
+ contextMenuAnchor: RNText | null;
/** Callback for updating context menu active state, used for showing context menu */
checkIfContextMenuActive: () => void;
@@ -84,14 +86,15 @@ function TaskPreview({
const StyleUtils = useStyleUtils();
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const {translate} = useLocalize();
+
// The reportAction might not contain details regarding the taskReport
// Only the direct parent reportAction will contain details about the taskReport
// Other linked reportActions will only contain the taskReportID and we will grab the details from there
const isTaskCompleted = !isEmptyObject(taskReport)
- ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && taskReport.statusNum === CONST.REPORT.STATUS.APPROVED
- : action?.childStateNum === CONST.REPORT.STATE_NUM.SUBMITTED && action?.childStatusNum === CONST.REPORT.STATUS.APPROVED;
+ ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED
+ : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? ''));
- const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport ?? {}) ?? action?.childManagerAccountID ?? '';
+ const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? '';
const assigneeLogin = personalDetails[taskAssigneeAccountID]?.login ?? '';
const assigneeDisplayName = personalDetails[taskAssigneeAccountID]?.displayName ?? '';
const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin);
@@ -111,7 +114,7 @@ function TaskPreview({
onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID))}
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
- onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action ?? {}, checkIfContextMenuActive)}
+ onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)}
style={[styles.flexRow, styles.justifyContentBetween]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('task.task')}
@@ -121,12 +124,12 @@ function TaskPreview({
style={[styles.mr2]}
containerStyle={[styles.taskCheckbox]}
isChecked={isTaskCompleted}
- disabled={!Task.canModifyTask(taskReport ?? {}, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role ?? '')}
+ disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role)}
onPress={Session.checkIfActionIsAllowed(() => {
if (isTaskCompleted) {
- Task.reopenTask(taskReport ?? {});
+ Task.reopenTask(taskReport);
} else {
- Task.completeTask(taskReport ?? {});
+ Task.completeTask(taskReport);
}
})}
accessibilityLabel={translate('task.task')}
@@ -151,7 +154,7 @@ export default withCurrentUserPersonalDetails(
},
rootParentReportpolicy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '0'}`,
- selector: (policy: Policy | null) => ({role: policy?.role ?? ''}),
+ selector: (policy: Policy | null) => ({role: policy?.role}),
},
})(TaskPreview),
);
diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js
index cfd39ab0ebb8..6a067ea0fe3d 100644
--- a/src/components/SelectionList/BaseListItem.js
+++ b/src/components/SelectionList/BaseListItem.js
@@ -105,11 +105,11 @@ function BaseListItem({
textStyles={[
styles.optionDisplayName,
isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText,
- isUserItem || item.isSelected || item.alternateText ? styles.sidebarLinkTextBold : null,
+ styles.sidebarLinkTextBold,
styles.pre,
item.alternateText ? styles.mb1 : null,
]}
- alternateTextStyles={[styles.optionAlternateText, styles.textLabelSupporting, isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, styles.pre]}
+ alternateTextStyles={[styles.textLabelSupporting, styles.lh16, styles.pre]}
isDisabled={isDisabled}
onSelectRow={onSelectRow}
showTooltip={showTooltip}
diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js
index c2e3fd2887cd..960618808fd9 100644
--- a/src/components/SelectionList/BaseSelectionList.js
+++ b/src/components/SelectionList/BaseSelectionList.js
@@ -63,6 +63,7 @@ function BaseSelectionList({
disableKeyboardShortcuts = false,
children,
shouldStopPropagation = false,
+ shouldShowTooltips = true,
shouldUseDynamicMaxToRenderPerBatch = false,
rightHandSideComponent,
}) {
@@ -304,7 +305,7 @@ function BaseSelectionList({
const isDisabled = section.isDisabled || item.isDisabled;
const isItemFocused = !isDisabled && focusedIndex === normalizedIndex;
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
- const showTooltip = normalizedIndex < 10;
+ const showTooltip = shouldShowTooltips && normalizedIndex < 10;
return (
{Boolean(item.icons) && (
@@ -24,7 +26,7 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip}) {
text={item.text}
>
{item.text}
@@ -36,7 +38,7 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip}) {
text={item.alternateText}
>
{item.alternateText}
diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js
index 2e380040bd91..f5178112a4c3 100644
--- a/src/components/SelectionList/selectionListPropTypes.js
+++ b/src/components/SelectionList/selectionListPropTypes.js
@@ -190,6 +190,9 @@ const propTypes = {
/** Custom content to display in the footer */
footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+ /** Whether to show the toolip text */
+ shouldShowTooltips: PropTypes.bool,
+
/** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
shouldUseDynamicMaxToRenderPerBatch: PropTypes.bool,
diff --git a/src/components/ShowContextMenuContext.js b/src/components/ShowContextMenuContext.js
deleted file mode 100644
index 04ccd5002b60..000000000000
--- a/src/components/ShowContextMenuContext.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
-import CONST from '@src/CONST';
-
-const ShowContextMenuContext = React.createContext({
- anchor: null,
- report: null,
- action: undefined,
- checkIfContextMenuActive: () => {},
-});
-
-ShowContextMenuContext.displayName = 'ShowContextMenuContext';
-
-/**
- * Show the report action context menu.
- *
- * @param {Object} event - Press event object
- * @param {Element} anchor - Context menu anchor
- * @param {String} reportID - Active Report ID
- * @param {Object} action - ReportAction for ContextMenu
- * @param {Function} checkIfContextMenuActive Callback to update context menu active state
- * @param {Boolean} [isArchivedRoom=false] - Is the report an archived room
- */
-function showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive, isArchivedRoom = false) {
- if (!DeviceCapabilities.canUseTouchScreen()) {
- return;
- }
- ReportActionContextMenu.showContextMenu(
- CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
- event,
- '',
- anchor,
- reportID,
- action.reportActionID,
- ReportUtils.getOriginalReportID(reportID, action),
- undefined,
- checkIfContextMenuActive,
- checkIfContextMenuActive,
- isArchivedRoom,
- );
-}
-
-export {ShowContextMenuContext, showContextMenuForReport};
diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts
new file mode 100644
index 000000000000..17557051bef9
--- /dev/null
+++ b/src/components/ShowContextMenuContext.ts
@@ -0,0 +1,64 @@
+import {createContext} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Text as RNText} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
+import CONST from '@src/CONST';
+import type {Report, ReportAction} from '@src/types/onyx';
+
+type ShowContextMenuContextProps = {
+ anchor: RNText | null;
+ report: OnyxEntry;
+ action: OnyxEntry;
+ checkIfContextMenuActive: () => void;
+};
+
+const ShowContextMenuContext = createContext({
+ anchor: null,
+ report: null,
+ action: null,
+ checkIfContextMenuActive: () => {},
+});
+
+ShowContextMenuContext.displayName = 'ShowContextMenuContext';
+
+/**
+ * Show the report action context menu.
+ *
+ * @param event - Press event object
+ * @param anchor - Context menu anchor
+ * @param reportID - Active Report ID
+ * @param action - ReportAction for ContextMenu
+ * @param checkIfContextMenuActive Callback to update context menu active state
+ * @param isArchivedRoom - Is the report an archived room
+ */
+function showContextMenuForReport(
+ event: GestureResponderEvent | MouseEvent,
+ anchor: RNText | null,
+ reportID: string,
+ action: OnyxEntry,
+ checkIfContextMenuActive: () => void,
+ isArchivedRoom = false,
+) {
+ if (!DeviceCapabilities.canUseTouchScreen()) {
+ return;
+ }
+
+ ReportActionContextMenu.showContextMenu(
+ CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
+ event,
+ '',
+ anchor,
+ reportID,
+ action?.reportActionID,
+ ReportUtils.getOriginalReportID(reportID, action),
+ undefined,
+ checkIfContextMenuActive,
+ checkIfContextMenuActive,
+ isArchivedRoom,
+ );
+}
+
+export {ShowContextMenuContext, showContextMenuForReport};
diff --git a/src/components/ShowMoreButton/index.js b/src/components/ShowMoreButton/index.js
index 34b55fa5dcf1..28c33d185cff 100644
--- a/src/components/ShowMoreButton/index.js
+++ b/src/components/ShowMoreButton/index.js
@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx
index 4c2ba9c34a9f..c8bf783032ad 100644
--- a/src/components/SingleChoiceQuestion.tsx
+++ b/src/components/SingleChoiceQuestion.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import type {MaybePhraseKey} from '@libs/Localize';
diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx
index 24acdf6c5f0b..d39f03c5aad4 100644
--- a/src/components/TaskHeaderActionButton.tsx
+++ b/src/components/TaskHeaderActionButton.tsx
@@ -32,7 +32,7 @@ function TaskHeaderActionButton({report, session, policy}: TaskHeaderActionButto
(ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))}
diff --git a/src/components/Text.tsx b/src/components/Text.tsx
index f436b9f4495a..b94530a423f7 100644
--- a/src/components/Text.tsx
+++ b/src/components/Text.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React from 'react';
+// eslint-disable-next-line no-restricted-imports
import {Text as RNText, StyleSheet} from 'react-native';
import type {TextProps as RNTextProps, TextStyle} from 'react-native';
import useTheme from '@hooks/useTheme';
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index d19d835d68bb..99b3e98588ac 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -263,7 +263,7 @@ function BaseTextInput(
return (
<>
diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx
index 507d40f475a7..8f6d3efdcd8d 100644
--- a/src/components/TextInput/TextInputLabel/index.tsx
+++ b/src/components/TextInput/TextInputLabel/index.tsx
@@ -1,4 +1,5 @@
import React, {useEffect, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text} from 'react-native';
import {Animated} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx
index d3c515115d56..c8cd39b05fcc 100644
--- a/src/components/TextLink.tsx
+++ b/src/components/TextLink.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef, KeyboardEventHandler, MouseEventHandler} from 'react';
import React, {forwardRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native';
import useEnvironment from '@hooks/useEnvironment';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
index 0df9993f8c69..21e19ac7c2e8 100644
--- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
+++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
@@ -1,8 +1,9 @@
import Str from 'expensify-common/lib/str';
import React, {useCallback} from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import Avatar from '@components/Avatar';
import {usePersonalDetails} from '@components/OnyxProvider';
+import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import type UserDetailsTooltipProps from '@components/UserDetailsTooltip/types';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/components/ValuePicker/ValueSelectorModal.js b/src/components/ValuePicker/ValueSelectorModal.js
index 79608edb3ef4..e45ba873d8a3 100644
--- a/src/components/ValuePicker/ValueSelectorModal.js
+++ b/src/components/ValuePicker/ValueSelectorModal.js
@@ -26,6 +26,9 @@ const propTypes = {
/** Function to call when the user closes the modal */
onClose: PropTypes.func,
+
+ /** Whether to show the toolip text */
+ shouldShowTooltips: PropTypes.bool,
};
const defaultProps = {
@@ -34,9 +37,10 @@ const defaultProps = {
label: '',
onClose: () => {},
onItemSelected: () => {},
+ shouldShowTooltips: true,
};
-function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onItemSelected}) {
+function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onItemSelected, shouldShowTooltips}) {
const styles = useThemeStyles();
const [sectionsData, setSectionsData] = useState([]);
@@ -69,6 +73,7 @@ function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onI
onSelectRow={onItemSelected}
initiallyFocusedOptionKey={selectedItem.value}
shouldStopPropagation
+ shouldShowTooltips={shouldShowTooltips}
/>
diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js
index a21402b9993f..d90529114af4 100644
--- a/src/components/ValuePicker/index.js
+++ b/src/components/ValuePicker/index.js
@@ -34,6 +34,9 @@ const propTypes = {
/** A ref to forward to MenuItemWithTopDescription */
forwardedRef: refPropTypes,
+
+ /** Whether to show the toolip text */
+ shouldShowTooltips: PropTypes.bool,
};
const defaultProps = {
@@ -45,9 +48,10 @@ const defaultProps = {
errorText: '',
furtherDetails: undefined,
onInputChange: () => {},
+ shouldShowTooltips: true,
};
-function ValuePicker({value, label, items, placeholder, errorText, onInputChange, furtherDetails, forwardedRef}) {
+function ValuePicker({value, label, items, placeholder, errorText, onInputChange, furtherDetails, shouldShowTooltips, forwardedRef}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [isPickerVisible, setIsPickerVisible] = useState(false);
@@ -67,7 +71,7 @@ function ValuePicker({value, label, items, placeholder, errorText, onInputChange
hidePickerModal();
};
- const descStyle = value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null;
+ const descStyle = !value || value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null;
const selectedItem = _.find(items, {value});
const selectedLabel = selectedItem ? selectedItem.label : '';
@@ -92,6 +96,7 @@ function ValuePicker({value, label, items, placeholder, errorText, onInputChange
items={items}
onClose={hidePickerModal}
onItemSelected={updateInput}
+ shouldShowTooltips={shouldShowTooltips}
/>
);
diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx
similarity index 71%
rename from src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
rename to src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx
index 54e7309ee48b..9f615cef525d 100755
--- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
+++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx
@@ -1,7 +1,5 @@
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Dimensions, View} from 'react-native';
-import _ from 'underscore';
import GoogleMeetIcon from '@assets/images/google-meet.svg';
import ZoomIcon from '@assets/images/zoom-icon.svg';
import Icon from '@components/Icon';
@@ -10,37 +8,34 @@ import MenuItem from '@components/MenuItem';
import Popover from '@components/Popover';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Link from '@userActions/Link';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
-import {defaultProps, propTypes as videoChatButtonAndMenuPropTypes} from './videoChatButtonAndMenuPropTypes';
+import type VideoChatButtonAndMenuProps from './types';
-const propTypes = {
+type BaseVideoChatButtonAndMenuProps = VideoChatButtonAndMenuProps & {
/** Link to open when user wants to create a new google meet meeting */
- googleMeetURL: PropTypes.string.isRequired,
-
- ...videoChatButtonAndMenuPropTypes,
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
+ googleMeetURL: string;
};
-function BaseVideoChatButtonAndMenu(props) {
+function BaseVideoChatButtonAndMenu({googleMeetURL, isConcierge = false, guideCalendarLink}: BaseVideoChatButtonAndMenuProps) {
const theme = useTheme();
const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
const [isVideoChatMenuActive, setIsVideoChatMenuActive] = useState(false);
const [videoChatIconPosition, setVideoChatIconPosition] = useState({x: 0, y: 0});
- const videoChatIconWrapperRef = useRef(null);
- const videoChatButtonRef = useRef(null);
+ const videoChatIconWrapperRef = useRef(null);
+ const videoChatButtonRef = useRef(null);
const menuItemData = [
{
icon: ZoomIcon,
- text: props.translate('videoChatButtonAndMenu.zoom'),
+ text: translate('videoChatButtonAndMenu.zoom'),
onPress: () => {
setIsVideoChatMenuActive(false);
Link.openExternalLink(CONST.NEW_ZOOM_MEETING_URL);
@@ -48,10 +43,10 @@ function BaseVideoChatButtonAndMenu(props) {
},
{
icon: GoogleMeetIcon,
- text: props.translate('videoChatButtonAndMenu.googleMeet'),
+ text: translate('videoChatButtonAndMenu.googleMeet'),
onPress: () => {
setIsVideoChatMenuActive(false);
- Link.openExternalLink(props.googleMeetURL);
+ Link.openExternalLink(googleMeetURL);
},
},
];
@@ -87,22 +82,22 @@ function BaseVideoChatButtonAndMenu(props) {
ref={videoChatIconWrapperRef}
onLayout={measureVideoChatIconPosition}
>
-
+
{
// Drop focus to avoid blue focus ring.
- videoChatButtonRef.current.blur();
+ videoChatButtonRef.current?.blur();
// If this is the Concierge chat, we'll open the modal for requesting a setup call instead
- if (props.isConcierge && props.guideCalendarLink) {
- Link.openExternalLink(props.guideCalendarLink);
+ if (isConcierge && guideCalendarLink) {
+ Link.openExternalLink(guideCalendarLink);
return;
}
setIsVideoChatMenuActive((previousVal) => !previousVal);
})}
style={styles.touchableButtonImage}
- accessibilityLabel={props.translate('videoChatButtonAndMenu.tooltip')}
+ accessibilityLabel={translate('videoChatButtonAndMenu.tooltip')}
role={CONST.ROLE.BUTTON}
>
-
- {_.map(menuItemData, ({icon, text, onPress}) => (
+
+ {menuItemData.map(({icon, text, onPress}) => (
);
}
ErrorBodyText.displayName = 'ErrorBodyText';
-ErrorBodyText.propTypes = propTypes;
-export default withLocalize(ErrorBodyText);
+
+export default ErrorBodyText;
diff --git a/src/pages/ErrorPage/ErrorBodyText/index.website.js b/src/pages/ErrorPage/ErrorBodyText/index.website.tsx
similarity index 100%
rename from src/pages/ErrorPage/ErrorBodyText/index.website.js
rename to src/pages/ErrorPage/ErrorBodyText/index.website.tsx
diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.tsx
similarity index 91%
rename from src/pages/ErrorPage/GenericErrorPage.js
rename to src/pages/ErrorPage/GenericErrorPage.tsx
index 56fb5b970084..f4f1d91418c7 100644
--- a/src/pages/ErrorPage/GenericErrorPage.js
+++ b/src/pages/ErrorPage/GenericErrorPage.tsx
@@ -9,7 +9,7 @@ import ImageSVG from '@components/ImageSVG';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -18,20 +18,18 @@ import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ErrorBodyText from './ErrorBodyText';
-const propTypes = {
- ...withLocalizePropTypes,
-};
-
-function GenericErrorPage({translate}) {
+function GenericErrorPage() {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+
const {resetBoundary} = useErrorBoundary();
return (
{({paddingBottom}) => (
-
+
@@ -78,7 +76,7 @@ function GenericErrorPage({translate}) {
-
+
void;
};
// eslint-disable-next-line rulesdir/no-negated-variables
-function NotFoundPage(props) {
+function NotFoundPage({onBackButtonPress}: NotFoundPageProps) {
return (
);
}
NotFoundPage.displayName = 'NotFoundPage';
-NotFoundPage.propTypes = propTypes;
-NotFoundPage.defaultProps = defaultProps;
export default NotFoundPage;
diff --git a/src/pages/GetAssistancePage.js b/src/pages/GetAssistancePage.tsx
similarity index 60%
rename from src/pages/GetAssistancePage.js
rename to src/pages/GetAssistancePage.tsx
index a880637ca100..cf3dcf98ac3b 100644
--- a/src/pages/GetAssistancePage.js
+++ b/src/pages/GetAssistancePage.tsx
@@ -1,61 +1,50 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import type {RouteProp} from '@react-navigation/native';
import React from 'react';
import {ScrollView, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx/lib/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
+import type {MenuItemWithLink} from '@components/MenuItemList';
import ScreenWrapper from '@components/ScreenWrapper';
import Section from '@components/Section';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as Link from '@userActions/Link';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
+import type {Account} from '@src/types/onyx';
-const propTypes = {
- /** Route object from navigation */
- route: PropTypes.shape({
- params: PropTypes.shape({
- /** The task ID to request the call for */
- taskID: PropTypes.string,
- }),
- }).isRequired,
-
+type GetAssistanceOnyxProps = {
/** The details about the account that the user is signing in with */
- account: PropTypes.shape({
- /** URL to the assigned guide's appointment booking calendar */
- guideCalendarLink: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
+ account: OnyxEntry;
};
-const defaultProps = {
- account: {
- guideCalendarLink: null,
- },
+type GetAssistancePageProps = GetAssistanceOnyxProps & {
+ /** Route object from navigation */
+ route: RouteProp<{params: {backTo: Route}}>;
};
-function GetAssistancePage(props) {
+function GetAssistancePage({route, account}: GetAssistancePageProps) {
const styles = useThemeStyles();
- const navigateBackTo = lodashGet(props.route, 'params.backTo', ROUTES.SETTINGS_CONTACT_METHODS);
- const menuItems = [
+ const {translate} = useLocalize();
+ const navigateBackTo = route?.params.backTo || ROUTES.SETTINGS_CONTACT_METHODS.getRoute();
+ const menuItems: MenuItemWithLink[] = [
{
- title: props.translate('getAssistancePage.chatWithConcierge'),
+ title: translate('getAssistancePage.chatWithConcierge'),
onPress: () => Report.navigateToConciergeChat(),
icon: Expensicons.ChatBubble,
shouldShowRightIcon: true,
wrapperStyle: [styles.cardMenuItem],
},
{
- title: props.translate('getAssistancePage.exploreHelpDocs'),
+ title: translate('getAssistancePage.exploreHelpDocs'),
onPress: () => Link.openExternalLink(CONST.NEWHELP_URL),
icon: Expensicons.QuestionMark,
shouldShowRightIcon: true,
@@ -66,10 +55,10 @@ function GetAssistancePage(props) {
];
// If the user is eligible for calls with their Guide, add the 'Schedule a setup call' item at the second position in the list
- const guideCalendarLink = lodashGet(props.account, 'guideCalendarLink');
+ const guideCalendarLink = account?.guideCalendarLink;
if (guideCalendarLink) {
menuItems.splice(1, 0, {
- title: props.translate('getAssistancePage.scheduleSetupCall'),
+ title: translate('getAssistancePage.scheduleSetupCall'),
onPress: () => Link.openExternalLink(guideCalendarLink),
icon: Expensicons.Phone,
shouldShowRightIcon: true,
@@ -82,17 +71,17 @@ function GetAssistancePage(props) {
return (
Navigation.goBack(navigateBackTo)}
/>
- {props.translate('getAssistancePage.description')}
+ {translate('getAssistancePage.description')}
@@ -100,16 +89,11 @@ function GetAssistancePage(props) {
);
}
-GetAssistancePage.propTypes = propTypes;
-GetAssistancePage.defaultProps = defaultProps;
GetAssistancePage.displayName = 'GetAssistancePage';
-export default compose(
- withLocalize,
- withOnyx({
- account: {
- key: ONYXKEYS.ACCOUNT,
- selector: (account) => account && {guideCalendarLink: account.guideCalendarLink},
- },
- }),
-)(GetAssistancePage);
+export default withOnyx({
+ account: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (account) => account && {guideCalendarLink: account.guideCalendarLink},
+ },
+})(GetAssistancePage);
diff --git a/src/pages/KeyboardShortcutsPage.js b/src/pages/KeyboardShortcutsPage.tsx
similarity index 76%
rename from src/pages/KeyboardShortcutsPage.js
rename to src/pages/KeyboardShortcutsPage.tsx
index 809d2ce6dc07..9b70defbf8af 100644
--- a/src/pages/KeyboardShortcutsPage.js
+++ b/src/pages/KeyboardShortcutsPage.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import {ScrollView, View} from 'react-native';
-import _ from 'underscore';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
@@ -10,11 +9,15 @@ import useThemeStyles from '@hooks/useThemeStyles';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import CONST from '@src/CONST';
+type Shortcut = {
+ displayName: string;
+ descriptionKey: 'search' | 'newChat' | 'openShortcutDialog' | 'escape' | 'copy';
+};
+
function KeyboardShortcutsPage() {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const shortcuts = _.chain(CONST.KEYBOARD_SHORTCUTS)
- .filter((shortcut) => !_.isEmpty(shortcut.descriptionKey))
+ const shortcuts = Object.values(CONST.KEYBOARD_SHORTCUTS)
.map((shortcut) => {
const platformAdjustedModifiers = KeyboardShortcut.getPlatformEquivalentForKeys(shortcut.modifiers);
return {
@@ -22,16 +25,12 @@ function KeyboardShortcutsPage() {
descriptionKey: shortcut.descriptionKey,
};
})
- .value();
-
+ .filter((shortcut): shortcut is Shortcut => !!shortcut.descriptionKey);
/**
* Render the information of a single shortcut
- * @param {Object} shortcut
- * @param {String} shortcut.displayName
- * @param {String} shortcut.descriptionKey
- * @returns {React.Component}
+ * @param shortcut - The shortcut to render
*/
- const renderShortcut = (shortcut) => (
+ const renderShortcut = (shortcut: Shortcut) => (
- {translate('keyboardShortcutsPage.subtitle')}
- {_.map(shortcuts, renderShortcut)}
+ {translate('keyboardShortcutsPage.subtitle')}
+ {shortcuts.map(renderShortcut)}
diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.js b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
similarity index 65%
rename from src/pages/LogInWithShortLivedAuthTokenPage.js
rename to src/pages/LogInWithShortLivedAuthTokenPage.tsx
index 1fe9b67eef16..c5f8a9c20d5b 100644
--- a/src/pages/LogInWithShortLivedAuthTokenPage.js
+++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
@@ -1,7 +1,7 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Icon from '@components/Icon';
@@ -13,62 +13,40 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
+import type {PublicScreensParamList} from '@libs/Navigation/types';
import * as Session from '@userActions/Session';
import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Account} from '@src/types/onyx';
-const propTypes = {
- /** The parameters needed to authenticate with a short-lived token are in the URL */
- route: PropTypes.shape({
- /** Each parameter passed via the URL */
- params: PropTypes.shape({
- /** Short-lived authToken to sign in a user */
- shortLivedAuthToken: PropTypes.string,
-
- /** Short-lived authToken to sign in as a user, if they are coming from the old mobile app */
- shortLivedToken: PropTypes.string,
-
- /** The email of the transitioning user */
- email: PropTypes.string,
- }),
- }).isRequired,
-
+type LogInWithShortLivedAuthTokenPageOnyxProps = {
/** The details about the account that the user is signing in with */
- account: PropTypes.shape({
- /** Whether a sign is loading */
- isLoading: PropTypes.bool,
- }),
+ account: OnyxEntry;
};
-const defaultProps = {
- account: {
- isLoading: false,
- },
-};
+type LogInWithShortLivedAuthTokenPageProps = LogInWithShortLivedAuthTokenPageOnyxProps & StackScreenProps;
-function LogInWithShortLivedAuthTokenPage(props) {
+function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedAuthTokenPageProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {email = '', shortLivedAuthToken = '', shortLivedToken = '', exitTo, error} = route?.params ?? {};
useEffect(() => {
- const email = lodashGet(props, 'route.params.email', '');
-
// We have to check for both shortLivedAuthToken and shortLivedToken, as the old mobile app uses shortLivedToken, and is not being actively updated.
- const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '') || lodashGet(props, 'route.params.shortLivedToken', '');
+ const token = shortLivedAuthToken || shortLivedToken;
// Try to authenticate using the shortLivedToken if we're not already trying to load the accounts
- if (shortLivedAuthToken && !props.account.isLoading) {
- Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
+ if (token && !account?.isLoading) {
+ Session.signInWithShortLivedAuthToken(email, token);
return;
}
// If an error is returned as part of the route, ensure we set it in the onyxData for the account
- const error = lodashGet(props, 'route.params.error', '');
if (error) {
Session.setAccountError(error);
}
- const exitTo = lodashGet(props, 'route.params.exitTo', '');
if (exitTo) {
Navigation.isNavigationReady().then(() => {
Navigation.navigate(exitTo);
@@ -76,9 +54,9 @@ function LogInWithShortLivedAuthTokenPage(props) {
}
// The only dependencies of the effect are based on props.route
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.route]);
+ }, [route]);
- if (props.account.isLoading) {
+ if (account?.isLoading) {
return ;
}
@@ -94,7 +72,7 @@ function LogInWithShortLivedAuthTokenPage(props) {
{translate('deeplinkWrapper.launching')}
-
+
{translate('deeplinkWrapper.expired')}{' '}
{
@@ -119,10 +97,8 @@ function LogInWithShortLivedAuthTokenPage(props) {
);
}
-LogInWithShortLivedAuthTokenPage.propTypes = propTypes;
-LogInWithShortLivedAuthTokenPage.defaultProps = defaultProps;
LogInWithShortLivedAuthTokenPage.displayName = 'LogInWithShortLivedAuthTokenPage';
-export default withOnyx({
+export default withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
})(LogInWithShortLivedAuthTokenPage);
diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js
index 9609f3f9bd56..5c8a39204467 100644
--- a/src/pages/LogOutPreviousUserPage.js
+++ b/src/pages/LogOutPreviousUserPage.js
@@ -5,10 +5,8 @@ import {Linking} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as SessionUtils from '@libs/SessionUtils';
-import Navigation from '@navigation/Navigation';
import * as Session from '@userActions/Session';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
const propTypes = {
/** The details about the account that the user is signing in with */
@@ -33,6 +31,10 @@ const defaultProps = {
},
};
+// This page is responsible for handling transitions from OldDot. Specifically, it logs the current user
+// out if the transition is for another user.
+//
+// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
function LogOutPreviousUserPage(props) {
useEffect(() => {
Linking.getInitialURL().then((transitionURL) => {
@@ -53,18 +55,11 @@ function LogOutPreviousUserPage(props) {
const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '');
Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
}
-
- const exitTo = lodashGet(props, 'route.params.exitTo', '');
- // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
- // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
- // which is already called when AuthScreens mounts.
- if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !props.account.isLoading && !isLoggingInAsNewUser) {
- Navigation.isNavigationReady().then(() => {
- Navigation.navigate(exitTo);
- });
- }
});
- }, [props]);
+
+ // We only want to run this effect once on mount (when the page first loads after transitioning from OldDot)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
return ;
}
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index 3a58727eddb7..b90ce6bbc247 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
-import {InteractionManager, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
@@ -15,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import doInteractionTask from '@libs/DoInteractionTask';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
import variables from '@styles/variables';
@@ -209,11 +210,16 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i
}, [reports, personalDetails, searchTerm]);
useEffect(() => {
- const interactionTask = InteractionManager.runAfterInteractions(() => {
+ const interactionTask = doInteractionTask(() => {
setDidScreenTransitionEnd(true);
});
- return interactionTask.cancel;
+ return () => {
+ if (!interactionTask) {
+ return;
+ }
+ interactionTask.cancel();
+ };
}, []);
useEffect(() => {
diff --git a/src/pages/ReferralDetailsPage.js b/src/pages/ReferralDetailsPage.js
index 209b8f5fadc3..c10500c428da 100644
--- a/src/pages/ReferralDetailsPage.js
+++ b/src/pages/ReferralDetailsPage.js
@@ -64,15 +64,15 @@ function ReferralDetailsPage({route, account}) {
headerContent={
}
headerContainerStyles={[styles.staticHeaderImage, styles.justifyContentEnd]}
backgroundColor={theme.PAGE_THEMES[SCREENS.RIGHT_MODAL.REFERRAL].backgroundColor}
>
- {contentHeader}
- {contentBody}
+ {contentHeader}
+ {contentBody}
{shouldShowClipboard && (
- Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
- subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'}
- />
-
- );
- }
-
const isLoading = (isLoadingApp || account.isLoading || reimbursementAccount.isLoading) && (!plaidCurrentEvent || plaidCurrentEvent === CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.EXIT);
const shouldShowOfflineLoader = !(
isOffline && _.contains([CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, CONST.BANK_ACCOUNT.STEP.COMPANY, CONST.BANK_ACCOUNT.STEP.REQUESTOR, CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT], currentStep)
@@ -427,6 +415,18 @@ function ReimbursementAccountPage({reimbursementAccount, route, onfidoToken, pol
);
}
+ if (!isLoading && (_.isEmpty(policy) || !PolicyUtils.isPolicyAdmin(policy))) {
+ return (
+
+ Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
+ subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'}
+ />
+
+ );
+ }
+
let errorText;
const userHasPhonePrimaryEmail = Str.endsWith(session.email, CONST.SMS.DOMAIN);
const throttledDate = lodashGet(reimbursementAccount, 'throttledDate', '');
diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js
index a8c600a91845..30ffd60aa4ac 100644
--- a/src/pages/RoomMembersPage.js
+++ b/src/pages/RoomMembersPage.js
@@ -282,6 +282,7 @@ function RoomMembersPage(props) {
canSelectMultiple
sections={[{data, indexOffset: 0, isDisabled: false}]}
textInputLabel={props.translate('optionsSelector.findMember')}
+ disableKeyboardShortcuts={removeMembersConfirmModalVisible}
textInputValue={searchValue}
onChangeText={setSearchValue}
headerMessage={headerMessage}
diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx
index 69429962869b..e0ef67ad9ec3 100644
--- a/src/pages/SearchPage/SearchPageFooter.tsx
+++ b/src/pages/SearchPage/SearchPageFooter.tsx
@@ -1,55 +1,15 @@
import React from 'react';
import {View} from 'react-native';
-import Icon from '@components/Icon';
-import {Info} from '@components/Icon/Expensicons';
-import {PressableWithoutFeedback} from '@components/Pressable';
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import useTheme from '@hooks/useTheme';
+import ReferralProgramCTA from '@components/ReferralProgramCTA';
import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
function SearchPageFooter() {
const themeStyles = useThemeStyles();
- const theme = useTheme();
- const {translate} = useLocalize();
return (
- {
- Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND));
- }}
- style={[
- themeStyles.p5,
- themeStyles.w100,
- themeStyles.br2,
- themeStyles.highlightBG,
- themeStyles.flexRow,
- themeStyles.justifyContentBetween,
- themeStyles.alignItemsCenter,
- {gap: 10},
- ]}
- accessibilityLabel="referral"
- role={CONST.ACCESSIBILITY_ROLE.BUTTON}
- >
-
- {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText1`)}
-
- {translate(`referralProgram.${CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}.buttonText2`)}
-
-
-
-
+
);
}
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index edf6b65b2f4a..7cc1b6ce26dd 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -109,9 +109,7 @@ function HeaderView(props) {
const isAutomatedExpensifyAccount = ReportUtils.hasSingleParticipant(props.report) && ReportUtils.hasAutomatedExpensifyAccountIDs(participants);
const parentReportAction = ReportActionsUtils.getParentReportAction(props.report);
const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(props.report, parentReportAction);
- const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(props.report.reportID);
const isWhisperAction = ReportActionsUtils.isWhisperAction(parentReportAction);
- const isEmptyChat = !props.report.lastMessageText && !props.report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey;
const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(props.report);
const isPolicyMember = useMemo(() => !_.isEmpty(props.policy), [props.policy]);
const canLeaveRoom = ReportUtils.canLeaveRoom(props.report, isPolicyMember);
@@ -133,7 +131,7 @@ function HeaderView(props) {
}
// Task is not closed
- if (props.report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum !== CONST.REPORT.STATUS.CLOSED && canModifyTask) {
+ if (props.report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && props.report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) {
threeDotMenuItems.push({
icon: Expensicons.Trashcan,
text: translate('common.delete'),
@@ -153,7 +151,7 @@ function HeaderView(props) {
),
);
- const canJoinOrLeave = (isChatThread && !isEmptyChat) || isUserCreatedPolicyRoom || canLeaveRoom;
+ const canJoinOrLeave = isChatThread || isUserCreatedPolicyRoom || canLeaveRoom;
const canJoin = canJoinOrLeave && !isWhisperAction && props.report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const canLeave = canJoinOrLeave && ((isChatThread && props.report.notificationPreference.length) || isUserCreatedPolicyRoom || canLeaveRoom);
if (canJoin) {
@@ -163,7 +161,7 @@ function HeaderView(props) {
onSelected: join,
});
} else if (canLeave) {
- const isWorkspaceMemberLeavingWorkspaceRoom = lodashGet(props.report, 'visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember;
+ const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && lodashGet(props.report, 'visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember;
threeDotMenuItems.push({
icon: Expensicons.ChatBubbles,
text: translate('common.leave'),
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index 64e48ecd5509..70a0f1a236bb 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -187,7 +187,7 @@ function ReportScreen({
// There are no reportActions at all to display and we are still in the process of loading the next set of actions.
const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions;
- const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED;
+ const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS_NUM.CLOSED;
const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas);
const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails);
const isSingleTransactionView = ReportUtils.isMoneyRequest(report);
@@ -383,8 +383,8 @@ function ReportScreen({
(prevOnyxReportID &&
prevOnyxReportID === routeReportID &&
!onyxReportID &&
- prevReport.statusNum === CONST.REPORT.STATUS.OPEN &&
- (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) ||
+ prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN &&
+ (report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) ||
((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport)) && _.isEmpty(report))
) {
Navigation.dismissModal();
@@ -393,6 +393,11 @@ function ReportScreen({
Navigation.goBack(ROUTES.HOME, false, true);
}
if (prevReport.parentReportID) {
+ // Prevent navigation to the Money Request Report if it is pending deletion.
+ const parentReport = ReportUtils.getReport(prevReport.parentReportID);
+ if (ReportUtils.isMoneyRequestReportPendingDeletion(parentReport)) {
+ return;
+ }
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(prevReport.parentReportID));
return;
}
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js
deleted file mode 100755
index fc06176edd3b..000000000000
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {memo, useMemo, useRef, useState} from 'react';
-import {InteractionManager, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import ContextMenuItem from '@components/ContextMenuItem';
-import {withBetas} from '@components/OnyxProvider';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
-import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
-import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
-import useNetwork from '@hooks/useNetwork';
-import useStyleUtils from '@hooks/useStyleUtils';
-import compose from '@libs/compose';
-import * as Session from '@userActions/Session';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ContextMenuActions from './ContextMenuActions';
-import {defaultProps as GenericReportActionContextMenuDefaultProps, propTypes as genericReportActionContextMenuPropTypes} from './genericReportActionContextMenuPropTypes';
-import {hideContextMenu} from './ReportActionContextMenu';
-
-const propTypes = {
- /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */
- type: PropTypes.string,
-
- /** Target node which is the target of ContentMenu */
- anchor: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
-
- /** Flag to check if the chat participant is Chronos */
- isChronosReport: PropTypes.bool,
-
- /** Whether the provided report is an archived room */
- isArchivedRoom: PropTypes.bool,
-
- contentRef: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.func]),
-
- ...genericReportActionContextMenuPropTypes,
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
-};
-
-const defaultProps = {
- type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
- anchor: null,
- contentRef: null,
- isChronosReport: false,
- isArchivedRoom: false,
- ...GenericReportActionContextMenuDefaultProps,
-};
-function BaseReportActionContextMenu(props) {
- const StyleUtils = useStyleUtils();
- const menuItemRefs = useRef({});
- const [shouldKeepOpen, setShouldKeepOpen] = useState(false);
- const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(props.isMini, props.isSmallScreenWidth);
- const {isOffline} = useNetwork();
-
- const reportAction = useMemo(() => {
- if (_.isEmpty(props.reportActions) || props.reportActionID === '0') {
- return {};
- }
- return props.reportActions[props.reportActionID] || {};
- }, [props.reportActions, props.reportActionID]);
-
- const shouldShowFilter = (contextAction) =>
- contextAction.shouldShow(
- props.type,
- reportAction,
- props.isArchivedRoom,
- props.betas,
- props.anchor,
- props.isChronosReport,
- props.reportID,
- props.isPinnedChat,
- props.isUnreadChat,
- isOffline,
- );
-
- const shouldEnableArrowNavigation = !props.isMini && (props.isVisible || shouldKeepOpen);
- const filteredContextMenuActions = _.filter(ContextMenuActions, shouldShowFilter);
-
- // Context menu actions that are not rendered as menu items are excluded from arrow navigation
- const nonMenuItemActionIndexes = _.map(filteredContextMenuActions, (contextAction, index) => (_.isFunction(contextAction.renderContent) ? index : undefined));
- const disabledIndexes = _.filter(nonMenuItemActionIndexes, (index) => !_.isUndefined(index));
-
- const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({
- initialFocusedIndex: -1,
- disabledIndexes,
- maxIndex: filteredContextMenuActions.length - 1,
- isActive: shouldEnableArrowNavigation,
- });
-
- /**
- * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and
- * shows the sign in modal. Else, executes the callback.
- *
- * @param {Function} callback
- * @param {Boolean} isAnonymousAction
- */
- const interceptAnonymousUser = (callback, isAnonymousAction = false) => {
- if (Session.isAnonymousUser() && !isAnonymousAction) {
- hideContextMenu(false);
-
- InteractionManager.runAfterInteractions(() => {
- Session.signOutAndRedirectToSignIn();
- });
- } else {
- callback();
- }
- };
-
- useKeyboardShortcut(
- CONST.KEYBOARD_SHORTCUTS.ENTER,
- (event) => {
- if (!menuItemRefs.current[focusedIndex]) {
- return;
- }
-
- // Ensures the event does not cause side-effects beyond the context menu, e.g. when an outside element is focused
- if (event) {
- event.stopPropagation();
- }
-
- menuItemRefs.current[focusedIndex].triggerPressAndUpdateSuccess();
- setFocusedIndex(-1);
- },
- {isActive: shouldEnableArrowNavigation},
- );
-
- return (
- (props.isVisible || shouldKeepOpen) && (
-
- {_.map(filteredContextMenuActions, (contextAction, index) => {
- const closePopup = !props.isMini;
- const payload = {
- reportAction,
- reportID: props.reportID,
- draftMessage: props.draftMessage,
- selection: props.selection,
- close: () => setShouldKeepOpen(false),
- openContextMenu: () => setShouldKeepOpen(true),
- interceptAnonymousUser,
- };
-
- if (contextAction.renderContent) {
- // make sure that renderContent isn't mixed with unsupported props
- if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) {
- throw new Error('Dev error: renderContent() and text/icon cannot be used together.');
- }
-
- return contextAction.renderContent(closePopup, payload);
- }
-
- return (
- {
- menuItemRefs.current[index] = ref;
- }}
- icon={contextAction.icon}
- text={props.translate(contextAction.textTranslateKey, {action: reportAction})}
- successIcon={contextAction.successIcon}
- successText={contextAction.successTextTranslateKey ? props.translate(contextAction.successTextTranslateKey) : undefined}
- isMini={props.isMini}
- key={contextAction.textTranslateKey}
- onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)}
- description={contextAction.getDescription(props.selection, props.isSmallScreenWidth)}
- isAnonymousAction={contextAction.isAnonymousAction}
- isFocused={focusedIndex === index}
- />
- );
- })}
-
- )
- );
-}
-
-BaseReportActionContextMenu.propTypes = propTypes;
-BaseReportActionContextMenu.defaultProps = defaultProps;
-
-export default compose(
- withLocalize,
- withBetas(),
- withWindowDimensions,
- withOnyx({
- reportActions: {
- key: ({originalReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`,
- canEvict: false,
- },
- }),
-)(
- memo(BaseReportActionContextMenu, (prevProps, nextProps) => {
- const prevReportAction = lodashGet(prevProps.reportActions, prevProps.reportActionID, '');
- const nextReportAction = lodashGet(nextProps.reportActions, nextProps.reportActionID, '');
-
- // We only want to re-render when the report action that is attached to is changed
- if (prevReportAction !== nextReportAction) {
- return false;
- }
- return _.isEqual(_.omit(prevProps, 'reportActions'), _.omit(nextProps, 'reportActions'));
- }),
-);
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
new file mode 100755
index 000000000000..3eecb74a048a
--- /dev/null
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -0,0 +1,254 @@
+import lodashIsEqual from 'lodash/isEqual';
+import type {MutableRefObject, RefObject} from 'react';
+import React, {memo, useMemo, useRef, useState} from 'react';
+import {InteractionManager, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import type {ContextMenuItemHandle} from '@components/ContextMenuItem';
+import ContextMenuItem from '@components/ContextMenuItem';
+import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
+import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as Session from '@userActions/Session';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Beta, ReportAction, ReportActions} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {ContextMenuActionPayload} from './ContextMenuActions';
+import ContextMenuActions from './ContextMenuActions';
+import type {ContextMenuType} from './ReportActionContextMenu';
+import {hideContextMenu} from './ReportActionContextMenu';
+
+type BaseReportActionContextMenuOnyxProps = {
+ /** Beta features list */
+ betas: OnyxEntry;
+
+ /** All of the actions of the report */
+ reportActions: OnyxEntry;
+};
+
+type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & {
+ /** The ID of the report this report action is attached to. */
+ reportID: string;
+
+ /** The ID of the report action this context menu is attached to. */
+ reportActionID: string;
+
+ /** The ID of the original report from which the given reportAction is first created. */
+ // originalReportID is used in withOnyx to get the reportActions for the original report
+ // eslint-disable-next-line react/no-unused-prop-types
+ originalReportID: string;
+
+ /**
+ * If true, this component will be a small, row-oriented menu that displays icons but not text.
+ * If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row.
+ */
+ isMini?: boolean;
+
+ /** Controls the visibility of this component. */
+ isVisible?: boolean;
+
+ /** The copy selection. */
+ selection?: string;
+
+ /** Draft message - if this is set the comment is in 'edit' mode */
+ draftMessage?: string;
+
+ /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */
+ type?: ContextMenuType;
+
+ /** Target node which is the target of ContentMenu */
+ anchor?: MutableRefObject;
+
+ /** Flag to check if the chat participant is Chronos */
+ isChronosReport?: boolean;
+
+ /** Whether the provided report is an archived room */
+ isArchivedRoom?: boolean;
+
+ /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */
+ isPinnedChat?: boolean;
+
+ /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */
+ isUnreadChat?: boolean;
+
+ /** Content Ref */
+ contentRef?: RefObject;
+
+ checkIfContextMenuActive?: () => void;
+};
+
+type MenuItemRefs = Record;
+
+function BaseReportActionContextMenu({
+ type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
+ anchor,
+ contentRef,
+ isChronosReport = false,
+ isArchivedRoom = false,
+ isMini = false,
+ isVisible = false,
+ isPinnedChat = false,
+ isUnreadChat = false,
+ selection = '',
+ draftMessage = '',
+ reportActionID,
+ reportID,
+ betas,
+ reportActions,
+ checkIfContextMenuActive,
+}: BaseReportActionContextMenuProps) {
+ const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const menuItemRefs = useRef({});
+ const [shouldKeepOpen, setShouldKeepOpen] = useState(false);
+ const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth);
+ const {isOffline} = useNetwork();
+
+ const reportAction: OnyxEntry = useMemo(() => {
+ if (isEmptyObject(reportActions) || reportActionID === '0') {
+ return null;
+ }
+ return reportActions[reportActionID] ?? null;
+ }, [reportActions, reportActionID]);
+
+ const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen);
+ let filteredContextMenuActions = ContextMenuActions.filter((contextAction) =>
+ contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini),
+ );
+ filteredContextMenuActions =
+ isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS
+ ? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions)
+ : filteredContextMenuActions;
+
+ // Context menu actions that are not rendered as menu items are excluded from arrow navigation
+ const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) =>
+ 'renderContent' in contextAction && typeof contextAction.renderContent === 'function' ? index : undefined,
+ );
+ const disabledIndexes = nonMenuItemActionIndexes.filter((index): index is number => index !== undefined);
+
+ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({
+ initialFocusedIndex: -1,
+ disabledIndexes,
+ maxIndex: filteredContextMenuActions.length - 1,
+ isActive: shouldEnableArrowNavigation,
+ });
+
+ /**
+ * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and
+ * shows the sign in modal. Else, executes the callback.
+ */
+ const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => {
+ if (Session.isAnonymousUser() && !isAnonymousAction) {
+ hideContextMenu(false);
+
+ InteractionManager.runAfterInteractions(() => {
+ Session.signOutAndRedirectToSignIn();
+ });
+ } else {
+ callback();
+ }
+ };
+
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.ENTER,
+ (event) => {
+ if (!menuItemRefs.current[focusedIndex]) {
+ return;
+ }
+
+ // Ensures the event does not cause side-effects beyond the context menu, e.g. when an outside element is focused
+ if (event) {
+ event.stopPropagation();
+ }
+
+ menuItemRefs.current[focusedIndex]?.triggerPressAndUpdateSuccess?.();
+ setFocusedIndex(-1);
+ },
+ {isActive: shouldEnableArrowNavigation},
+ );
+
+ return (
+ (isVisible || shouldKeepOpen) && (
+
+ {filteredContextMenuActions.map((contextAction, index) => {
+ const closePopup = !isMini;
+ const payload: ContextMenuActionPayload = {
+ reportAction: reportAction as ReportAction,
+ reportID,
+ draftMessage,
+ selection,
+ close: () => setShouldKeepOpen(false),
+ openContextMenu: () => setShouldKeepOpen(true),
+ interceptAnonymousUser,
+ anchor,
+ checkIfContextMenuActive,
+ };
+
+ if ('renderContent' in contextAction) {
+ return contextAction.renderContent(closePopup, payload);
+ }
+
+ const {textTranslateKey} = contextAction;
+ const isKeyInActionUpdateKeys =
+ textTranslateKey === 'reportActionContextMenu.editAction' ||
+ textTranslateKey === 'reportActionContextMenu.deleteAction' ||
+ textTranslateKey === 'reportActionContextMenu.deleteConfirmation';
+ const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: reportAction}) : translate(textTranslateKey));
+
+ return (
+ {
+ menuItemRefs.current[index] = ref;
+ }}
+ icon={contextAction.icon}
+ text={text ?? ''}
+ successIcon={contextAction.successIcon}
+ successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined}
+ isMini={isMini}
+ key={contextAction.textTranslateKey}
+ onPress={(event) => interceptAnonymousUser(() => contextAction.onPress?.(closePopup, {...payload, event}), contextAction.isAnonymousAction)}
+ description={contextAction.getDescription?.(selection) ?? ''}
+ isAnonymousAction={contextAction.isAnonymousAction}
+ isFocused={focusedIndex === index}
+ />
+ );
+ })}
+
+ )
+ );
+}
+
+export default withOnyx({
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ reportActions: {
+ key: ({originalReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`,
+ canEvict: false,
+ },
+})(
+ memo(BaseReportActionContextMenu, (prevProps, nextProps) => {
+ const {reportActions: prevReportActions, ...prevPropsWithoutReportActions} = prevProps;
+ const {reportActions: nextReportActions, ...nextPropsWithoutReportActions} = nextProps;
+
+ const prevReportAction = prevReportActions?.[prevProps.reportActionID] ?? '';
+ const nextReportAction = nextReportActions?.[nextProps.reportActionID] ?? '';
+
+ // We only want to re-render when the report action that is attached to is changed
+ if (prevReportAction !== nextReportAction) {
+ return false;
+ }
+
+ return lodashIsEqual(prevPropsWithoutReportActions, nextPropsWithoutReportActions);
+ }),
+);
+
+export type {BaseReportActionContextMenuProps};
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
similarity index 65%
rename from src/pages/home/report/ContextMenu/ContextMenuActions.js
rename to src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index f22eda58ce7f..ea25a00ee1d3 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -1,7 +1,10 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import lodashGet from 'lodash/get';
+import type {MutableRefObject} from 'react';
import React from 'react';
-import _ from 'underscore';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Text as RNText, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {Emoji} from '@assets/emojis/types';
import * as Expensicons from '@components/Icon/Expensicons';
import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions';
import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions';
@@ -22,24 +25,20 @@ 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 ROUTES from '@src/ROUTES';
-import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu';
+import type {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx';
+import type IconAsset from '@src/types/utils/IconAsset';
+import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu';
-/**
- * Gets the HTML version of the message in an action.
- * @param {Object} reportAction
- * @return {String}
- */
-function getActionText(reportAction) {
- const message = _.last(lodashGet(reportAction, 'message', null));
- return lodashGet(message, 'html', '');
+/** Gets the HTML version of the message in an action */
+function getActionText(reportAction: OnyxEntry): string {
+ const message = reportAction?.message?.at(-1) ?? null;
+ return message?.html ?? '';
}
-/**
- * Sets the HTML string to Clipboard.
- * @param {String} content
- */
-function setClipboardMessage(content) {
+/** Sets the HTML string to Clipboard */
+function setClipboardMessage(content: string) {
const parser = new ExpensiMark();
if (!Clipboard.canSetHtml()) {
Clipboard.setString(parser.htmlToMarkdown(content));
@@ -49,16 +48,67 @@ function setClipboardMessage(content) {
}
}
+type ShouldShow = (
+ type: string,
+ reportAction: OnyxEntry,
+ isArchivedRoom: boolean,
+ betas: OnyxEntry,
+ menuTarget: MutableRefObject | undefined,
+ isChronosReport: boolean,
+ reportID: string,
+ isPinnedChat: boolean,
+ isUnreadChat: boolean,
+ isOffline: boolean,
+ isMini: boolean,
+) => boolean;
+
+type ContextMenuActionPayload = {
+ reportAction: ReportAction;
+ reportID: string;
+ draftMessage: string;
+ selection: string;
+ close: () => void;
+ openContextMenu: () => void;
+ interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void;
+ anchor?: MutableRefObject;
+ checkIfContextMenuActive?: () => void;
+ event?: GestureResponderEvent | MouseEvent | KeyboardEvent;
+};
+
+type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void;
+
+type RenderContent = (closePopover: boolean, payload: ContextMenuActionPayload) => React.ReactElement;
+
+type GetDescription = (selection?: string) => string | void;
+
+type ContextMenuActionWithContent = {
+ renderContent: RenderContent;
+};
+
+type ContextMenuActionWithIcon = {
+ textTranslateKey: TranslationPaths;
+ icon: IconAsset;
+ successTextTranslateKey?: TranslationPaths;
+ successIcon?: IconAsset;
+ onPress: OnPress;
+ getDescription: GetDescription;
+};
+
+type ContextMenuAction = (ContextMenuActionWithContent | ContextMenuActionWithIcon) & {
+ isAnonymousAction: boolean;
+ shouldShow: ShouldShow;
+};
+
// A list of all the context actions in this menu.
-export default [
+const ContextMenuActions: ContextMenuAction[] = [
{
isAnonymousAction: false,
- shouldKeepOpen: true,
- shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && _.has(reportAction, 'message') && !ReportActionsUtils.isMessageDeleted(reportAction),
+ shouldShow: (type, reportAction): reportAction is ReportAction =>
+ type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction),
renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => {
const isMini = !closePopover;
- const closeContextMenu = (onHideCallback) => {
+ const closeContextMenu = (onHideCallback?: () => void) => {
if (isMini) {
closeManually();
if (onHideCallback) {
@@ -69,7 +119,7 @@ export default [
}
};
- const toggleEmojiAndCloseMenu = (emoji, existingReactions) => {
+ const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: OnyxEntry) => {
Report.toggleEmojiReaction(reportID, reportAction, emoji, existingReactions);
closeContextMenu();
};
@@ -81,7 +131,7 @@ export default [
onEmojiSelected={toggleEmojiAndCloseMenu}
onPressOpenPicker={openContextMenu}
onEmojiPickerClosed={closeContextMenu}
- reportActionID={reportAction.reportActionID}
+ reportActionID={reportAction?.reportActionID}
reportAction={reportAction}
/>
);
@@ -92,7 +142,7 @@ export default [
key="BaseQuickEmojiReactions"
closeContextMenu={closeContextMenu}
onEmojiSelected={toggleEmojiAndCloseMenu}
- reportActionID={reportAction.reportActionID}
+ reportActionID={reportAction?.reportActionID}
reportAction={reportAction}
/>
);
@@ -104,20 +154,20 @@ export default [
icon: Expensicons.Download,
successTextTranslateKey: 'common.download',
successIcon: Expensicons.Download,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => {
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline): reportAction is ReportAction => {
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
- const messageHtml = lodashGet(reportAction, ['message', 0, 'html']);
- return isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline;
+ const messageHtml = reportAction?.message?.at(0)?.html;
+ return (
+ isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction?.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline
+ );
},
onPress: (closePopover, {reportAction}) => {
- const message = _.last(lodashGet(reportAction, 'message', [{}]));
- const html = lodashGet(message, 'html', '');
- const attachmentDetails = getAttachmentDetails(html);
- const {originalFileName, sourceURL} = attachmentDetails;
- const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL);
- const sourceID = (sourceURL.match(CONST.REGEX.ATTACHMENT_ID) || [])[1];
+ const html = getActionText(reportAction);
+ const {originalFileName, sourceURL} = getAttachmentDetails(html);
+ const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '');
+ const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1];
Download.setDownload(sourceID, true);
- fileDownload(sourceURLWithAuth, originalFileName).then(() => Download.setDownload(sourceID, false));
+ fileDownload(sourceURLWithAuth, originalFileName ?? '').then(() => Download.setDownload(sourceID, false));
if (closePopover) {
hideContextMenu(true, ReportActionComposeFocusManager.focus);
}
@@ -127,10 +177,8 @@ export default [
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.replyInThread',
- icon: Expensicons.ChatBubble,
- successTextTranslateKey: '',
- successIcon: null,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => {
+ icon: Expensicons.ChatBubbleAdd,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => {
if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) {
return false;
}
@@ -140,94 +188,145 @@ export default [
if (closePopover) {
hideContextMenu(false, () => {
ReportActionComposeFocusManager.focus();
- Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID);
+ Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID);
});
return;
}
- Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID);
+ Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID);
},
getDescription: () => {},
},
{
isAnonymousAction: false,
- textTranslateKey: 'reportActionContextMenu.subscribeToThread',
- icon: Expensicons.Bell,
- successTextTranslateKey: '',
- successIcon: null,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => {
- let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', '');
- if (!childReportNotificationPreference) {
- const isActionCreator = ReportUtils.isActionCreator(reportAction);
- childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+ textTranslateKey: 'reportActionContextMenu.editAction',
+ icon: Expensicons.Pencil,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport) =>
+ type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && !isArchivedRoom && !isChronosReport,
+ onPress: (closePopover, {reportID, reportAction, draftMessage}) => {
+ if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
+ hideContextMenu(false);
+ const childReportID = reportAction?.childReportID ?? '0';
+ if (!childReportID) {
+ const thread = ReportUtils.buildTransactionThread(reportAction, reportID);
+ const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []);
+ Report.openReport(thread.reportID, userLogins, thread, reportAction?.reportActionID);
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID));
+ return;
+ }
+ Report.openReport(childReportID);
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
+ return;
+ }
+ const editAction = () => {
+ if (!draftMessage) {
+ Report.saveReportActionDraft(reportID, reportAction, getActionText(reportAction));
+ } else {
+ Report.deleteReportActionDraft(reportID, reportAction);
+ }
+ };
+
+ if (closePopover) {
+ // Hide popover, then call editAction
+ hideContextMenu(false, editAction);
+ return;
}
+
+ // No popover to hide, call editAction immediately
+ editAction();
+ },
+ getDescription: () => {},
+ },
+ {
+ isAnonymousAction: false,
+ textTranslateKey: 'reportActionContextMenu.markAsUnread',
+ icon: Expensicons.Mail,
+ successIcon: Expensicons.Checkmark,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) =>
+ type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat),
+ onPress: (closePopover, {reportAction, reportID}) => {
+ Report.markCommentAsUnread(reportID, reportAction?.created);
+ if (closePopover) {
+ hideContextMenu(true, ReportActionComposeFocusManager.focus);
+ }
+ },
+ getDescription: () => {},
+ },
+ {
+ isAnonymousAction: false,
+ textTranslateKey: 'reportActionContextMenu.markAsRead',
+ icon: Expensicons.Mail,
+ successIcon: Expensicons.Checkmark,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) =>
+ type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat,
+ onPress: (closePopover, {reportID}) => {
+ Report.readNewestAction(reportID);
+ if (closePopover) {
+ hideContextMenu(true, ReportActionComposeFocusManager.focus);
+ }
+ },
+ getDescription: () => {},
+ },
+ {
+ isAnonymousAction: false,
+ textTranslateKey: 'reportActionContextMenu.joinThread',
+ icon: Expensicons.Bell,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => {
+ const childReportNotificationPreference = ReportUtils.getChildReportNotificationPreference(reportAction);
const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction);
const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID);
const subscribed = childReportNotificationPreference !== 'hidden';
- const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID);
- const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
- const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction);
+ const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID);
+ const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
+ const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction);
+
const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction);
return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction) && (!isDeletedAction || shouldDisplayThreadReplies);
},
onPress: (closePopover, {reportAction, reportID}) => {
- let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', '');
- if (!childReportNotificationPreference) {
- const isActionCreator = ReportUtils.isActionCreator(reportAction);
- childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
- }
+ const childReportNotificationPreference = ReportUtils.getChildReportNotificationPreference(reportAction);
if (closePopover) {
hideContextMenu(false, () => {
ReportActionComposeFocusManager.focus();
- Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference);
+ Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference);
});
return;
}
ReportActionComposeFocusManager.focus();
- Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference);
+ Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference);
},
getDescription: () => {},
},
{
isAnonymousAction: false,
- textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread',
+ textTranslateKey: 'reportActionContextMenu.leaveThread',
icon: Expensicons.BellSlash,
- successTextTranslateKey: '',
- successIcon: null,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => {
- let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', '');
- if (!childReportNotificationPreference) {
- const isActionCreator = ReportUtils.isActionCreator(reportAction);
- childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
- }
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => {
+ const childReportNotificationPreference = ReportUtils.getChildReportNotificationPreference(reportAction);
const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction);
const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID);
- const subscribed = childReportNotificationPreference !== 'hidden';
+ const subscribed = childReportNotificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) {
return false;
}
- const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID);
- const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
- const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction);
+ const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID);
+ const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
+ const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction);
return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction) && (!isDeletedAction || shouldDisplayThreadReplies);
},
onPress: (closePopover, {reportAction, reportID}) => {
- let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', '');
- if (!childReportNotificationPreference) {
- const isActionCreator = ReportUtils.isActionCreator(reportAction);
- childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
- }
+ const childReportNotificationPreference = ReportUtils.getChildReportNotificationPreference(reportAction);
if (closePopover) {
hideContextMenu(false, () => {
ReportActionComposeFocusManager.focus();
- Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference);
+ Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference);
});
return;
}
ReportActionComposeFocusManager.focus();
- Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference);
+ Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference);
},
getDescription: () => {},
},
@@ -255,7 +354,7 @@ export default [
Clipboard.setString(EmailUtils.trimMailTo(selection));
hideContextMenu(true, ReportActionComposeFocusManager.focus);
},
- getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection)),
+ getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')),
},
{
isAnonymousAction: true,
@@ -272,8 +371,7 @@ export default [
onPress: (closePopover, {reportAction, selection}) => {
const isTaskAction = ReportActionsUtils.isTaskAction(reportAction);
const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction);
- const message = _.last(lodashGet(reportAction, 'message', [{}]));
- const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : lodashGet(message, 'html', '');
+ const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionText(reportAction);
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
if (!isAttachment) {
@@ -292,11 +390,11 @@ export default [
const taskPreviewMessage = TaskUtils.getTaskCreatedMessage(reportAction);
Clipboard.setString(taskPreviewMessage);
} else if (ReportActionsUtils.isMemberChangeAction(reportAction)) {
- const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html;
+ const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html ?? '';
setClipboardMessage(logMessage);
} else if (ReportActionsUtils.isSubmittedExpenseAction(reportAction)) {
- const submittedMessage = _.reduce(reportAction.message, (acc, curr) => `${acc}${curr.text}`, '');
- Clipboard.setString(submittedMessage);
+ const submittedMessage = reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, '');
+ Clipboard.setString(submittedMessage ?? '');
} else if (content) {
setClipboardMessage(content);
}
@@ -308,7 +406,6 @@ export default [
},
getDescription: () => {},
},
-
{
isAnonymousAction: true,
textTranslateKey: 'reportActionContextMenu.copyLink',
@@ -319,90 +416,18 @@ export default [
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
// Only hide the copylink menu item when context menu is opened over img element.
- const isAttachmentTarget = lodashGet(menuTarget, 'tagName') === 'IMG' && isAttachment;
+ const isAttachmentTarget = menuTarget?.current?.tagName === 'IMG' && isAttachment;
return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction);
},
onPress: (closePopover, {reportAction, reportID}) => {
Environment.getEnvironmentURL().then((environmentURL) => {
- const reportActionID = lodashGet(reportAction, 'reportActionID');
+ const reportActionID = reportAction?.reportActionID;
Clipboard.setString(`${environmentURL}/r/${reportID}/${reportActionID}`);
});
hideContextMenu(true, ReportActionComposeFocusManager.focus);
},
getDescription: () => {},
},
-
- {
- isAnonymousAction: false,
- textTranslateKey: 'reportActionContextMenu.markAsUnread',
- icon: Expensicons.Mail,
- successIcon: Expensicons.Checkmark,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) =>
- type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat),
- onPress: (closePopover, {reportAction, reportID}) => {
- Report.markCommentAsUnread(reportID, reportAction.created);
- if (closePopover) {
- hideContextMenu(true, ReportActionComposeFocusManager.focus);
- }
- },
- getDescription: () => {},
- },
-
- {
- isAnonymousAction: false,
- textTranslateKey: 'reportActionContextMenu.markAsRead',
- icon: Expensicons.Mail,
- successIcon: Expensicons.Checkmark,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat,
- onPress: (closePopover, {reportID}) => {
- Report.readNewestAction(reportID);
- if (closePopover) {
- hideContextMenu(true, ReportActionComposeFocusManager.focus);
- }
- },
- getDescription: () => {},
- },
-
- {
- isAnonymousAction: false,
- textTranslateKey: 'reportActionContextMenu.editAction',
- icon: Expensicons.Pencil,
- shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport) =>
- type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && !isArchivedRoom && !isChronosReport,
- onPress: (closePopover, {reportID, reportAction, draftMessage}) => {
- if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
- hideContextMenu(false);
- const childReportID = lodashGet(reportAction, 'childReportID', 0);
- if (!childReportID) {
- const thread = ReportUtils.buildTransactionThread(reportAction, reportID);
- const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
- Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID);
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID));
- return;
- }
- Report.openReport(childReportID);
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
- return;
- }
- const editAction = () => {
- if (_.isUndefined(draftMessage)) {
- Report.saveReportActionDraft(reportID, reportAction, getActionText(reportAction));
- } else {
- Report.deleteReportActionDraft(reportID, reportAction);
- }
- };
-
- if (closePopover) {
- // Hide popover, then call editAction
- hideContextMenu(false, editAction);
- return;
- }
-
- // No popover to hide, call editAction immediately
- editAction();
- },
- getDescription: () => {},
- },
{
isAnonymousAction: false,
textTranslateKey: 'reportActionContextMenu.deleteAction',
@@ -430,7 +455,7 @@ export default [
isAnonymousAction: false,
textTranslateKey: 'common.pin',
icon: Expensicons.Pin,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat,
onPress: (closePopover, {reportID}) => {
Report.togglePinnedState(reportID, false);
if (closePopover) {
@@ -443,7 +468,7 @@ export default [
isAnonymousAction: false,
textTranslateKey: 'common.unPin',
icon: Expensicons.Pin,
- shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat,
onPress: (closePopover, {reportID}) => {
Report.togglePinnedState(reportID, true);
if (closePopover) {
@@ -461,16 +486,43 @@ export default [
ReportUtils.canFlagReportAction(reportAction, reportID) &&
!isArchivedRoom &&
!isChronosReport &&
- !ReportUtils.isConciergeChatReport(reportID) &&
- reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE,
+ reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE,
onPress: (closePopover, {reportID, reportAction}) => {
if (closePopover) {
- hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID)));
+ hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID)));
return;
}
- Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID));
+ Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID));
+ },
+ getDescription: () => {},
+ },
+ {
+ isAnonymousAction: true,
+ textTranslateKey: 'reportActionContextMenu.menu',
+ icon: Expensicons.ThreeDots,
+ shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini,
+ onPress: (closePopover, {reportAction, reportID, event, anchor, selection, draftMessage, checkIfContextMenuActive}) => {
+ const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction);
+ const originalReport = ReportUtils.getReport(originalReportID);
+ showContextMenu(
+ CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
+ event as GestureResponderEvent | MouseEvent,
+ selection,
+ anchor?.current as View | RNText | null,
+ reportID,
+ reportAction.reportActionID,
+ originalReportID,
+ draftMessage,
+ checkIfContextMenuActive,
+ checkIfContextMenuActive,
+ ReportUtils.isArchivedRoom(originalReport),
+ ReportUtils.chatIncludesChronos(originalReport),
+ );
},
getDescription: () => {},
},
];
+
+export default ContextMenuActions;
+export type {ContextMenuActionPayload};
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js
deleted file mode 100644
index d858206cdfc3..000000000000
--- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {View} from 'react-native';
-import _ from 'underscore';
-import useStyleUtils from '@hooks/useStyleUtils';
-import BaseReportActionContextMenu from '@pages/home/report/ContextMenu/BaseReportActionContextMenu';
-import {
- defaultProps as GenericReportActionContextMenuDefaultProps,
- propTypes as genericReportActionContextMenuPropTypes,
-} from '@pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes';
-import CONST from '@src/CONST';
-
-const propTypes = {
- ..._.omit(genericReportActionContextMenuPropTypes, ['isMini']),
-
- /** Should the reportAction this menu is attached to have the appearance of being
- * grouped with the previous reportAction? */
- displayAsGroup: PropTypes.bool,
-};
-
-const defaultProps = {
- ..._.omit(GenericReportActionContextMenuDefaultProps, ['isMini']),
- displayAsGroup: false,
-};
-
-function MiniReportActionContextMenu(props) {
- const StyleUtils = useStyleUtils();
-
- return (
-
-
-
- );
-}
-
-MiniReportActionContextMenu.propTypes = propTypes;
-MiniReportActionContextMenu.defaultProps = defaultProps;
-MiniReportActionContextMenu.displayName = 'MiniReportActionContextMenu';
-
-export default MiniReportActionContextMenu;
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js
deleted file mode 100644
index 461f67a0a4bc..000000000000
--- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js
+++ /dev/null
@@ -1 +0,0 @@
-export default () => null;
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx
new file mode 100644
index 000000000000..7be6a850d51b
--- /dev/null
+++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx
@@ -0,0 +1,4 @@
+import type MiniReportActionContextMenuProps from './types';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export default (props: MiniReportActionContextMenuProps) => null;
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx
new file mode 100644
index 000000000000..df1226eed900
--- /dev/null
+++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import {View} from 'react-native';
+import useStyleUtils from '@hooks/useStyleUtils';
+import BaseReportActionContextMenu from '@pages/home/report/ContextMenu/BaseReportActionContextMenu';
+import CONST from '@src/CONST';
+import type MiniReportActionContextMenuProps from './types';
+
+function MiniReportActionContextMenu({displayAsGroup = false, ...rest}: MiniReportActionContextMenuProps) {
+ const StyleUtils = useStyleUtils();
+
+ return (
+
+
+
+ );
+}
+
+MiniReportActionContextMenu.displayName = 'MiniReportActionContextMenu';
+
+export default MiniReportActionContextMenu;
diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
new file mode 100644
index 000000000000..98b38dcb6968
--- /dev/null
+++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts
@@ -0,0 +1,8 @@
+import type {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu';
+
+type MiniReportActionContextMenuProps = Omit & {
+ /** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */
+ displayAsGroup?: boolean;
+};
+
+export default MiniReportActionContextMenuProps;
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
similarity index 65%
rename from src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
rename to src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index 1c93c3bc90c7..46b783bca3f9 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -1,23 +1,45 @@
+import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
+import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native';
import {Dimensions} from 'react-native';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
import useLocalize from '@hooks/useLocalize';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as IOU from '@userActions/IOU';
import * as Report from '@userActions/Report';
+import CONST from '@src/CONST';
+import type {ReportAction} from '@src/types/onyx';
import BaseReportActionContextMenu from './BaseReportActionContextMenu';
+import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu';
-function PopoverReportActionContextMenu(_props, ref) {
+type ContextMenuAnchorCallback = (x: number, y: number) => void;
+
+type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void};
+
+type Location = {
+ x: number;
+ y: number;
+};
+
+function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent {
+ if ('nativeEvent' in event) {
+ return event.nativeEvent;
+ }
+ return event;
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef) {
const {translate} = useLocalize();
const reportIDRef = useRef('0');
- const typeRef = useRef(undefined);
- const reportActionRef = useRef({});
+ const typeRef = useRef();
+ const reportActionRef = useRef>(null);
const reportActionIDRef = useRef('0');
const originalReportIDRef = useRef('0');
const selectionRef = useRef('');
- const reportActionDraftMessageRef = useRef(undefined);
+ const reportActionDraftMessageRef = useRef();
const cursorRelativePosition = useRef({
horizontal: 0,
@@ -41,11 +63,11 @@ function PopoverReportActionContextMenu(_props, ref) {
const [isChatPinned, setIsChatPinned] = useState(false);
const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
- const contentRef = useRef(null);
- const anchorRef = useRef(null);
- const dimensionsEventListener = useRef(null);
- const contextMenuAnchorRef = useRef(null);
- const contextMenuTargetNode = useRef(null);
+ const contentRef = useRef(null);
+ const anchorRef = useRef(null);
+ const dimensionsEventListener = useRef(null);
+ const contextMenuAnchorRef = useRef(null);
+ const contextMenuTargetNode = useRef(null);
const onPopoverShow = useRef(() => {});
const onPopoverHide = useRef(() => {});
@@ -55,16 +77,11 @@ function PopoverReportActionContextMenu(_props, ref) {
const onPopoverHideActionCallback = useRef(() => {});
const callbackWhenDeleteModalHide = useRef(() => {});
- /**
- * Get the Context menu anchor position
- * We calculate the achor coordinates from measureInWindow async method
- *
- * @returns {Promise
)}
+ {/**
+ These are the actionable buttons that appear at the bottom of a Concierge message
+ for example: Invite a user mentioned but not a member of the room
+ https://github.com/Expensify/App/issues/32741
+ */}
+ {actionableItemButtons.length > 0 && (
+
+ )}
) : (
@@ -683,12 +715,14 @@ function ReportActionItem(props) {
(report && 'policyID' in report ? `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID}` : undefined),
+ initialValue: [],
+ },
emojiReactions: {
key: ({action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${action.reportActionID}`,
initialValue: {},
@@ -807,6 +845,8 @@ export default compose(
prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine &&
lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) &&
lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) &&
- prevProps.linkedReportActionID === nextProps.linkedReportActionID,
+ prevProps.linkedReportActionID === nextProps.linkedReportActionID &&
+ _.isEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
+ _.isEqual(prevProps.report.reportFields, nextProps.report.reportFields),
),
);
diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx
index f0097a6dce26..35141a42b726 100644
--- a/src/pages/home/report/ReportActionItemBasicMessage.tsx
+++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx
@@ -1,3 +1,4 @@
+import Str from 'expensify-common/lib/str';
import React from 'react';
import {View} from 'react-native';
import Text from '@components/Text';
@@ -12,7 +13,7 @@ function ReportActionItemBasicMessage({message, children}: ReportActionItemBasic
const styles = useThemeStyles();
return (
- {message}
+ {Str.htmlDecode(message)}
{children}
);
diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx
index 3a71ee8356b3..6fc70c78e605 100644
--- a/src/pages/home/report/ReportActionItemMessage.tsx
+++ b/src/pages/home/report/ReportActionItemMessage.tsx
@@ -1,7 +1,8 @@
import type {ReactElement} from 'react';
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js
index c11200ccc4db..d1a294881eb9 100644
--- a/src/pages/home/report/ReportActionItemParentAction.js
+++ b/src/pages/home/report/ReportActionItemParentAction.js
@@ -24,6 +24,9 @@ const propTypes = {
/** The id of the report */
reportID: PropTypes.string.isRequired,
+ /** Position index of the report parent action in the overall report FlatList view */
+ index: PropTypes.number.isRequired,
+
/** The id of the parent report */
// eslint-disable-next-line react/no-unused-prop-types
parentReportID: PropTypes.string.isRequired,
@@ -72,7 +75,7 @@ function ReportActionItemParentAction(props) {
displayAsGroup={false}
isMostRecentIOUReportAction={false}
shouldDisplayNewMarker={props.shouldDisplayNewMarker}
- index={0}
+ index={props.index}
/>
)}
diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx
index e38021cf6ec1..f7c7e5fcf91d 100644
--- a/src/pages/home/report/ReportActionItemThread.tsx
+++ b/src/pages/home/report/ReportActionItemThread.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import MultipleAvatars from '@components/MultipleAvatars';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Report from '@userActions/Report';
diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js
index dba8ef2e11d0..8d79e7af8dd4 100644
--- a/src/pages/home/report/ReportActionsList.js
+++ b/src/pages/home/report/ReportActionsList.js
@@ -343,7 +343,8 @@ function ReportActionsList({
const initialNumToRender = useMemo(() => {
const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight;
const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight);
- return Math.ceil(availableHeight / minimumReportActionHeight);
+ const itemsToRender = Math.ceil(availableHeight / minimumReportActionHeight);
+ return itemsToRender > 0 ? itemsToRender : undefined;
}, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight]);
/**
diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js
index ba47e804de06..a9ae2b4c73b9 100644
--- a/src/pages/home/report/ReportActionsListItemRenderer.js
+++ b/src/pages/home/report/ReportActionsListItemRenderer.js
@@ -61,6 +61,7 @@ function ReportActionsListItemRenderer({
reportID={report.reportID}
parentReportID={`${report.parentReportID}`}
shouldDisplayNewMarker={shouldDisplayNewMarker}
+ index={index}
/>
) : (
;
+
+function ReportAttachments({route}: ReportAttachmentsProps) {
+ const reportID = route.params.reportID;
const report = ReportUtils.getReport(reportID);
// In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource
- const decodedSource = decodeURI(_.get(props, ['route', 'params', 'source']));
+ const decodedSource = decodeURI(route.params.source);
const source = Number(decodedSource) || decodedSource;
const onCarouselAttachmentChange = useCallback(
- (attachment) => {
- const route = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, attachment.source);
- Navigation.navigate(route);
+ (attachment: Attachment) => {
+ const routeToNavigate = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, attachment.source);
+ Navigation.navigate(routeToNavigate);
},
[reportID],
);
return (
;
-}
-
-MoneyRequestEditWaypointPage.displayName = 'MoneyRequestEditWaypointPage';
-MoneyRequestEditWaypointPage.propTypes = propTypes;
-MoneyRequestEditWaypointPage.defaultProps = defaultProps;
-export default MoneyRequestEditWaypointPage;
diff --git a/src/pages/iou/NewDistanceRequestWaypointEditorPage.js b/src/pages/iou/MoneyRequestWaypointPage.js
similarity index 77%
rename from src/pages/iou/NewDistanceRequestWaypointEditorPage.js
rename to src/pages/iou/MoneyRequestWaypointPage.js
index 269cde577040..2f8b8b9cc729 100644
--- a/src/pages/iou/NewDistanceRequestWaypointEditorPage.js
+++ b/src/pages/iou/MoneyRequestWaypointPage.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
-import WaypointEditor from './WaypointEditor';
+import IOURequestStepWaypoint from './request/step/IOURequestStepWaypoint';
const propTypes = {
/** The transactionID of this request */
@@ -32,9 +32,9 @@ const defaultProps = {
// This component is responsible for grabbing the transactionID from the IOU key
// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that WaypointEditor can subscribe to the transaction.
-function NewDistanceRequestWaypointEditorPage({transactionID, route}) {
+function MoneyRequestWaypointPage({transactionID, route}) {
return (
- iou && iou.transactionID},
-})(NewDistanceRequestWaypointEditorPage);
+})(MoneyRequestWaypointPage);
diff --git a/src/pages/iou/ReceiptDropUI.js b/src/pages/iou/ReceiptDropUI.js
index f620f42514ff..0f7226668a80 100644
--- a/src/pages/iou/ReceiptDropUI.js
+++ b/src/pages/iou/ReceiptDropUI.js
@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import ReceiptUpload from '@assets/images/receipt-upload.svg';
import DragAndDropConsumer from '@components/DragAndDrop/Consumer';
import ImageSVG from '@components/ImageSVG';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js
deleted file mode 100644
index ab8874091152..000000000000
--- a/src/pages/iou/WaypointEditor.js
+++ /dev/null
@@ -1,292 +0,0 @@
-import {useNavigation} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useMemo, useRef, useState} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import AddressSearch from '@components/AddressSearch';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import ConfirmModal from '@components/ConfirmModal';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import * as Expensicons from '@components/Icon/Expensicons';
-import ScreenWrapper from '@components/ScreenWrapper';
-import transactionPropTypes from '@components/transactionPropTypes';
-import useLocalize from '@hooks/useLocalize';
-import useLocationBias from '@hooks/useLocationBias';
-import useNetwork from '@hooks/useNetwork';
-import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
-import * as ErrorUtils from '@libs/ErrorUtils';
-import Navigation from '@libs/Navigation/Navigation';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import * as Transaction from '@userActions/Transaction';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-
-const propTypes = {
- /** Route params */
- route: PropTypes.shape({
- params: PropTypes.shape({
- /** IOU type */
- iouType: PropTypes.string,
-
- /** Thread reportID */
- threadReportID: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- /** ID of the transaction being edited */
- transactionID: PropTypes.string,
-
- /** Index of the waypoint being edited */
- waypointIndex: PropTypes.string,
- }),
- }),
-
- /* Current location coordinates of the user */
- userLocation: PropTypes.shape({
- /** Latitude of the location */
- latitude: PropTypes.number,
-
- /** Longitude of the location */
- longitude: PropTypes.number,
- }),
-
- recentWaypoints: PropTypes.arrayOf(
- PropTypes.shape({
- /** The name of the location */
- name: PropTypes.string,
-
- /** A description of the location (usually the address) */
- description: PropTypes.string,
-
- /** Data required by the google auto complete plugin to know where to put the markers on the map */
- geometry: PropTypes.shape({
- /** Data about the location */
- location: PropTypes.shape({
- /** Latitude of the location */
- lat: PropTypes.number,
-
- /** Longitude of the location */
- lng: PropTypes.number,
- }),
- }),
- }),
- ),
-
- /* Onyx props */
- /** The optimistic transaction for this request */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- route: {},
- recentWaypoints: [],
- transaction: {},
- userLocation: undefined,
-};
-
-function WaypointEditor({route: {params: {iouType = '', transactionID = '', waypointIndex = '', threadReportID = 0}} = {}, transaction, recentWaypoints, userLocation}) {
- const styles = useThemeStyles();
- const {windowWidth} = useWindowDimensions();
- const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false);
- const navigation = useNavigation();
- const isFocused = navigation.isFocused();
- const {translate} = useLocalize();
- const {isOffline} = useNetwork();
- const textInput = useRef(null);
- const parsedWaypointIndex = parseInt(waypointIndex, 10);
- const allWaypoints = lodashGet(transaction, 'comment.waypoints', {});
- const currentWaypoint = lodashGet(allWaypoints, `waypoint${waypointIndex}`, {});
-
- const waypointCount = _.size(allWaypoints);
- const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint)));
- const locationBias = useLocationBias(allWaypoints, userLocation);
- const waypointDescriptionKey = useMemo(() => {
- switch (parsedWaypointIndex) {
- case 0:
- return 'distance.waypointDescription.start';
- case waypointCount - 1:
- return 'distance.waypointDescription.finish';
- default:
- return 'distance.waypointDescription.stop';
- }
- }, [parsedWaypointIndex, waypointCount]);
-
- const waypointAddress = lodashGet(currentWaypoint, 'address', '');
- const isEditingWaypoint = Boolean(threadReportID);
- // Hide the menu when there is only start and finish waypoint
- const shouldShowThreeDotsButton = waypointCount > 2;
- const shouldDisableEditor =
- isFocused &&
- (Number.isNaN(parsedWaypointIndex) || parsedWaypointIndex < 0 || parsedWaypointIndex > waypointCount || (filledWaypointCount < 2 && parsedWaypointIndex >= waypointCount));
-
- const validate = (values) => {
- const errors = {};
- const waypointValue = values[`waypoint${waypointIndex}`] || '';
- if (isOffline && waypointValue !== '' && !ValidationUtils.isValidAddress(waypointValue)) {
- ErrorUtils.addErrorMessage(errors, `waypoint${waypointIndex}`, 'bankAccount.error.address');
- }
-
- // If the user is online, and they are trying to save a value without using the autocomplete, show an error message instructing them to use a selected address instead.
- // That enables us to save the address with coordinates when it is selected
- if (!isOffline && waypointValue !== '' && waypointAddress !== waypointValue) {
- ErrorUtils.addErrorMessage(errors, `waypoint${waypointIndex}`, 'distance.errors.selectSuggestedAddress');
- }
-
- return errors;
- };
-
- const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, waypointIndex, waypoint, isEditingWaypoint);
-
- const submit = (values) => {
- const waypointValue = values[`waypoint${waypointIndex}`] || '';
-
- // Allows letting you set a waypoint to an empty value
- if (waypointValue === '') {
- Transaction.removeWaypoint(transaction, waypointIndex);
- }
-
- // While the user is offline, the auto-complete address search will not work
- // Therefore, we're going to save the waypoint as just the address, and the lat/long will be filled in on the backend
- if (isOffline && waypointValue) {
- const waypoint = {
- lat: null,
- lng: null,
- address: waypointValue,
- name: null,
- };
- saveWaypoint(waypoint);
- }
-
- // Other flows will be handled by selecting a waypoint with selectWaypoint as this is mainly for the offline flow
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- };
-
- const deleteStopAndHideModal = () => {
- Transaction.removeWaypoint(transaction, waypointIndex);
- setIsDeleteStopModalOpen(false);
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- };
-
- const selectWaypoint = (values) => {
- const waypoint = {
- lat: values.lat,
- lng: values.lng,
- address: values.address,
- name: values.name || null,
- };
- saveWaypoint(waypoint);
-
- if (isEditingWaypoint) {
- Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(threadReportID));
- return;
- }
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- };
-
- return (
- textInput.current && textInput.current.focus()}
- shouldEnableMaxHeight
- testID={WaypointEditor.displayName}
- >
-
- {
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- }}
- shouldShowThreeDotsButton={shouldShowThreeDotsButton}
- threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)}
- threeDotsMenuItems={[
- {
- icon: Expensicons.Trashcan,
- text: translate('distance.deleteWaypoint'),
- onSelected: () => setIsDeleteStopModalOpen(true),
- },
- ]}
- />
- setIsDeleteStopModalOpen(false)}
- prompt={translate('distance.deleteWaypointConfirmation')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- />
-
- (textInput.current = e)}
- hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''}
- containerStyles={[styles.mt3]}
- label={translate('distance.address')}
- defaultValue={waypointAddress}
- onPress={selectWaypoint}
- maxInputLength={CONST.FORM_CHARACTER_LIMIT}
- renamedInputKeys={{
- address: `waypoint${waypointIndex}`,
- city: null,
- country: null,
- street: null,
- street2: null,
- zipCode: null,
- lat: null,
- lng: null,
- state: null,
- }}
- predefinedPlaces={recentWaypoints}
- resultTypes=""
- />
-
-
-
- );
-}
-
-WaypointEditor.displayName = 'WaypointEditor';
-WaypointEditor.propTypes = propTypes;
-WaypointEditor.defaultProps = defaultProps;
-export default withOnyx({
- userLocation: {
- key: ONYXKEYS.USER_LOCATION,
- },
- transaction: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(route, 'params.transactionID')}`,
- },
- recentWaypoints: {
- key: ONYXKEYS.NVP_RECENT_WAYPOINTS,
-
- // Only grab the most recent 5 waypoints because that's all that is shown in the UI. This also puts them into the format of data
- // that the google autocomplete component expects for it's "predefined places" feature.
- selector: (waypoints) =>
- _.map(waypoints ? waypoints.slice(0, 5) : [], (waypoint) => ({
- name: waypoint.name,
- description: waypoint.address,
- geometry: {
- location: {
- lat: waypoint.lat,
- lng: waypoint.lng,
- },
- },
- })),
- },
-})(WaypointEditor);
diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
index 68e9f68a2bc5..0949081435c4 100644
--- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
+++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
@@ -8,6 +8,7 @@ import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
import {usePersonalDetails} from '@components/OnyxProvider';
import {PressableWithFeedback} from '@components/Pressable';
+import ReferralProgramCTA from '@components/ReferralProgramCTA';
import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import useLocalize from '@hooks/useLocalize';
@@ -16,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as Report from '@libs/actions/Report';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
-import MoneyRequestReferralProgramCTA from '@pages/iou/MoneyRequestReferralProgramCTA';
import reportPropTypes from '@pages/reportPropTypes';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -120,7 +120,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
// We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now.
// This functionality is being built here: https://github.com/Expensify/App/issues/23291
iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
- true,
+ false,
);
const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(
@@ -136,7 +136,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
indexOffset = formatResults.newIndexOffset;
if (maxParticipantsReached) {
- return newSections;
+ return [newSections, {}];
}
newSections.push({
@@ -229,13 +229,13 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
const headerMessage = useMemo(
() =>
OptionsListUtils.getHeaderMessage(
- newChatOptions.personalDetails.length + newChatOptions.recentReports.length !== 0,
+ _.get(newChatOptions, 'personalDetails.length', 0) + _.get(newChatOptions, 'recentReports.length', 0) !== 0,
Boolean(newChatOptions.userToInvite),
searchTerm.trim(),
maxParticipantsReached,
_.some(participants, (participant) => participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase())),
),
- [maxParticipantsReached, newChatOptions.personalDetails.length, newChatOptions.recentReports.length, newChatOptions.userToInvite, participants, searchTerm],
+ [maxParticipantsReached, newChatOptions, participants, searchTerm],
);
// When search term updates we will fetch any reports
@@ -266,7 +266,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
() => (
-
+
{shouldShowSplitBillErrorMessage && (
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index bbe703e50d18..9df2564ae38d 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -217,7 +217,6 @@ function IOURequestStepConfirmation({
// If we have a receipt let's start the split bill by creating only the action, the transaction, and the group DM if needed
if (iouType === CONST.IOU.TYPE.SPLIT && receiptFile) {
- const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID) ? reportID : '';
IOU.startSplitBill(
selectedParticipants,
currentUserPersonalDetails.login,
@@ -226,7 +225,7 @@ function IOURequestStepConfirmation({
transaction.category,
transaction.tag,
receiptFile,
- existingSplitChatReportID,
+ report.reportID,
);
return;
}
@@ -277,7 +276,7 @@ function IOURequestStepConfirmation({
requestMoney(selectedParticipants, trimmedComment);
},
- [iouType, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, reportID, requestType, createDistanceRequest, requestMoney, receiptFile],
+ [iouType, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, requestType, createDistanceRequest, requestMoney, receiptFile],
);
/**
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js
index ddf692fedd46..9549a93c8124 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.js
+++ b/src/pages/iou/request/step/IOURequestStepDistance.js
@@ -21,6 +21,7 @@ import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import * as MapboxToken from '@userActions/MapboxToken';
import * as Transaction from '@userActions/Transaction';
+import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
import StepScreenWrapper from './StepScreenWrapper';
@@ -102,7 +103,9 @@ function IOURequestStepDistance({
* @param {Number} index of the waypoint to edit
*/
const navigateToWaypointEditPage = (index) => {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(iouType, transactionID, reportID, index));
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.REQUEST, transactionID, report.reportID, index, Navigation.getActiveRouteWithoutParams()),
+ );
};
const navigateToNextStep = useCallback(() => {
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js
index c0c96826d124..7c6efca4a32f 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import React, {useCallback, useContext, useReducer, useRef, useState} from 'react';
-import {ActivityIndicator, PanResponder, PixelRatio, Text, View} from 'react-native';
+import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native';
import Hand from '@assets/images/hand.svg';
import ReceiptUpload from '@assets/images/receipt-upload.svg';
import Shutter from '@assets/images/shutter.svg';
@@ -12,6 +12,7 @@ import {DragAndDropContext} from '@components/DragAndDrop/Provider';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
@@ -135,13 +136,9 @@ function IOURequestStepScan({
const updateScanAndNavigate = useCallback(
(file, source) => {
IOU.replaceReceipt(transactionID, file, source);
- if (backTo) {
- Navigation.goBack(backTo);
- return;
- }
Navigation.dismissModal();
},
- [backTo, transactionID],
+ [transactionID],
);
/**
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js
index 38bcd16faf39..23e30ce25711 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {ActivityIndicator, Alert, AppState, Text, View} from 'react-native';
+import {ActivityIndicator, Alert, AppState, View} from 'react-native';
import {RESULTS} from 'react-native-permissions';
import {useCameraDevices} from 'react-native-vision-camera';
import Hand from '@assets/images/hand.svg';
@@ -11,6 +11,7 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js
index 09617026576d..1087018eeed9 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.js
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js
@@ -81,7 +81,7 @@ const defaultProps = {
function IOURequestStepWaypoint({
recentWaypoints,
route: {
- params: {iouType, pageIndex, reportID, transactionID},
+ params: {action, backTo, iouType, pageIndex, reportID, transactionID},
},
transaction,
userLocation,
@@ -135,7 +135,7 @@ function IOURequestStepWaypoint({
return errors;
};
- const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, pageIndex, waypoint, false);
+ const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE);
const submit = (values) => {
const waypointValue = values[`waypoint${pageIndex}`] || '';
@@ -180,7 +180,11 @@ function IOURequestStepWaypoint({
address: values.address,
name: values.name || null,
};
- Transaction.saveWaypoint(transactionID, pageIndex, waypoint, false);
+ Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE);
+ if (backTo) {
+ Navigation.goBack(backTo);
+ return;
+ }
Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(iouType, transactionID, reportID));
};
diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.js b/src/pages/iou/request/step/withFullTransactionOrNotFound.js
index 001159f944e9..7cdbb3484999 100644
--- a/src/pages/iou/request/step/withFullTransactionOrNotFound.js
+++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.js
@@ -70,7 +70,8 @@ export default function (WrappedComponent) {
transaction: {
key: ({route}) => {
const transactionID = lodashGet(route, 'params.transactionID', 0);
- return `${transactionID === CONST.IOU.OPTIMISTIC_TRANSACTION_ID ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
+ const userAction = lodashGet(route, 'params.action', CONST.IOU.ACTION.CREATE);
+ return `${userAction === CONST.IOU.ACTION.CREATE ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
})(WithFullTransactionOrNotFoundWithRef);
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 9edede770233..9567b17ecdf5 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -8,6 +8,7 @@ import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
import {usePersonalDetails} from '@components/OnyxProvider';
import {PressableWithFeedback} from '@components/Pressable';
+import ReferralProgramCTA from '@components/ReferralProgramCTA';
import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import useLocalize from '@hooks/useLocalize';
@@ -16,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as Report from '@libs/actions/Report';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as OptionsListUtils from '@libs/OptionsListUtils';
-import MoneyRequestReferralProgramCTA from '@pages/iou/MoneyRequestReferralProgramCTA';
import reportPropTypes from '@pages/reportPropTypes';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -285,7 +285,7 @@ function MoneyRequestParticipantsSelector({
() => (
-
+
{shouldShowSplitBillErrorMessage && (
diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js
index 329bf66f7275..3a056ee7c0a3 100644
--- a/src/pages/reportPropTypes.js
+++ b/src/pages/reportPropTypes.js
@@ -63,11 +63,14 @@ export default PropTypes.shape({
stateNum: PropTypes.oneOf(_.values(CONST.REPORT.STATE_NUM)),
/** The status of the current report */
- statusNum: PropTypes.oneOf(_.values(CONST.REPORT.STATUS)),
+ statusNum: PropTypes.oneOf(_.values(CONST.REPORT.STATUS_NUM)),
/** Which user role is capable of posting messages on the report */
writeCapability: PropTypes.oneOf(_.values(CONST.REPORT.WRITE_CAPABILITIES)),
/** Field-specific pending states for offline UI status */
pendingFields: PropTypes.objectOf(PropTypes.string),
+
+ /** Custom fields attached to the report */
+ reportFields: PropTypes.objectOf(PropTypes.string),
});
diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js
index 81186af3fcd1..a460c95cdfe6 100644
--- a/src/pages/settings/AboutPage/AboutPage.js
+++ b/src/pages/settings/AboutPage/AboutPage.js
@@ -1,17 +1,16 @@
-import React, {useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import React, {useCallback, useMemo, useRef} from 'react';
+import {View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import _ from 'underscore';
-import Logo from '@assets/images/new-expensify.svg';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
-import ImageSVG from '@components/ImageSVG';
+import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout';
+import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
-import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import compose from '@libs/compose';
@@ -22,6 +21,7 @@ import * as Link from '@userActions/Link';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import pkg from '../../../../package.json';
const propTypes = {
@@ -41,6 +41,7 @@ function getFlavor() {
}
function AboutPage(props) {
+ const theme = useTheme();
const styles = useThemeStyles();
const {translate} = props;
const popoverAnchor = useRef(null);
@@ -95,64 +96,61 @@ function AboutPage(props) {
}));
}, [translate, waitForNavigate]);
+ const overlayContent = useCallback(
+ () => (
+
+
+ v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`}
+
+
+ ),
+ // disabling this rule, as we want this to run only on the first render
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
return (
- Navigation.goBack(ROUTES.SETTINGS)}
+ illustration={LottieAnimations.Coin}
+ backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.ABOUT].backgroundColor}
+ overlayContent={overlayContent}
>
- {({safeAreaPaddingBottomStyle}) => (
- <>
- Navigation.goBack(ROUTES.SETTINGS)}
- />
-
-
-
-
-
-
- v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`}
-
- {props.translate('initialSettingsPage.aboutPage.description')}
-
-
-
-
-
-
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '}
-
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')}
- {' '}
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '}
-
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')}
-
- .
-
-
-
- >
- )}
-
+
+ {props.translate('footer.aboutExpensify')}
+ {props.translate('initialSettingsPage.aboutPage.description')}
+
+
+
+
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '}
+
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')}
+ {' '}
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '}
+
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')}
+
+ .
+
+
+
);
}
diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js
index d2b91ed6b76b..6e310b9a62bd 100755
--- a/src/pages/settings/InitialSettingsPage.js
+++ b/src/pages/settings/InitialSettingsPage.js
@@ -267,11 +267,11 @@ function InitialSettingsPage(props) {
translationKey: 'initialSettingsPage.goToExpensifyClassic',
icon: Expensicons.NewExpensify,
action: () => {
- Link.openExternalLink(CONST.EXPENSIFY_INBOX_URL);
+ Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX);
},
shouldShowRightIcon: true,
iconRight: Expensicons.NewWindow,
- link: CONST.EXPENSIFY_INBOX_URL,
+ link: Link.buildOldDotURL(CONST.OLDDOT_URLS.INBOX),
},
{
translationKey: 'initialSettingsPage.signOut',
diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js
index 2586be9fb673..8280d9b5c604 100644
--- a/src/pages/settings/Profile/TimezoneSelectPage.js
+++ b/src/pages/settings/Profile/TimezoneSelectPage.js
@@ -96,6 +96,7 @@ function TimezoneSelectPage(props) {
sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]}
initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')}
showScrollIndicator
+ shouldShowTooltips={false}
/>
);
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js
index 27f9c0f04404..59c145f9e348 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js
@@ -1,9 +1,10 @@
import React, {useState} from 'react';
-import {ScrollView, Text, View} from 'react-native';
+import {ScrollView, View} from 'react-native';
import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import Section from '@components/Section';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js
index f1c3fbe90533..da77d1fa6a15 100644
--- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js
+++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js
@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef} from 'react';
-import {Text} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import FormProvider from '@components/Form/FormProvider';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import * as FormActions from '@libs/actions/FormActions';
import * as Wallet from '@libs/actions/Wallet';
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js
index 3c44f806fdb8..856c0613cec7 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage.js
+++ b/src/pages/settings/Wallet/ExpensifyCardPage.js
@@ -208,7 +208,7 @@ function ExpensifyCardPage({
medium
style={[styles.mh5, styles.mb5]}
text={translate('cardPage.reviewTransaction')}
- onPress={() => Link.openOldDotLink('inbox')}
+ onPress={() => Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX)}
/>
>
) : null}
diff --git a/src/pages/settings/Wallet/ReportCardLostPage.js b/src/pages/settings/Wallet/ReportCardLostPage.js
index b01dc99cb485..49b69188c377 100644
--- a/src/pages/settings/Wallet/ReportCardLostPage.js
+++ b/src/pages/settings/Wallet/ReportCardLostPage.js
@@ -23,14 +23,19 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import assignedCardPropTypes from './assignedCardPropTypes';
+const OPTIONS_KEYS = {
+ DAMAGED: 'damaged',
+ STOLEN: 'stolen',
+};
+
/** Options for reason selector */
const OPTIONS = [
{
- key: 'damaged',
+ key: OPTIONS_KEYS.DAMAGED,
label: 'reportCardLostOrDamaged.cardDamaged',
},
{
- key: 'stolen',
+ key: OPTIONS_KEYS.STOLEN,
label: 'reportCardLostOrDamaged.cardLostOrStolen',
},
];
@@ -107,7 +112,7 @@ function ReportCardLostPage({
return;
}
- Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARDS.getRoute(domain));
+ Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain));
}, [domain, formData.isLoading, prevIsLoading, physicalCard.errors]);
useEffect(() => {
@@ -156,6 +161,8 @@ function ReportCardLostPage({
Navigation.goBack(ROUTES.SETTINGS_WALLET);
};
+ const isDamaged = reason && reason.key === OPTIONS_KEYS.DAMAGED;
+
return (
Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS)}
numberOfLinesTitle={2}
/>
- {translate('reportCardLostOrDamaged.currentCardInfo')}
+ {isDamaged ? (
+ {translate('reportCardLostOrDamaged.cardDamagedInfo')}
+ ) : (
+ {translate('reportCardLostOrDamaged.cardLostOrStolenInfo')}
+ )}
>
) : (
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js
index bf547bc4bd10..8382014a01e5 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.js
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js
@@ -556,6 +556,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod
}}
onItemSelected={(method) => addPaymentMethodTypePressed(method)}
anchorRef={addPaymentMethodAnchorRef}
+ shouldShowPersonalBankAccountOption
/>
>
);
diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js
index a4fc61910be2..1a526a9cdd9b 100644
--- a/src/pages/tasks/TaskAssigneeSelectorModal.js
+++ b/src/pages/tasks/TaskAssigneeSelectorModal.js
@@ -184,29 +184,29 @@ function TaskAssigneeSelectorModal(props) {
return sectionsList;
}, [filteredCurrentUserOption, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, props]);
- const selectReport = (option) => {
- if (!option) {
- return;
- }
-
- // Check to see if we're creating a new task
- // If there's no route params, we're creating a new task
- if (!props.route.params && option.accountID) {
- Task.setAssigneeValue(option.login, option.accountID, props.task.shareDestination, OptionsListUtils.isCurrentUser(option));
- return Navigation.goBack(ROUTES.NEW_TASK);
- }
-
- // Check to see if we're editing a task and if so, update the assignee
- if (report) {
- if (option.accountID !== report.managerID) {
- const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, props.route.params.reportID, OptionsListUtils.isCurrentUser(option));
+ const selectReport = useCallback(
+ (option) => {
+ if (!option) {
+ return;
+ }
- // Pass through the selected assignee
- Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport);
+ // Check to see if we're editing a task and if so, update the assignee
+ if (report) {
+ if (option.accountID !== report.managerID) {
+ const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option));
+
+ // Pass through the selected assignee
+ Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport);
+ }
+ Navigation.dismissModal(report.reportID);
+ // If there's no report, we're creating a new task
+ } else if (option.accountID) {
+ Task.setAssigneeValue(option.login, option.accountID, props.task.shareDestination, OptionsListUtils.isCurrentUser(option));
+ Navigation.goBack(ROUTES.NEW_TASK);
}
- return Navigation.dismissModal(report.reportID);
- }
- };
+ },
+ [props.session.accountID, props.task.shareDestination, report],
+ );
const isOpen = ReportUtils.isOpenTaskReport(report);
const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID, lodashGet(props.rootParentReportPolicy, 'role', ''));
diff --git a/src/pages/wallet/WalletStatementPage.js b/src/pages/wallet/WalletStatementPage.js
index e79a2add5213..e97bb33519a0 100644
--- a/src/pages/wallet/WalletStatementPage.js
+++ b/src/pages/wallet/WalletStatementPage.js
@@ -11,6 +11,7 @@ import {withNetwork} from '@components/OnyxProvider';
import ScreenWrapper from '@components/ScreenWrapper';
import WalletStatementModal from '@components/WalletStatementModal';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useThemePreference from '@hooks/useThemePreference';
import compose from '@libs/compose';
import DateUtils from '@libs/DateUtils';
import fileDownload from '@libs/fileDownload';
@@ -53,6 +54,7 @@ const defaultProps = {
};
function WalletStatementPage(props) {
+ const themePreference = useThemePreference();
const yearMonth = lodashGet(props.route.params, 'yearMonth', null);
useEffect(() => {
@@ -89,7 +91,7 @@ function WalletStatementPage(props) {
const month = yearMonth.substring(4) || getMonth(new Date());
const monthName = format(new Date(year, month - 1), CONST.DATE.MONTH_FORMAT);
const title = props.translate('statementPage.title', year, monthName);
- const url = `${CONFIG.EXPENSIFY.EXPENSIFY_URL}statement.php?period=${yearMonth}`;
+ const url = `${CONFIG.EXPENSIFY.EXPENSIFY_URL}statement.php?period=${yearMonth}${themePreference === CONST.THEME.DARK ? '&isDarkMode=true' : ''}`;
return (
{
setSearchTerm(SearchInputManager.searchInput);
- }, []);
+ return () => {
+ Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {});
+ };
+ }, [props.route.params.policyID]);
useEffect(() => {
Policy.clearErrors(props.route.params.policyID);
@@ -105,6 +110,12 @@ function WorkspaceInvitePage(props) {
_.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
const newSelectedOptions = [];
+ _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => {
+ if (!_.has(detailsMap, login)) {
+ return;
+ }
+ newSelectedOptions.push({...detailsMap[login], isSelected: true});
+ });
_.each(selectedOptions, (option) => {
newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
});
@@ -323,5 +334,8 @@ export default compose(
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
+ invitedEmailsToAccountIDsDraft: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`,
+ },
}),
)(WorkspaceInvitePage);
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index fc42782cf562..92bc5ecc8e9c 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -480,6 +480,7 @@ function WorkspaceMembersPage(props) {
SearchInputManager.searchInput = value;
setSearchValue(value);
}}
+ disableKeyboardShortcuts={removeMembersConfirmModalVisible}
headerMessage={getHeaderMessage()}
headerContent={getHeaderContent()}
onSelectRow={(item) => toggleUser(item.accountID)}
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 21c93b87806a..35fab36e5d41 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -77,6 +77,9 @@ const propTypes = {
/** accountID of current user */
accountID: PropTypes.number,
}),
+
+ /** policyID for main workspace */
+ activePolicyID: PropTypes.string,
};
const defaultProps = {
reports: {},
@@ -88,6 +91,7 @@ const defaultProps = {
session: {
accountID: 0,
},
+ activePolicyID: null,
};
function WorkspaceNewRoomPage(props) {
@@ -96,7 +100,7 @@ function WorkspaceNewRoomPage(props) {
const {isOffline} = useNetwork();
const {isSmallScreenWidth} = useWindowDimensions();
const [visibility, setVisibility] = useState(CONST.REPORT.VISIBILITY.RESTRICTED);
- const [policyID, setPolicyID] = useState(null);
+ const [policyID, setPolicyID] = useState(props.activePolicyID);
const [writeCapability, setWriteCapability] = useState(CONST.REPORT.WRITE_CAPABILITIES.ALL);
const wasLoading = usePrevious(props.formState.isLoading);
const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]);
@@ -138,6 +142,13 @@ function WorkspaceNewRoomPage(props) {
Report.clearNewRoomFormError();
}, []);
+ useEffect(() => {
+ if (policyID) {
+ return;
+ }
+ setPolicyID(props.activePolicyID);
+ }, [props.activePolicyID, policyID]);
+
useEffect(() => {
if (!(((wasLoading && !props.formState.isLoading) || (isOffline && props.formState.isLoading)) && _.isEmpty(props.formState.errorFields))) {
return;
@@ -296,6 +307,7 @@ function WorkspaceNewRoomPage(props) {
inputID="policyID"
label={translate('workspace.common.workspace')}
items={workspaceOptions}
+ value={policyID}
onValueChange={setPolicyID}
/>
@@ -320,6 +332,7 @@ function WorkspaceNewRoomPage(props) {
onValueChange={setVisibility}
value={visibility}
furtherDetails={visibilityDescription}
+ shouldShowTooltips={false}
/>
@@ -353,5 +366,10 @@ export default compose(
session: {
key: ONYXKEYS.SESSION,
},
+ activePolicyID: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (account) => (account && account.activePolicyID) || null,
+ initialValue: null,
+ },
}),
)(WorkspaceNewRoomPage);
diff --git a/src/styles/index.ts b/src/styles/index.ts
index df2db6b995df..aace13c34594 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -443,6 +443,10 @@ const styles = (theme: ThemeColors) =>
color: theme.link,
},
+ textIvoryLight: {
+ color: theme.iconColorfulBackground,
+ },
+
textNoWrap: {
...whiteSpace.noWrap,
},
@@ -673,8 +677,11 @@ const styles = (theme: ThemeColors) =>
},
loadingVBAAnimation: {
- width: 140,
- height: 140,
+ width: '100%',
+ },
+
+ loadingVBAAnimationWeb: {
+ width: '100%',
},
pickerSmall: (backgroundColor = theme.highlightBG) =>
@@ -2281,9 +2288,10 @@ const styles = (theme: ThemeColors) =>
},
reportActionContextMenuMiniButton: {
- ...spacing.p1,
- ...spacing.mv1,
- ...spacing.mh1,
+ height: 28,
+ width: 28,
+ ...flex.alignItemsCenter,
+ ...flex.justifyContentCenter,
...{borderRadius: variables.buttonBorderRadius},
},
@@ -3318,8 +3326,8 @@ const styles = (theme: ThemeColors) =>
},
miniQuickEmojiReactionText: {
- fontSize: 15,
- lineHeight: 20,
+ fontSize: 18,
+ lineHeight: 22,
verticalAlign: 'middle',
},
diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts
index 8ac7b0a2359c..4d4234e167ef 100644
--- a/src/styles/theme/themes/dark.ts
+++ b/src/styles/theme/themes/dark.ts
@@ -126,6 +126,10 @@ const darkTheme = {
backgroundColor: colors.productDark200,
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
},
+ [SCREENS.SETTINGS.ABOUT]: {
+ backgroundColor: colors.yellow600,
+ statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
+ },
[SCREENS.RIGHT_MODAL.REFERRAL]: {
backgroundColor: colors.pink800,
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts
index 663b94aa0fc7..9cc5b03ac777 100644
--- a/src/styles/theme/themes/light.ts
+++ b/src/styles/theme/themes/light.ts
@@ -126,6 +126,10 @@ const lightTheme = {
backgroundColor: colors.productLight200,
statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT,
},
+ [SCREENS.SETTINGS.ABOUT]: {
+ backgroundColor: colors.yellow600,
+ statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
+ },
[SCREENS.RIGHT_MODAL.REFERRAL]: {
backgroundColor: colors.pink800,
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
diff --git a/src/styles/utils/generators/ReportActionContextMenuStyleUtils.ts b/src/styles/utils/generators/ReportActionContextMenuStyleUtils.ts
index e45b5db98b53..e8761468f640 100644
--- a/src/styles/utils/generators/ReportActionContextMenuStyleUtils.ts
+++ b/src/styles/utils/generators/ReportActionContextMenuStyleUtils.ts
@@ -12,6 +12,10 @@ const getMiniWrapperStyle = (theme: ThemeColors, styles: ThemeStyles): ViewStyle
styles.flexRow,
getDefaultWrapperStyle(theme),
{
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 4,
+ height: 36,
borderRadius: variables.buttonBorderRadius,
borderWidth: 1,
borderColor: theme.border,
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 08a89526e4c3..b11d48898af5 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -70,6 +70,7 @@ export default {
iconSizeXXSmall: 8,
iconSizeExtraSmall: 12,
iconSizeSmall: 16,
+ iconSizeMedium: 18,
iconSizeNormal: 20,
iconSizeLarge: 24,
iconSizeXLarge: 28,
diff --git a/src/types/modules/react-native-google-places-autocomplete.d.ts b/src/types/modules/react-native-google-places-autocomplete.d.ts
new file mode 100644
index 000000000000..442c941ed9cd
--- /dev/null
+++ b/src/types/modules/react-native-google-places-autocomplete.d.ts
@@ -0,0 +1,15 @@
+import type {ViewProps} from 'react-native';
+import type {GooglePlacesAutocompleteProps as BaseGooglePlacesAutocompleteProps, Term} from 'react-native-google-places-autocomplete';
+
+declare module 'react-native-google-places-autocomplete' {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface GooglePlacesAutocompleteProps extends ViewProps, BaseGooglePlacesAutocompleteProps {}
+
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface GooglePlaceData {
+ isPredefinedPlace: string;
+ name: string;
+ description: string;
+ terms?: Term[];
+ }
+}
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index c4e30157bf6f..f00fd8c4c972 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -19,6 +19,7 @@ type OriginalMessageActionName =
| 'TASKCOMPLETED'
| 'TASKEDITED'
| 'TASKREOPENED'
+ | 'ACTIONABLEMENTIONWHISPER'
| ValueOf;
type OriginalMessageApproved = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.APPROVED;
@@ -109,6 +110,18 @@ type OriginalMessageAddComment = {
};
};
+type OriginalMessageActionableMentionWhisper = {
+ actionName: typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER;
+ originalMessage: {
+ inviteeAccountIDs: number[];
+ inviteeEmails: string;
+ lastModified: string;
+ reportID: number;
+ resolution?: ValueOf | null;
+ whisperedTo?: number[];
+ };
+};
+
type OriginalMessageSubmitted = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.SUBMITTED;
originalMessage: unknown;
@@ -222,7 +235,9 @@ type OriginalMessageReimbursementQueued = {
type OriginalMessageReimbursementDequeued = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED;
- originalMessage: unknown;
+ originalMessage: {
+ expenseReportID: string;
+ };
};
type OriginalMessageMoved = {
@@ -239,6 +254,7 @@ type OriginalMessage =
| OriginalMessageApproved
| OriginalMessageIOU
| OriginalMessageAddComment
+ | OriginalMessageActionableMentionWhisper
| OriginalMessageSubmitted
| OriginalMessageClosed
| OriginalMessageCreated
@@ -266,4 +282,5 @@ export type {
OriginalMessageIOU,
OriginalMessageCreated,
OriginalMessageAddComment,
+ OriginalMessageReimbursementDequeued,
};
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index da4522487a7a..2fd04af328ff 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -5,20 +5,29 @@ import type * as OnyxCommon from './OnyxCommon';
type Unit = 'mi' | 'km';
type Rate = {
- name: string;
- rate: number;
- currency: string;
+ name?: string;
+ rate?: number;
+ currency?: string;
+ customUnitRateID?: string;
+ errors?: OnyxCommon.Errors;
+ pendingAction?: string;
+};
+
+type Attributes = {
+ unit: Unit;
};
type CustomUnit = {
- customUnitID?: string;
- name?: string;
- attributes: {
- unit: Unit;
- };
- rates?: Record;
+ name: string;
+ customUnitID: string;
+ attributes: Attributes;
+ rates: Record;
+ pendingAction?: string;
+ errors?: OnyxCommon.Errors;
};
+type AutoReportingOffset = number | ValueOf;
+
type Policy = {
/** The ID of the policy */
id: string;
@@ -77,13 +86,34 @@ type Policy = {
/** Whether the scheduled submit is enabled */
isHarvestingEnabled?: boolean;
+ /** Whether the scheduled submit is enabled */
+ isPreventSelfApprovalEnabled?: boolean;
+
+ /** When the monthly scheduled submit should happen */
+ autoReportingOffset?: AutoReportingOffset;
+
/** The accountID of manager who the employee submits their expenses to on paid policies */
submitsTo?: number;
/** The employee list of the policy */
employeeList?: [];
+
+ /** Whether to leave the calling account as an admin on the policy */
+ makeMeAdmin?: boolean;
+
+ /** Pending fields for the policy */
+ pendingFields?: Record;
+
+ /** Original file name which is used for the policy avatar */
+ originalFileName?: string;
+
+ /** Alert message for the policy */
+ alertMessage?: string;
+
+ /** Informative messages about which policy members were added with primary logins when invited with their secondary login */
+ primaryLoginsInvited?: Record;
};
export default Policy;
-export type {Unit};
+export type {Unit, CustomUnit};
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index f6af87038d00..b1571e7514e4 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -83,14 +83,11 @@ type Report = {
/** ID of the chat report */
chatReportID?: string;
- /** The state of the report */
- state?: ValueOf;
-
/** The state that the report is currently in */
stateNum?: ValueOf;
/** The status of the current report */
- statusNum?: ValueOf;
+ statusNum?: ValueOf;
/** Which user role is capable of posting messages on the report */
writeCapability?: WriteCapability;
@@ -119,10 +116,12 @@ type Report = {
welcomeMessage?: string;
lastActorAccountID?: number;
ownerAccountID?: number;
+ ownerEmail?: string;
participantAccountIDs?: number[];
visibleChatMemberAccountIDs?: number[];
total?: number;
currency?: string;
+ managerEmail?: string;
parentReportActionIDs?: number[];
errorFields?: OnyxCommon.ErrorFields;
@@ -155,6 +154,8 @@ type Report = {
isHidden?: boolean;
isChatRoom?: boolean;
participantsList?: PersonalDetails[];
+ text?: string;
+ updateReportInLHN?: boolean;
privateNotes?: Record;
isLoadingPrivateNotes?: boolean;
diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts
index d81335b284ac..3dbd4c1e3667 100644
--- a/src/types/onyx/ReportAction.ts
+++ b/src/types/onyx/ReportAction.ts
@@ -53,6 +53,9 @@ type Message = {
/** ID of a task report */
taskReportID?: string;
+
+ /** resolution for actionable mention whisper */
+ resolution?: ValueOf | null;
};
type ImageMetadata = {
@@ -149,7 +152,7 @@ type ReportActionBase = {
childManagerAccountID?: number;
/** The status of the child report */
- childStatusNum?: ValueOf;
+ childStatusNum?: ValueOf;
/** Report action child status name */
childStateNum?: ValueOf;
diff --git a/src/types/onyx/Task.ts b/src/types/onyx/Task.ts
index 804be21d3e1c..2aec786d77ad 100644
--- a/src/types/onyx/Task.ts
+++ b/src/types/onyx/Task.ts
@@ -22,6 +22,9 @@ type Task = {
/** Report id only when a task was created from a report */
parentReportID?: string;
+
+ /** Chat report with assignee of task */
+ assigneeChatReport?: Report;
};
export default Task;
diff --git a/src/types/onyx/WalletTerms.ts b/src/types/onyx/WalletTerms.ts
index b8fcdfeabe3e..f0563310859a 100644
--- a/src/types/onyx/WalletTerms.ts
+++ b/src/types/onyx/WalletTerms.ts
@@ -1,3 +1,5 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
import type * as OnyxCommon from './OnyxCommon';
type WalletTerms = {
@@ -7,8 +9,8 @@ type WalletTerms = {
/** When the user accepts the Wallet's terms in order to pay an IOU, this is the ID of the chatReport the IOU is linked to */
chatReportID?: string;
- /** Source that triggered the KYC wall */
- source?: string;
+ /** The source that triggered the KYC wall */
+ source?: ValueOf;
/** Loading state to provide feedback when we are waiting for a request to finish */
isLoading?: boolean;
diff --git a/src/types/utils/textRef.ts b/src/types/utils/textRef.ts
index 4e80f00b7bc6..668f54d59e36 100644
--- a/src/types/utils/textRef.ts
+++ b/src/types/utils/textRef.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line no-restricted-imports
import type {Text} from 'react-native';
const textRef = (ref: React.RefObject) => ref as React.RefObject;
diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js
index 3ba19199c30a..5b304346f38f 100644
--- a/tests/actions/IOUTest.js
+++ b/tests/actions/IOUTest.js
@@ -1237,9 +1237,8 @@ describe('actions/IOU', () => {
expect(chatReport.pendingFields).toBeFalsy();
expect(iouReport.pendingFields).toBeFalsy();
- // expect(iouReport.status).toBe(CONST.REPORT.STATUS.SUBMITTED);
- // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED);
- // expect(iouReport.state).toBe(CONST.REPORT.STATE.SUBMITTED);
+ // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED);
+ // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED);
resolve();
},
@@ -1306,9 +1305,8 @@ describe('actions/IOU', () => {
expect(chatReport.iouReportID).toBeFalsy();
- // expect(iouReport.status).toBe(CONST.REPORT.STATUS.REIMBURSED);
- // expect(iouReport.state).toBe(CONST.REPORT.STATE.MANUALREIMBURSED);
- // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED);
+ // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED);
+ // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED);
resolve();
},
@@ -1356,9 +1354,8 @@ describe('actions/IOU', () => {
expect(chatReport.iouReportID).toBeFalsy();
- // expect(iouReport.status).toBe(CONST.REPORT.STATUS.REIMBURSED);
- // expect(iouReport.state).toBe(CONST.REPORT.STATE.MANUALREIMBURSED);
- // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED);
+ // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED);
+ // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED);
resolve();
},
@@ -1770,9 +1767,8 @@ describe('actions/IOU', () => {
expect.objectContaining({
lastMessageHtml: `paid $${amount / 100}.00 with Expensify`,
lastMessageText: `paid $${amount / 100}.00 with Expensify`,
- state: CONST.REPORT.STATE.SUBMITTED,
- statusNum: CONST.REPORT.STATUS.REIMBURSED,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
+ statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
}),
);
expect(updatedChatReport).toEqual(
diff --git a/tests/perf-test/OptionsSelector.perf-test.js b/tests/perf-test/OptionsSelector.perf-test.js
index 3959ed87460a..b30169b8b53f 100644
--- a/tests/perf-test/OptionsSelector.perf-test.js
+++ b/tests/perf-test/OptionsSelector.perf-test.js
@@ -82,7 +82,7 @@ test('[OptionsSelector] should render 1 section', () => {
measurePerformance(, {runs});
});
-test('[OptionsSelector] should render mutliple sections', () => {
+test('[OptionsSelector] should render multiple sections', () => {
const sections = generateSections(mutlipleSectionsConfig);
measurePerformance(, {runs});
});
diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts
index ea3b48bacf47..2a96a5959942 100644
--- a/tests/perf-test/ReportActionsUtils.perf-test.ts
+++ b/tests/perf-test/ReportActionsUtils.perf-test.ts
@@ -9,18 +9,6 @@ import createCollection from '../utils/collections/createCollection';
import createRandomReportAction from '../utils/collections/reportActions';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
-beforeAll(() =>
- Onyx.init({
- keys: ONYXKEYS,
- safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
- }),
-);
-
-// Clear out Onyx after each test so that each test starts with a clean slate
-afterEach(() => {
- Onyx.clear();
-});
-
const getMockedReportActionsMap = (reportsLength = 10, actionsPerReportLength = 100) => {
const mockReportActions = Array.from({length: actionsPerReportLength}, (v, i) => {
const reportActionKey = i + 1;
@@ -49,120 +37,113 @@ const reportId = '1';
const runs = CONST.PERFORMANCE_TESTS.RUNS;
-/**
- * This function will be executed 20 times and the average time will be used on the comparison.
- * It will fail based on the CI configuration around Reassure:
- * @see /.github/workflows/reassurePerformanceTests.yml
- *
- * Max deviation on the duration is set to 20% at the time of writing.
- *
- * More on the measureFunction API:
- * @see https://callstack.github.io/reassure/docs/api#measurefunction-function
- */
-test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions', async () => {
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+describe('ReportActionsUtils', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+ });
+
+ Onyx.multiSet({
+ ...mockedReportActionsMap,
+ });
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId), {runs});
-});
+ afterAll(() => {
+ Onyx.clear();
+ });
-test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions with actionsToMerge', async () => {
- const parentReportActionId = '1';
- const fakeParentAction = reportActions[parentReportActionId];
- const actionsToMerge = {
- [parentReportActionId]: {
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
- previousMessage: fakeParentAction.message,
- message: [
- {
- translationKey: '',
- type: 'COMMENT',
- html: '',
- text: '',
- isEdited: true,
- isDeletedParentAction: true,
- },
- ],
- errors: null,
- linkMetaData: [],
- },
- } as unknown as ReportActions;
-
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ /**
+ * This function will be executed 20 times and the average time will be used on the comparison.
+ * It will fail based on the CI configuration around Reassure:
+ * @see /.github/workflows/reassurePerformanceTests.yml
+ *
+ * Max deviation on the duration is set to 20% at the time of writing.
+ *
+ * More on the measureFunction API:
+ * @see https://callstack.github.io/reassure/docs/api#measurefunction-function
+ */
+ test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions', async () => {
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, actionsToMerge), {runs});
-});
-test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => {
- const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions);
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ test('[ReportActionsUtils] getLastVisibleAction on 10k reportActions with actionsToMerge', async () => {
+ const parentReportActionId = '1';
+ const fakeParentAction = reportActions[parentReportActionId];
+ const actionsToMerge = {
+ [parentReportActionId]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ previousMessage: fakeParentAction.message,
+ message: [
+ {
+ translationKey: '',
+ type: 'COMMENT',
+ html: '',
+ text: '',
+ isEdited: true,
+ isDeletedParentAction: true,
+ },
+ ],
+ errors: null,
+ linkMetaData: [],
+ },
+ } as unknown as ReportActions;
+
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, actionsToMerge), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray), {runs});
-});
-test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions', async () => {
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => {
+ const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions);
+
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId), {runs});
-});
-test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions with actionsToMerge', async () => {
- const parentReportActionId = '1';
- const fakeParentAction = reportActions[parentReportActionId];
- const actionsToMerge = {
- [parentReportActionId]: {
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
- previousMessage: fakeParentAction.message,
- message: [
- {
- translationKey: '',
- type: 'COMMENT',
- html: '',
- text: '',
- isEdited: true,
- isDeletedParentAction: true,
- },
- ],
- errors: null,
- linkMetaData: [],
- },
- } as unknown as ReportActions;
-
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions', async () => {
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, actionsToMerge), {runs});
-});
-test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => {
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ test('[ReportActionsUtils] getLastVisibleMessage on 10k ReportActions with actionsToMerge', async () => {
+ const parentReportActionId = '1';
+ const fakeParentAction = reportActions[parentReportActionId];
+ const actionsToMerge = {
+ [parentReportActionId]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ previousMessage: fakeParentAction.message,
+ message: [
+ {
+ translationKey: '',
+ type: 'COMMENT',
+ html: '',
+ text: '',
+ isEdited: true,
+ isDeletedParentAction: true,
+ },
+ ],
+ errors: null,
+ linkMetaData: [],
+ },
+ } as unknown as ReportActions;
+
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, actionsToMerge), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions), {runs});
-});
-test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => {
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => {
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions), {runs});
+ });
+
+ test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => {
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getLastClosedReportAction(reportActions), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getLastClosedReportAction(reportActions), {runs});
-});
-test('[ReportActionsUtils] getMostRecentReportActionLastModified', async () => {
- await Onyx.multiSet({
- ...mockedReportActionsMap,
+ test('[ReportActionsUtils] getMostRecentReportActionLastModified', async () => {
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportActionsUtils.getMostRecentReportActionLastModified(), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportActionsUtils.getMostRecentReportActionLastModified(), {runs});
});
diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts
index 62b911fb0417..30f874c669ab 100644
--- a/tests/perf-test/ReportUtils.perf-test.ts
+++ b/tests/perf-test/ReportUtils.perf-test.ts
@@ -13,19 +13,6 @@ import createRandomTransaction from '../utils/collections/transaction';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
const runs = CONST.PERFORMANCE_TESTS.RUNS;
-
-beforeAll(() =>
- Onyx.init({
- keys: ONYXKEYS,
- safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
- }),
-);
-
-// Clear out Onyx after each test so that each test starts with a clean state
-afterEach(() => {
- Onyx.clear();
-});
-
const getMockedReports = (length = 500) =>
createCollection(
(item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
@@ -50,195 +37,169 @@ const mockedReportsMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COL
const mockedPoliciesMap = getMockedPolicies(5000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>;
const participantAccountIDs = Array.from({length: 1000}, (v, i) => i + 1);
-test('[ReportUtils] findLastAccessedReport on 2k reports and policies', async () => {
- const ignoreDomainRooms = true;
- const isFirstTimeNewExpensifyUser = true;
- const reports = getMockedReports(2000);
- const policies = getMockedPolicies(2000);
- const openOnAdminRoom = true;
-
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom), {runs});
-});
+describe('ReportUtils', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+ });
+
+ Onyx.multiSet({
+ ...mockedPoliciesMap,
+ ...mockedReportsMap,
+ });
+ });
-test('[ReportUtils] canDeleteReportAction on 5k reports and policies', async () => {
- const reportID = '1';
+ afterAll(() => {
+ Onyx.clear();
+ });
- const reportAction = {...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT} as unknown as ReportAction;
+ test('[ReportUtils] findLastAccessedReport on 2k reports and policies', async () => {
+ const ignoreDomainRooms = true;
+ const isFirstTimeNewExpensifyUser = true;
+ const reports = getMockedReports(2000);
+ const policies = getMockedPolicies(2000);
+ const openOnAdminRoom = true;
- await Onyx.multiSet({
- ...mockedPoliciesMap,
- ...mockedReportsMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.canDeleteReportAction(reportAction, reportID), {runs});
-});
+ test('[ReportUtils] canDeleteReportAction on 5k reports and policies', async () => {
+ const reportID = '1';
+ const reportAction = {...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT} as unknown as ReportAction;
-test('[ReportUtils] getReportRecipientAccountID on 1k participants', async () => {
- const report = {...createRandomReport(1), participantAccountIDs};
- const currentLoginAccountID = 1;
-
- await Onyx.multiSet({
- ...mockedReportsMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.canDeleteReportAction(reportAction, reportID), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getReportRecipientAccountIDs(report, currentLoginAccountID), {runs});
-});
-
-test('[ReportUtils] getIconsForParticipants on 1k participants', async () => {
- const participants = Array.from({length: 1000}, (v, i) => i + 1);
+ test('[ReportUtils] getReportRecipientAccountID on 1k participants', async () => {
+ const report = {...createRandomReport(1), participantAccountIDs};
+ const currentLoginAccountID = 1;
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getIconsForParticipants(participants, personalDetails), {runs});
-});
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getReportRecipientAccountIDs(report, currentLoginAccountID), {runs});
+ });
-test('[ReportUtils] getIcons on 1k participants', async () => {
- const report = {...createRandomReport(1), parentReportID: '1', parentReportActionID: '1', type: CONST.REPORT.TYPE.CHAT};
- const policy = createRandomPolicy(1);
- const defaultIcon = null;
- const defaultName = '';
- const defaultIconId = -1;
+ test('[ReportUtils] getIconsForParticipants on 1k participants', async () => {
+ const participants = Array.from({length: 1000}, (v, i) => i + 1);
- await Onyx.multiSet({
- [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getIconsForParticipants(participants, personalDetails), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getIcons(report, personalDetails, defaultIcon, defaultName, defaultIconId, policy), {runs});
-});
-
-test('[ReportUtils] getDisplayNamesWithTooltips 1k participants', async () => {
- const isMultipleParticipantReport = true;
- const shouldFallbackToHidden = true;
+ test('[ReportUtils] getIcons on 1k participants', async () => {
+ const report = {...createRandomReport(1), parentReportID: '1', parentReportActionID: '1', type: CONST.REPORT.TYPE.CHAT};
+ const policy = createRandomPolicy(1);
+ const defaultIcon = null;
+ const defaultName = '';
+ const defaultIconId = -1;
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getDisplayNamesWithTooltips(personalDetails, isMultipleParticipantReport, shouldFallbackToHidden), {runs});
-});
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getIcons(report, personalDetails, defaultIcon, defaultName, defaultIconId, policy), {runs});
+ });
-test('[ReportUtils] getReportPreviewMessage on 5k policies', async () => {
- const reportAction = createRandomReportAction(1);
- const report = createRandomReport(1);
- const policy = createRandomPolicy(1);
- const shouldConsiderReceiptBeingScanned = true;
- const isPreviewMessageForParentChatReport = true;
+ test('[ReportUtils] getDisplayNamesWithTooltips 1k participants', async () => {
+ const isMultipleParticipantReport = true;
+ const shouldFallbackToHidden = true;
- await Onyx.multiSet({
- ...mockedPoliciesMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getDisplayNamesWithTooltips(personalDetails, isMultipleParticipantReport, shouldFallbackToHidden), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getReportPreviewMessage(report, reportAction, shouldConsiderReceiptBeingScanned, isPreviewMessageForParentChatReport, policy), {runs});
-});
+ test('[ReportUtils] getReportPreviewMessage on 5k policies', async () => {
+ const reportAction = createRandomReportAction(1);
+ const report = createRandomReport(1);
+ const policy = createRandomPolicy(1);
+ const shouldConsiderReceiptBeingScanned = true;
+ const isPreviewMessageForParentChatReport = true;
-test('[ReportUtils] getReportName on 1k participants', async () => {
- const report = {...createRandomReport(1), chatType: undefined, participantAccountIDs};
- const policy = createRandomPolicy(1);
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getReportPreviewMessage(report, reportAction, shouldConsiderReceiptBeingScanned, isPreviewMessageForParentChatReport, policy), {runs});
+ });
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getReportName(report, policy), {runs});
-});
+ test('[ReportUtils] getReportName on 1k participants', async () => {
+ const report = {...createRandomReport(1), chatType: undefined, participantAccountIDs};
+ const policy = createRandomPolicy(1);
-test('[ReportUtils] canShowReportRecipientLocalTime on 1k participants', async () => {
- const report = {...createRandomReport(1), participantAccountIDs};
- const accountID = 1;
- await Onyx.multiSet({
- ...mockedReportsMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getReportName(report, policy), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.canShowReportRecipientLocalTime(personalDetails, report, accountID), {runs});
-});
-
-test('[ReportUtils] shouldReportBeInOptionList on 1k participant', async () => {
- const report = {...createRandomReport(1), participantAccountIDs, type: CONST.REPORT.TYPE.CHAT};
- const currentReportId = '2';
- const isInGSDMode = true;
- const betas = [CONST.BETAS.DEFAULT_ROOMS];
- const policies = getMockedPolicies();
+ test('[ReportUtils] canShowReportRecipientLocalTime on 1k participants', async () => {
+ const report = {...createRandomReport(1), participantAccountIDs};
+ const accountID = 1;
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies), {runs});
-});
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.canShowReportRecipientLocalTime(personalDetails, report, accountID), {runs});
+ });
-test('[ReportUtils] getWorkspaceIcon on 5k policies', async () => {
- const report = createRandomReport(1);
- const policy = createRandomPolicy(1);
+ test('[ReportUtils] shouldReportBeInOptionList on 1k participant', async () => {
+ const report = {...createRandomReport(1), participantAccountIDs, type: CONST.REPORT.TYPE.CHAT};
+ const currentReportId = '2';
+ const isInGSDMode = true;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ const policies = getMockedPolicies();
- await Onyx.multiSet({
- ...mockedPoliciesMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getWorkspaceIcon(report, policy), {runs});
-});
-
-test('[ReportUtils] getMoneyRequestOptions on 1k participants', async () => {
- const report = {...createRandomReport(1), type: CONST.REPORT.TYPE.CHAT, chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: true};
- const policy = createRandomPolicy(1);
- const reportParticipants = Array.from({length: 1000}, (v, i) => i + 1);
+ test('[ReportUtils] getWorkspaceIcon on 5k policies', async () => {
+ const report = createRandomReport(1);
+ const policy = createRandomPolicy(1);
- await Onyx.multiSet({
- ...mockedPoliciesMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getWorkspaceIcon(report, policy), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getMoneyRequestOptions(report, policy, reportParticipants), {runs});
-});
-
-test('[ReportUtils] getWorkspaceAvatar on 5k policies', async () => {
- const report = createRandomReport(1);
+ test('[ReportUtils] getMoneyRequestOptions on 1k participants', async () => {
+ const report = {...createRandomReport(1), type: CONST.REPORT.TYPE.CHAT, chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, isOwnPolicyExpenseChat: true};
+ const policy = createRandomPolicy(1);
+ const reportParticipants = Array.from({length: 1000}, (v, i) => i + 1);
- await Onyx.multiSet({
- ...mockedPoliciesMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getMoneyRequestOptions(report, policy, reportParticipants), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getWorkspaceAvatar(report), {runs});
-});
-test('[ReportUtils] getWorkspaceChat on 5k policies', async () => {
- const policyID = '1';
- const accountsID = Array.from({length: 20}, (v, i) => i + 1);
+ test('[ReportUtils] getWorkspaceAvatar on 5k policies', async () => {
+ const report = createRandomReport(1);
- await Onyx.multiSet({
- ...mockedReportsMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getWorkspaceAvatar(report), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getWorkspaceChats(policyID, accountsID), {runs});
-});
-
-test('[ReportUtils] getTransactionDetails on 5k reports', async () => {
- const transaction = createRandomTransaction(1);
+ test('[ReportUtils] getWorkspaceChat on 5k policies', async () => {
+ const policyID = '1';
+ const accountsID = Array.from({length: 20}, (v, i) => i + 1);
- await Onyx.multiSet({
- ...mockedReportsMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getWorkspaceChats(policyID, accountsID), {runs});
});
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getTransactionDetails(transaction, 'yyyy-MM-dd'), {runs});
-});
+ test('[ReportUtils] getTransactionDetails on 5k reports', async () => {
+ const transaction = createRandomTransaction(1);
+
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getTransactionDetails(transaction, 'yyyy-MM-dd'), {runs});
+ });
-test('[ReportUtils] getIOUReportActionDisplayMessage on 5k policies', async () => {
- const reportAction = {
- ...createRandomReportAction(1),
- actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
- originalMessage: {
- IOUReportID: '1',
- IOUTransactionID: '1',
- amount: 100,
- participantAccountID: 1,
- currency: CONST.CURRENCY.USD,
- type: CONST.IOU.REPORT_ACTION_TYPE.PAY,
- paymentType: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
- },
- };
-
- await Onyx.multiSet({
- ...mockedPoliciesMap,
- });
-
- await waitForBatchedUpdates();
- await measureFunction(() => ReportUtils.getIOUReportActionDisplayMessage(reportAction), {runs});
+ test('[ReportUtils] getIOUReportActionDisplayMessage on 5k policies', async () => {
+ const reportAction = {
+ ...createRandomReportAction(1),
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ originalMessage: {
+ IOUReportID: '1',
+ IOUTransactionID: '1',
+ amount: 100,
+ participantAccountID: 1,
+ currency: CONST.CURRENCY.USD,
+ type: CONST.IOU.REPORT_ACTION_TYPE.PAY,
+ paymentType: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
+ },
+ };
+
+ await waitForBatchedUpdates();
+ await measureFunction(() => ReportUtils.getIOUReportActionDisplayMessage(reportAction), {runs});
+ });
});
diff --git a/tests/perf-test/SidebarLinks.perf-test.js b/tests/perf-test/SidebarLinks.perf-test.js
index 1f529b08e6b3..1bc674045c23 100644
--- a/tests/perf-test/SidebarLinks.perf-test.js
+++ b/tests/perf-test/SidebarLinks.perf-test.js
@@ -15,25 +15,6 @@ jest.mock('../../src/components/Icon/Expensicons');
jest.mock('@react-navigation/native');
-beforeAll(() =>
- Onyx.init({
- keys: ONYXKEYS,
- safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
- registerStorageEventListener: () => {},
- }),
-);
-
-// Initialize the network key for OfflineWithFeedback
-beforeEach(() => {
- wrapOnyxWithWaitForBatchedUpdates(Onyx);
- return Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false});
-});
-
-// Clear out Onyx after each test so that each test starts with a clean slate
-afterEach(() => {
- Onyx.clear();
-});
-
const getMockedReportsMap = (length = 100) => {
const mockReports = Array.from({length}, (__, i) => {
const reportID = i + 1;
@@ -51,71 +32,81 @@ const mockedResponseMap = getMockedReportsMap(500);
const runs = CONST.PERFORMANCE_TESTS.RUNS;
-test('[SidebarLinks] should render Sidebar with 500 reports stored', () => {
- const scenario = async () => {
- // Query for the sidebar
- await screen.findByTestId('lhn-options-list');
- /**
- * Query for display names of participants [1, 2].
- * This will ensure that the sidebar renders a list of items.
- */
- await screen.findAllByText('One, Two');
- };
-
- return waitForBatchedUpdates()
- .then(() =>
- Onyx.multiSet({
- [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT,
- [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
- [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
- [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
- [ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
- ...mockedResponseMap,
- }),
- )
- .then(() => measurePerformance(, {scenario, runs}));
-});
+describe('SidebarLinks', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+ registerStorageEventListener: () => {},
+ });
+
+ Onyx.multiSet({
+ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT,
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
+ [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
+ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
+ [ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
+ ...mockedResponseMap,
+ });
+ });
-test('[SidebarLinks] should scroll and click some of the items', () => {
- const scenario = async () => {
- const eventData = {
- nativeEvent: {
- contentOffset: {
- y: variables.optionRowHeight * 5,
- },
- contentSize: {
- // Dimensions of the scrollable content
- height: variables.optionRowHeight * 10,
- width: 100,
- },
- layoutMeasurement: {
- // Dimensions of the device
- height: variables.optionRowHeight * 5,
- width: 100,
+ // Initialize the network key for OfflineWithFeedback
+ beforeEach(() => {
+ wrapOnyxWithWaitForBatchedUpdates(Onyx);
+ return Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false});
+ });
+
+ afterAll(() => {
+ Onyx.clear();
+ });
+
+ test('[SidebarLinks] should render Sidebar with 500 reports stored', async () => {
+ const scenario = async () => {
+ // Query for the sidebar
+ await screen.findByTestId('lhn-options-list');
+ /**
+ * Query for display names of participants [1, 2].
+ * This will ensure that the sidebar renders a list of items.
+ */
+ await screen.findAllByText('One, Two');
+ };
+
+ await waitForBatchedUpdates();
+ await measurePerformance(, {scenario, runs});
+ });
+
+ test('[SidebarLinks] should scroll and click some of the items', async () => {
+ const scenario = async () => {
+ const eventData = {
+ nativeEvent: {
+ contentOffset: {
+ y: variables.optionRowHeight * 5,
+ },
+ contentSize: {
+ // Dimensions of the scrollable content
+ height: variables.optionRowHeight * 10,
+ width: 100,
+ },
+ layoutMeasurement: {
+ // Dimensions of the device
+ height: variables.optionRowHeight * 5,
+ width: 100,
+ },
},
- },
+ };
+
+ const lhnOptionsList = await screen.findByTestId('lhn-options-list');
+
+ fireEvent.scroll(lhnOptionsList, eventData);
+ // find elements that are currently visible in the viewport
+ const button1 = await screen.findByTestId('7');
+ const button2 = await screen.findByTestId('8');
+ fireEvent.press(button1);
+ fireEvent.press(button2);
};
- const lhnOptionsList = await screen.findByTestId('lhn-options-list');
-
- fireEvent.scroll(lhnOptionsList, eventData);
- // find elements that are currently visible in the viewport
- const button1 = await screen.findByTestId('7');
- const button2 = await screen.findByTestId('8');
- fireEvent.press(button1);
- fireEvent.press(button2);
- };
-
- return waitForBatchedUpdates()
- .then(() =>
- Onyx.multiSet({
- [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT,
- [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
- [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS],
- [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD,
- [ONYXKEYS.IS_LOADING_REPORT_DATA]: false,
- ...mockedResponseMap,
- }),
- )
- .then(() => measurePerformance(, {scenario, runs}));
+ await waitForBatchedUpdates();
+
+ await measurePerformance(, {scenario, runs});
+ });
});
diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts
index 6722cbf493a5..6ca81796d3ac 100644
--- a/tests/perf-test/SidebarUtils.perf-test.ts
+++ b/tests/perf-test/SidebarUtils.perf-test.ts
@@ -15,18 +15,6 @@ import createRandomReportAction from '../utils/collections/reportActions';
import createRandomReport from '../utils/collections/reports';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
-beforeAll(() =>
- Onyx.init({
- keys: ONYXKEYS,
- safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
- }),
-);
-
-// Clear out Onyx after each test so that each test starts with a clean slate
-afterEach(() => {
- Onyx.clear();
-});
-
const getMockedReports = (length = 500) =>
createCollection(
(item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
@@ -47,52 +35,61 @@ const personalDetails = createCollection(
const mockedResponseMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>;
const runs = CONST.PERFORMANCE_TESTS.RUNS;
-test('[SidebarUtils] getOptionData on 5k reports', async () => {
- const report = createRandomReport(1);
- const preferredLocale = 'en';
- const policy = createRandomPolicy(1);
- const parentReportAction = createRandomReportAction(1);
+describe('SidebarUtils', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+ });
- Onyx.multiSet({
- ...mockedResponseMap,
+ Onyx.multiSet({
+ ...mockedResponseMap,
+ });
});
- await waitForBatchedUpdates();
- await measureFunction(() => SidebarUtils.getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction), {runs});
-});
+ afterAll(() => {
+ Onyx.clear();
+ });
-test('[SidebarUtils] getOrderedReportIDs on 5k reports', async () => {
- const currentReportId = '1';
- const allReports = getMockedReports();
- const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ test('[SidebarUtils] getOptionData on 5k reports', async () => {
+ const report = createRandomReport(1);
+ const preferredLocale = 'en';
+ const policy = createRandomPolicy(1);
+ const parentReportAction = createRandomReportAction(1);
- const policies = createCollection(
- (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`,
- (index) => createRandomPolicy(index),
- );
+ await waitForBatchedUpdates();
+ await measureFunction(() => SidebarUtils.getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction), {runs});
+ });
+
+ test('[SidebarUtils] getOrderedReportIDs on 5k reports', async () => {
+ const currentReportId = '1';
+ const allReports = getMockedReports();
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
- const allReportActions = Object.fromEntries(
- Object.keys(reportActions).map((key) => [
- key,
- [
- {
- errors: reportActions[key].errors ?? [],
- message: [
- {
- moderationDecision: {
- decision: reportActions[key].message?.[0]?.moderationDecision?.decision,
+ const policies = createCollection(
+ (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`,
+ (index) => createRandomPolicy(index),
+ );
+
+ const allReportActions = Object.fromEntries(
+ Object.keys(reportActions).map((key) => [
+ key,
+ [
+ {
+ errors: reportActions[key].errors ?? [],
+ message: [
+ {
+ moderationDecision: {
+ decision: reportActions[key].message?.[0]?.moderationDecision?.decision,
+ },
},
- },
- ],
- },
- ],
- ]),
- ) as unknown as OnyxCollection;
+ ],
+ },
+ ],
+ ]),
+ ) as unknown as OnyxCollection;
- Onyx.multiSet({
- ...mockedResponseMap,
+ await waitForBatchedUpdates();
+ await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, betas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions), {runs});
});
-
- await waitForBatchedUpdates();
- await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, betas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions), {runs});
});
diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts
index 02990aa5c751..aedc02cc628b 100644
--- a/tests/unit/ModifiedExpenseMessageTest.ts
+++ b/tests/unit/ModifiedExpenseMessageTest.ts
@@ -275,5 +275,42 @@ describe('ModifiedExpenseMessage', () => {
expect(result).toEqual(expectedResult);
});
});
+
+ describe('when the created date is changed', () => {
+ const reportAction = {
+ ...createRandomReportAction(1),
+ actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE,
+ originalMessage: {
+ created: '2023-12-27',
+ oldCreated: '2023-12-26',
+ },
+ };
+
+ it('returns the correct text message', () => {
+ const expectedResult = 'changed the date to 2023-12-27 (previously 2023-12-26).';
+
+ const result = ModifiedExpenseMessage.getForReportAction(reportAction);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('when the created date was not changed', () => {
+ const reportAction = {
+ ...createRandomReportAction(1),
+ actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE,
+ originalMessage: {
+ created: '2023-12-27',
+ },
+ };
+
+ it('returns the correct text message', () => {
+ const expectedResult = 'changed the request';
+
+ const result = ModifiedExpenseMessage.getForReportAction(reportAction);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
});
});
diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js
index d700aa4724f1..23b748068b65 100644
--- a/tests/unit/ReportUtilsTest.js
+++ b/tests/unit/ReportUtilsTest.js
@@ -149,8 +149,8 @@ describe('ReportUtils', () => {
test('Archived', () => {
const archivedAdminsRoom = {
...baseAdminsRoom,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
expect(ReportUtils.getReportName(archivedAdminsRoom)).toBe('#admins (archived)');
@@ -172,8 +172,8 @@ describe('ReportUtils', () => {
test('Archived', () => {
const archivedPolicyRoom = {
...baseUserCreatedRoom,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
expect(ReportUtils.getReportName(archivedPolicyRoom)).toBe('#VikingsChat (archived)');
@@ -213,8 +213,8 @@ describe('ReportUtils', () => {
ownerAccountID: 1,
policyID: policy.policyID,
oldPolicyName: policy.name,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
test('as member', () => {
@@ -307,7 +307,7 @@ describe('ReportUtils', () => {
managerID: currentUserAccountID,
isUnreadWithMention: false,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
- statusNum: CONST.REPORT.STATUS.OPEN,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
};
expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true);
});
@@ -368,7 +368,7 @@ describe('ReportUtils', () => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.IOU,
- statusNum: CONST.REPORT.STATUS.REIMBURSED,
+ statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]);
expect(moneyRequestOptions.length).toBe(0);
@@ -378,8 +378,8 @@ describe('ReportUtils', () => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.EXPENSE,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
- statusNum: CONST.REPORT.STATUS.APPROVED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
+ statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]);
expect(moneyRequestOptions.length).toBe(0);
@@ -389,7 +389,7 @@ describe('ReportUtils', () => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.EXPENSE,
- statusNum: CONST.REPORT.STATUS.REIMBURSED,
+ statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID]);
expect(moneyRequestOptions.length).toBe(0);
@@ -419,8 +419,8 @@ describe('ReportUtils', () => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.EXPENSE,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
parentReportID: '101',
};
const paidPolicy = {
@@ -508,7 +508,7 @@ describe('ReportUtils', () => {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.EXPENSE,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
- statusNum: CONST.REPORT.STATUS.OPEN,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
parentReportID: '103',
};
const paidPolicy = {
@@ -523,9 +523,8 @@ describe('ReportUtils', () => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.IOU,
- state: CONST.REPORT.STATE.SUBMITTED,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]);
expect(moneyRequestOptions.length).toBe(1);
@@ -536,9 +535,8 @@ describe('ReportUtils', () => {
const report = {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.IOU,
- state: CONST.REPORT.STATE.SUBMITTED,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, {}, [currentUserAccountID, participantsAccountIDs[0]]);
expect(moneyRequestOptions.length).toBe(1);
@@ -590,15 +588,15 @@ describe('ReportUtils', () => {
describe('sortReportsByLastRead', () => {
it('should filter out report without reportID & lastReadTime and sort lastReadTime in ascending order', () => {
- const reports = {
- 1: {reportID: 1, lastReadTime: '2023-07-08 07:15:44.030'},
- 2: {reportID: 2, lastReadTime: null},
- 3: {reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'},
- 4: {reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU},
- 5: {lastReadTime: '2023-07-09 07:15:44.030'},
- 6: {reportID: 6},
- 7: {},
- };
+ const reports = [
+ {reportID: 1, lastReadTime: '2023-07-08 07:15:44.030'},
+ {reportID: 2, lastReadTime: null},
+ {reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'},
+ {reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU},
+ {lastReadTime: '2023-07-09 07:15:44.030'},
+ {reportID: 6},
+ {},
+ ];
const sortedReports = [
{reportID: 3, lastReadTime: '2023-07-06 07:15:44.030'},
{reportID: 4, lastReadTime: '2023-07-07 07:15:44.030', type: CONST.REPORT.TYPE.IOU},
diff --git a/tests/unit/SidebarFilterTest.js b/tests/unit/SidebarFilterTest.js
index 088e5a1af4d0..83ccf3e92752 100644
--- a/tests/unit/SidebarFilterTest.js
+++ b/tests/unit/SidebarFilterTest.js
@@ -457,20 +457,20 @@ describe('Sidebar', () => {
// Given an archived chat report, an archived default policy room, and an archived user created policy room
const archivedReport = {
...LHNTestUtils.getFakeReport([1, 2]),
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
const archivedPolicyRoomReport = {
...LHNTestUtils.getFakeReport([1, 2]),
chatType: CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
const archivedUserCreatedPolicyRoomReport = {
...LHNTestUtils.getFakeReport([1, 2]),
chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
LHNTestUtils.getDefaultRenderedSidebarLinks();
@@ -681,8 +681,8 @@ describe('Sidebar', () => {
const report = {
...LHNTestUtils.getFakeReport(),
lastVisibleActionCreated: '2022-11-22 03:48:27.267',
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
// Given the user is in all betas
@@ -732,8 +732,8 @@ describe('Sidebar', () => {
// Given an archived report that has all comments read
const report = {
...LHNTestUtils.getFakeReport(),
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
// Given the user is in all betas
@@ -781,8 +781,8 @@ describe('Sidebar', () => {
const report = {
...LHNTestUtils.getFakeReport(),
isPinned: false,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
// Given the user is in all betas
@@ -826,8 +826,8 @@ describe('Sidebar', () => {
// Given an archived report that is not the active report
const report = {
...LHNTestUtils.getFakeReport(),
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
// Given the user is in all betas
diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js
index 4d49cb3ad516..ed50c32559a7 100644
--- a/tests/unit/SidebarOrderTest.js
+++ b/tests/unit/SidebarOrderTest.js
@@ -255,7 +255,7 @@ describe('Sidebar', () => {
reportName: taskReportName,
managerID: 2,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
- statusNum: CONST.REPORT.STATUS.OPEN,
+ statusNum: CONST.REPORT.STATUS_NUM.OPEN,
};
// Each report has at least one ADDCOMMENT action so should be rendered in the LNH
@@ -313,8 +313,8 @@ describe('Sidebar', () => {
total: 10000,
currency: 'USD',
chatReportID: report3.reportID,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
report3.iouReportID = iouReport.reportID;
@@ -374,9 +374,8 @@ describe('Sidebar', () => {
policyName: 'Workspace',
total: -10000,
currency: 'USD',
- state: CONST.REPORT.STATE.SUBMITTED,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
chatReportID: report3.reportID,
parentReportID: report3.reportID,
};
@@ -575,9 +574,8 @@ describe('Sidebar', () => {
total: 10000,
currency: 'USD',
chatReportID: report3.reportID,
- state: CONST.REPORT.STATE.SUBMITTED,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
report3.iouReportID = iouReport.reportID;
const currentReportId = report2.reportID;
@@ -740,8 +738,8 @@ describe('Sidebar', () => {
const report1 = {
...LHNTestUtils.getFakeReport([1, 2]),
chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
const report2 = LHNTestUtils.getFakeReport([3, 4]);
const report3 = LHNTestUtils.getFakeReport([5, 6]);
@@ -837,8 +835,8 @@ describe('Sidebar', () => {
const report1 = {
...LHNTestUtils.getFakeReport([1, 2], 3, true),
chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
const report2 = LHNTestUtils.getFakeReport([3, 4], 2, true);
const report3 = LHNTestUtils.getFakeReport([5, 6], 1, true);
@@ -914,8 +912,8 @@ describe('Sidebar', () => {
total: 10000,
currency: 'USD',
chatReportID: report3.reportID,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
const iouReport2 = {
...LHNTestUtils.getFakeReport([9, 10]),
@@ -926,8 +924,8 @@ describe('Sidebar', () => {
total: 10000,
currency: 'USD',
chatReportID: report3.reportID,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
const iouReport3 = {
...LHNTestUtils.getFakeReport([11, 12]),
@@ -938,8 +936,8 @@ describe('Sidebar', () => {
total: 100000,
currency: 'USD',
chatReportID: report3.reportID,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
const iouReport4 = {
...LHNTestUtils.getFakeReport([11, 12]),
@@ -950,8 +948,8 @@ describe('Sidebar', () => {
total: 10000,
currency: 'USD',
chatReportID: report3.reportID,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
const iouReport5 = {
...LHNTestUtils.getFakeReport([11, 12]),
@@ -962,8 +960,8 @@ describe('Sidebar', () => {
total: 10000,
currency: 'USD',
chatReportID: report3.reportID,
- stateNum: CONST.REPORT.STATE_NUM.PROCESSING,
- statusNum: CONST.REPORT.STATUS.SUBMITTED,
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
};
report1.iouReportID = iouReport1.reportID;
diff --git a/tests/unit/SidebarTest.js b/tests/unit/SidebarTest.js
index 56009ee382d5..1101c5732707 100644
--- a/tests/unit/SidebarTest.js
+++ b/tests/unit/SidebarTest.js
@@ -51,8 +51,8 @@ describe('Sidebar', () => {
const report = {
...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true),
chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
// Given the user is in all betas
@@ -86,8 +86,8 @@ describe('Sidebar', () => {
...LHNTestUtils.getFakeReport(['email1@test.com', 'email2@test.com'], 3, true),
policyName: 'Vikings Policy',
chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM,
- statusNum: CONST.REPORT.STATUS.CLOSED,
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
};
const action = {
...LHNTestUtils.getFakeReportAction('email1@test.com', 3, true),
diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js
index 535fb018dbc3..67831bd32bca 100644
--- a/tests/utils/LHNTestUtils.js
+++ b/tests/utils/LHNTestUtils.js
@@ -209,8 +209,8 @@ function getAdvancedFakeReport(isArchived, isUserCreatedPolicyRoom, hasAddWorksp
...getFakeReport([1, 2], 0, isUnread),
type: CONST.REPORT.TYPE.CHAT,
chatType: isUserCreatedPolicyRoom ? CONST.REPORT.CHAT_TYPE.POLICY_ROOM : CONST.REPORT.CHAT_TYPE.POLICY_ADMINS,
- statusNum: isArchived ? CONST.REPORT.STATUS.CLOSED : 0,
- stateNum: isArchived ? CONST.REPORT.STATE_NUM.SUBMITTED : 0,
+ statusNum: isArchived ? CONST.REPORT.STATUS_NUM.CLOSED : 0,
+ stateNum: isArchived ? CONST.REPORT.STATE_NUM.APPROVED : 0,
errorFields: hasAddWorkspaceError ? {addWorkspaceRoom: 'blah'} : null,
isPinned,
hasDraft,
@@ -257,6 +257,8 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') {
autoReporting: true,
autoReportingFrequency: 'immediate',
isHarvestingEnabled: true,
+ autoReportingOffset: 1,
+ isPreventSelfApprovalEnabled: true,
submitsTo: 123456,
defaultBillable: false,
disabledFields: {defaultBillable: true, reimbursable: false},
diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts
index 96e13f915c49..acdd22253173 100644
--- a/tests/utils/collections/policies.ts
+++ b/tests/utils/collections/policies.ts
@@ -12,6 +12,8 @@ export default function createRandomPolicy(index: number): Policy {
isPolicyExpenseChatEnabled: randBoolean(),
autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)),
isHarvestingEnabled: randBoolean(),
+ autoReportingOffset: 1,
+ isPreventSelfApprovalEnabled: randBoolean(),
submitsTo: index,
outputCurrency: randCurrencyCode(),
role: rand(Object.values(CONST.POLICY.ROLE)),
diff --git a/web/index.html b/web/index.html
index 967873fe586c..7c02614d17b2 100644
--- a/web/index.html
+++ b/web/index.html
@@ -96,6 +96,11 @@
caret-color: #ffffff;
}
+ /* Customize Plaid iframe */
+ [id^="plaid-link-iframe"] {
+ color-scheme: dark !important;
+ }
+
/* Prevent autofill from overlapping with the input label in Chrome */
div:has(input:-webkit-autofill, input[chrome-autofilled]) > label {
transform: translateY(var(--active-label-translate-y)) scale(var(--active-label-scale)) !important;