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 162147aeff0c..47f19acfe6ae 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 1001042407
- versionName "1.4.24-7"
+ versionCode 1001042601
+ versionName "1.4.26-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/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/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 7081805db569..bb67f5840fad 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.24
+ 1.4.26
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.24.7
+ 1.4.26.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 20d4ea1a4820..8d5fb7867c37 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.24
+ 1.4.26
CFBundleSignature
????
CFBundleVersion
- 1.4.24.7
+ 1.4.26.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index f941edc1100e..85f148305fde 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -3,9 +3,9 @@
CFBundleShortVersionString
- 1.4.24
+ 1.4.26
CFBundleVersion
- 1.4.24.7
+ 1.4.26.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 ac012bea728f..ab98b21fca69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.24-7",
+ "version": "1.4.26-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.24-7",
+ "version": "1.4.26-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",
@@ -94,9 +94,9 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.126",
+ "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"
}
@@ -47034,17 +47034,17 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.126",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz",
- "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==",
+ "version": "1.0.118",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz",
+ "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
"underscore": "^1.13.6"
},
"engines": {
- "node": "20.9.0",
- "npm": "10.1.0"
+ "node": ">=16.15.1 <=20.9.0",
+ "npm": ">=8.11.0 <=10.1.0"
},
"peerDependencies": {
"idb-keyval": "^6.2.1",
@@ -47079,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"
@@ -62630,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": {
@@ -89702,9 +89702,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.126",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz",
- "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==",
+ "version": "1.0.118",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz",
+ "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -89718,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"
diff --git a/package.json b/package.json
index 4a28617f649d..7d792cae8cc0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.24-7",
+ "version": "1.4.26-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.",
@@ -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",
@@ -142,9 +142,9 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.126",
+ "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 8b5c0f5a88ca..d6f3d3cdcef6 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,
@@ -601,9 +603,11 @@ const CONST = {
},
THREAD_DISABLED: ['CREATED'],
},
+ CANCEL_PAYMENT_REASONS: {
+ ADMIN: 'CANCEL_REASON_ADMIN',
+ },
ACTIONABLE_MENTION_WHISPER_RESOLUTION: {
INVITE: 'invited',
- NOTHING: 'nothing',
},
ARCHIVE_REASON: {
DEFAULT: 'default',
@@ -635,18 +639,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,
@@ -1445,6 +1444,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: {
@@ -2730,7 +2731,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: {
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/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/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 ? (
+
)}
+ setIsConfirmModalVisible(false)}
+ prompt={translate('iou.cancelPaymentConfirmation')}
+ confirmText={translate('iou.cancelPayment')}
+ cancelText={translate('common.dismiss')}
+ danger
+ />
);
}
diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx
index 4b30276a204f..7d2b749cce0a 100644
--- a/src/components/MoneyReportHeaderStatusBar.tsx
+++ b/src/components/MoneyReportHeaderStatusBar.tsx
@@ -1,11 +1,12 @@
import React, {useMemo} from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as NextStepUtils from '@libs/NextStepUtils';
import CONST from '@src/CONST';
import type ReportNextStep from '@src/types/onyx/ReportNextStep';
import RenderHTML from './RenderHTML';
+import Text from './Text';
type MoneyReportHeaderStatusBarProps = {
/** The next step for the report */
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 13dce9337673..f66e73a2ef02 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -657,7 +657,7 @@ function MoneyRequestConfirmationList(props) {
/>
{!shouldShowAllFields && (
)}
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index 197829bb1ea9..412aeedcf965 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -7,12 +7,9 @@ import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager';
import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import FormHelpMessage from '@components/FormHelpMessage';
-import Icon from '@components/Icon';
-import {Info} from '@components/Icon/Expensicons';
import OptionsList from '@components/OptionsList';
-import {PressableWithoutFeedback} from '@components/Pressable';
+import ReferralProgramCTA from '@components/ReferralProgramCTA';
import ShowMoreButton from '@components/ShowMoreButton';
-import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withNavigationFocus from '@components/withNavigationFocus';
@@ -21,10 +18,8 @@ import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeSt
import compose from '@libs/compose';
import getPlatform from '@libs/getPlatform';
import KeyboardShortcut from '@libs/KeyboardShortcut';
-import Navigation from '@libs/Navigation/Navigation';
import setSelection from '@libs/setSelection';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
import {defaultProps as optionsSelectorDefaultProps, propTypes as optionsSelectorPropTypes} from './optionsSelectorPropTypes';
const propTypes = {
@@ -667,39 +662,7 @@ class BaseOptionsSelector extends Component {
{this.props.shouldShowReferralCTA && (
- {
- Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(this.props.referralContentType));
- }}
- style={[
- this.props.themeStyles.p5,
- this.props.themeStyles.w100,
- this.props.themeStyles.br2,
- this.props.themeStyles.highlightBG,
- this.props.themeStyles.flexRow,
- this.props.themeStyles.justifyContentBetween,
- this.props.themeStyles.alignItemsCenter,
- {gap: 10},
- ]}
- accessibilityLabel="referral"
- role={CONST.ACCESSIBILITY_ROLE.BUTTON}
- >
-
- {this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText1`)}
-
- {this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText2`)}
-
-
-
-
+
)}
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index 3d1f95822e6a..87a09895d50f 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -1,8 +1,11 @@
+import type {RefObject} from 'react';
+import type {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import type {PopoverAnchorPosition} from '@components/Modal/types';
import type BaseModalProps from '@components/Modal/types';
import type {WindowDimensionsProps} from '@components/withWindowDimensions/types';
import type CONST from '@src/CONST';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
type AnchorAlignment = {
/** The horizontal anchor alignment of the popover */
@@ -17,34 +20,32 @@ type PopoverDimensions = {
height: number;
};
-type PopoverProps = BaseModalProps & {
- /** The anchor position of the popover */
- anchorPosition?: PopoverAnchorPosition;
+type PopoverProps = BaseModalProps &
+ ChildrenProps & {
+ /** The anchor position of the popover */
+ anchorPosition?: PopoverAnchorPosition;
- /** The anchor alignment of the popover */
- anchorAlignment: AnchorAlignment;
+ /** The anchor alignment of the popover */
+ anchorAlignment?: AnchorAlignment;
- /** The anchor ref of the popover */
- anchorRef: React.RefObject;
+ /** The anchor ref of the popover */
+ anchorRef: RefObject;
- /** Whether disable the animations */
- disableAnimation: boolean;
+ /** Whether disable the animations */
+ disableAnimation?: boolean;
- /** Whether we don't want to show overlay */
- withoutOverlay: boolean;
+ /** Whether we don't want to show overlay */
+ withoutOverlay: boolean;
- /** The dimensions of the popover */
- popoverDimensions?: PopoverDimensions;
+ /** The dimensions of the popover */
+ popoverDimensions?: PopoverDimensions;
- /** The ref of the popover */
- withoutOverlayRef?: React.RefObject;
+ /** The ref of the popover */
+ withoutOverlayRef?: RefObject;
- /** Whether we want to show the popover on the right side of the screen */
- fromSidebarMediumScreen?: boolean;
-
- /** The popover children */
- children: React.ReactNode;
-};
+ /** Whether we want to show the popover on the right side of the screen */
+ fromSidebarMediumScreen?: boolean;
+ };
type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps;
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 2d6f74f7cd46..17b1a119671a 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -3,13 +3,13 @@ import type {RefObject} from 'react';
import React, {useRef} from 'react';
import {View} from 'react-native';
import type {ModalProps} from 'react-native-modal';
-import type {SvgProps} from 'react-native-svg';
import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
+import type IconAsset from '@src/types/utils/IconAsset';
import MenuItem from './MenuItem';
import type {AnchorAlignment} from './Popover/types';
import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
@@ -17,7 +17,7 @@ import Text from './Text';
type PopoverMenuItem = {
/** An icon element displayed on the left side */
- icon: React.FC;
+ icon: IconAsset;
/** Text label */
text: string;
@@ -46,7 +46,7 @@ type PopoverMenuItem = {
type PopoverModalProps = Pick;
-type PopoverMenuProps = PopoverModalProps & {
+type PopoverMenuProps = Partial & {
/** Callback method fired when the user requests to close the modal */
onClose: () => void;
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index b50b04289813..b1a6ebb0c5c0 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -1,18 +1,27 @@
-import React from 'react';
+import type {RefObject} from 'react';
+import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {View} from 'react-native';
import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types';
-const PopoverContext = React.createContext({
+const PopoverContext = createContext({
onOpen: () => {},
popover: {},
close: () => {},
isOpen: false,
});
+function elementContains(ref: RefObject | undefined, target: EventTarget | null) {
+ if (ref?.current && 'contains' in ref?.current && ref?.current?.contains(target as Node)) {
+ return true;
+ }
+ return false;
+}
+
function PopoverContextProvider(props: PopoverContextProps) {
- const [isOpen, setIsOpen] = React.useState(false);
- const activePopoverRef = React.useRef(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const activePopoverRef = useRef(null);
- const closePopover = React.useCallback((anchorRef?: React.RefObject) => {
+ const closePopover = useCallback((anchorRef?: RefObject) => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return;
}
@@ -25,10 +34,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
setIsOpen(false);
}, []);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: Event) => {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) {
+ if (elementContains(activePopoverRef.current?.ref, e.target) || elementContains(activePopoverRef.current?.anchorRef, e.target)) {
return;
}
const ref = activePopoverRef.current?.anchorRef;
@@ -40,9 +48,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: Event) => {
- if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) {
+ if (elementContains(activePopoverRef.current?.ref, e.target)) {
return;
}
closePopover();
@@ -53,7 +61,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return;
@@ -66,7 +74,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = () => {
if (document.hasFocus()) {
return;
@@ -79,9 +87,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: Event) => {
- if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) {
+ if (elementContains(activePopoverRef.current?.ref, e.target)) {
return;
}
@@ -93,7 +101,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- const onOpen = React.useCallback(
+ const onOpen = useCallback(
(popoverParams: AnchorRef) => {
if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) {
closePopover(activePopoverRef.current.anchorRef);
@@ -107,7 +115,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
[closePopover],
);
- const contextValue = React.useMemo(
+ const contextValue = useMemo(
() => ({
onOpen,
close: closePopover,
diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts
index ffd0087cd5ff..49705d7ea7a8 100644
--- a/src/components/PopoverProvider/types.ts
+++ b/src/components/PopoverProvider/types.ts
@@ -1,18 +1,21 @@
+import type {ReactNode, RefObject} from 'react';
+import type {View} from 'react-native';
+
type PopoverContextProps = {
- children: React.ReactNode;
+ children: ReactNode;
};
type PopoverContextValue = {
onOpen?: (popoverParams: AnchorRef) => void;
popover?: AnchorRef | Record | null;
- close: (anchorRef?: React.RefObject) => void;
+ close: (anchorRef?: RefObject) => void;
isOpen: boolean;
};
type AnchorRef = {
- ref: React.RefObject;
- close: (anchorRef?: React.RefObject) => void;
- anchorRef: React.RefObject;
+ ref: RefObject;
+ close: (anchorRef?: RefObject) => void;
+ anchorRef: RefObject;
onOpenCallback?: () => void;
onCloseCallback?: () => void;
};
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index 6aed275bd2dc..58d022ef9d65 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -8,6 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Modal from '@userActions/Modal';
+import viewRef from '@src/types/utils/viewRef';
import type PopoverWithoutOverlayProps from './types';
function PopoverWithoutOverlay(
@@ -52,7 +53,7 @@ function PopoverWithoutOverlay(
close: onClose,
anchorRef,
});
- removeOnClose = Modal.setCloseModal(() => onClose(anchorRef));
+ removeOnClose = Modal.setCloseModal(onClose);
} else {
onModalHide();
close(anchorRef);
@@ -119,7 +120,7 @@ function PopoverWithoutOverlay(
return (
;
+ anchorRef: RefObject;
/** A react-native-animatable animation timing for the modal display animation */
animationInTiming?: number;
@@ -22,7 +23,7 @@ type PopoverWithoutOverlayProps = ChildrenProps &
disableAnimation?: boolean;
/** The ref of the popover */
- withoutOverlayRef: React.RefObject;
+ withoutOverlayRef: RefObject;
};
export default PopoverWithoutOverlayProps;
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index 1b711633ed3b..5f32240aca9b 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -1,3 +1,4 @@
+import type {RefObject} from 'react';
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
@@ -27,7 +28,7 @@ type ProcessMoneyRequestHoldMenuProps = {
anchorAlignment: AnchorAlignment;
/** The anchor ref of the popover menu */
- anchorRef: React.RefObject;
+ anchorRef: RefObject;
};
function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment, anchorRef}: ProcessMoneyRequestHoldMenuProps) {
diff --git a/src/components/QRShare/QRShareWithDownload/index.native.tsx b/src/components/QRShare/QRShareWithDownload/index.native.tsx
index d1d9f13147f1..7d192c84c454 100644
--- a/src/components/QRShare/QRShareWithDownload/index.native.tsx
+++ b/src/components/QRShare/QRShareWithDownload/index.native.tsx
@@ -3,6 +3,7 @@ import React, {forwardRef, useImperativeHandle, useRef} from 'react';
import ViewShot from 'react-native-view-shot';
import getQrCodeFileName from '@components/QRShare/getQrCodeDownloadFileName';
import type {QRShareProps} from '@components/QRShare/types';
+import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import fileDownload from '@libs/fileDownload';
import QRShare from '..';
@@ -10,14 +11,16 @@ import type QRShareWithDownloadHandle from './types';
function QRShareWithDownload(props: QRShareProps, ref: ForwardedRef) {
const {isOffline} = useNetwork();
+ const {translate} = useLocalize();
+
const qrCodeScreenshotRef = useRef(null);
useImperativeHandle(
ref,
() => ({
- download: () => qrCodeScreenshotRef.current?.capture?.().then((uri) => fileDownload(uri, getQrCodeFileName(props.title))),
+ download: () => qrCodeScreenshotRef.current?.capture?.().then((uri) => fileDownload(uri, getQrCodeFileName(props.title), translate('fileDownload.success.qrMessage'))),
}),
- [props.title],
+ [props.title, translate],
);
return (
diff --git a/src/components/Reactions/MiniQuickEmojiReactions.tsx b/src/components/Reactions/MiniQuickEmojiReactions.tsx
index 9f38da6bdb3d..1b489166e949 100644
--- a/src/components/Reactions/MiniQuickEmojiReactions.tsx
+++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx
@@ -1,6 +1,5 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem';
@@ -16,16 +15,7 @@ import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReportActionReactions} from '@src/types/onyx';
-import type {BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types';
-
-type MiniQuickEmojiReactionsOnyxProps = {
- /** All the emoji reactions for the report action. */
- emojiReactions: OnyxEntry;
-
- /** The user's preferred skin tone. */
- preferredSkinTone: OnyxEntry;
-};
+import type {BaseQuickEmojiReactionsOnyxProps, BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types';
type MiniQuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & {
/**
@@ -112,11 +102,14 @@ function MiniQuickEmojiReactions({
MiniQuickEmojiReactions.displayName = 'MiniQuickEmojiReactions';
-export default withOnyx({
+export default withOnyx({
preferredSkinTone: {
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
},
emojiReactions: {
key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`,
},
+ preferredLocale: {
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ },
})(MiniQuickEmojiReactions);
diff --git a/src/components/Reactions/QuickEmojiReactions/types.ts b/src/components/Reactions/QuickEmojiReactions/types.ts
index d782d5ae35c7..9c17a87c56c0 100644
--- a/src/components/Reactions/QuickEmojiReactions/types.ts
+++ b/src/components/Reactions/QuickEmojiReactions/types.ts
@@ -11,18 +11,7 @@ type OpenPickerCallback = (element?: PickerRefElement, anchorOrigin?: AnchorOrig
type CloseContextMenuCallback = () => void;
-type BaseQuickEmojiReactionsOnyxProps = {
- /** All the emoji reactions for the report action. */
- emojiReactions: OnyxEntry;
-
- /** The user's preferred locale. */
- preferredLocale: OnyxEntry;
-
- /** The user's preferred skin tone. */
- preferredSkinTone: OnyxEntry;
-};
-
-type BaseQuickEmojiReactionsProps = BaseQuickEmojiReactionsOnyxProps & {
+type BaseReactionsProps = {
/** Callback to fire when an emoji is selected. */
onEmojiSelected: (emoji: Emoji, emojiReactions: OnyxEntry) => void;
@@ -45,7 +34,20 @@ type BaseQuickEmojiReactionsProps = BaseQuickEmojiReactionsOnyxProps & {
reportActionID: string;
};
-type QuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & {
+type BaseQuickEmojiReactionsOnyxProps = {
+ /** All the emoji reactions for the report action. */
+ emojiReactions: OnyxEntry;
+
+ /** The user's preferred locale. */
+ preferredLocale: OnyxEntry;
+
+ /** The user's preferred skin tone. */
+ preferredSkinTone: OnyxEntry;
+};
+
+type BaseQuickEmojiReactionsProps = BaseReactionsProps & BaseQuickEmojiReactionsOnyxProps;
+
+type QuickEmojiReactionsProps = BaseReactionsProps & {
/**
* Function that can be called to close the context menu
* in which this component is rendered.
diff --git a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
similarity index 68%
rename from src/pages/iou/MoneyRequestReferralProgramCTA.tsx
rename to src/components/ReferralProgramCTA.tsx
index 31394e1bd0e1..473d5cdbed08 100644
--- a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -1,20 +1,24 @@
import React from 'react';
-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 useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import Navigation from '@src/libs/Navigation/Navigation';
import ROUTES from '@src/ROUTES';
+import Icon from './Icon';
+import {Info} from './Icon/Expensicons';
+import {PressableWithoutFeedback} from './Pressable';
+import Text from './Text';
-type MoneyRequestReferralProgramCTAProps = {
- referralContentType: typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
+type ReferralProgramCTAProps = {
+ referralContentType:
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
+ | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
};
-function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestReferralProgramCTAProps) {
+function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
@@ -41,9 +45,10 @@ function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestRefer
src={Info}
height={20}
width={20}
+ fill={theme.icon}
/>
);
}
-export default MoneyRequestReferralProgramCTA;
+export default ReferralProgramCTA;
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={[]}
+ />
+
+ );
+ })}
{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..622cd75a568b 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');
@@ -162,7 +173,7 @@ function ReportPreview(props) {
const previewSubtitle =
formattedMerchant ||
props.translate('iou.requestCount', {
- count: numberOfRequests,
+ count: numberOfRequests - numberOfScanningReceipts,
scanningReceipts: numberOfScanningReceipts,
});
@@ -218,28 +229,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
@@ -370,5 +359,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..8ef837ed986d 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';
@@ -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,12 +86,13 @@ 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 assigneeLogin = personalDetails[taskAssigneeAccountID]?.login ?? '';
@@ -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')}
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index bc19a837a7af..59a1c4dd08ce 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -102,14 +102,8 @@ function BaseListItem({
{isRadioItem ? (
onSelectRow(item)}
showTooltip={showTooltip}
@@ -124,7 +118,7 @@ function BaseListItem({
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(item)}
showTooltip={showTooltip}
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/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/index.js b/src/components/ValuePicker/index.js
index d96333e08241..d90529114af4 100644
--- a/src/components/ValuePicker/index.js
+++ b/src/components/ValuePicker/index.js
@@ -71,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 : '';
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}) => (