diff --git a/Gemfile.lock b/Gemfile.lock
index ed190579d306..beb2c1762936 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,7 +8,7 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
- addressable (2.8.5)
+ addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
@@ -18,20 +18,20 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.857.0)
- aws-sdk-core (3.188.0)
- aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (1.883.0)
+ aws-sdk-core (3.190.3)
+ aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
- aws-sigv4 (~> 1.5)
+ aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.73.0)
+ aws-sdk-kms (1.76.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.140.0)
- aws-sdk-core (~> 3, >= 3.188.0)
+ aws-sdk-s3 (1.142.0)
+ aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-kms (~> 1)
- aws-sigv4 (~> 1.6)
- aws-sigv4 (1.7.0)
+ aws-sigv4 (~> 1.8)
+ aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@@ -80,14 +80,13 @@ GEM
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
- domain_name (0.5.20190701)
- unf (>= 0.0.5, < 1.0.0)
+ domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
- excon (0.104.0)
+ excon (0.109.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -116,8 +115,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
- fastimage (2.2.7)
- fastlane (2.217.0)
+ fastimage (2.3.0)
+ fastlane (2.219.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -136,6 +135,7 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
@@ -144,7 +144,7 @@ GEM
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
- optparse (~> 0.1.1)
+ optparse (>= 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
@@ -165,9 +165,9 @@ GEM
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
- google-apis-androidpublisher_v3 (0.53.0)
+ google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-apis-core (0.11.2)
+ google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -175,24 +175,23 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
- webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-apis-storage_v1 (0.29.0)
+ google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-cloud-core (1.6.0)
- google-cloud-env (~> 1.0)
+ google-cloud-core (1.6.1)
+ google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1)
- google-cloud-storage (1.45.0)
+ google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
- google-apis-storage_v1 (~> 0.29.0)
+ google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
@@ -209,7 +208,7 @@ GEM
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
- json (2.6.3)
+ json (2.7.1)
jwt (2.7.1)
mime-types (3.5.1)
mime-types-data (~> 3.2015)
@@ -224,9 +223,9 @@ GEM
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
- optparse (0.1.1)
+ optparse (0.4.0)
os (1.1.4)
- plist (3.7.0)
+ plist (3.7.1)
public_suffix (4.0.7)
rake (13.1.0)
representable (3.2.0)
@@ -253,7 +252,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
- tty-screen (0.8.1)
+ tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.1)
@@ -261,11 +260,7 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
- unf (0.1.4)
- unf_ext
- unf_ext (0.0.9.1)
unicode-display_width (2.5.0)
- webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.23.0)
CFPropertyList (>= 2.3.3, < 4.0)
@@ -297,4 +292,4 @@ RUBY VERSION
ruby 2.6.10p210
BUNDLED WITH
- 2.4.7
+ 2.4.19
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 071823fdea97..49f1b017d5e0 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 1001042900
- versionName "1.4.29-0"
+ versionCode 1001043102
+ versionName "1.4.31-2"
}
flavorDimensions "default"
diff --git a/assets/images/scan.svg b/assets/images/scan.svg
new file mode 100644
index 000000000000..629dc3823a12
--- /dev/null
+++ b/assets/images/scan.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/desktop/main.js b/desktop/main.js
index c9d614d3de15..e53f03530b57 100644
--- a/desktop/main.js
+++ b/desktop/main.js
@@ -512,10 +512,10 @@ const mainWindow = () => {
});
browserWindow.on('swipe', (e, direction) => {
- if (direction === 'right') {
+ if (direction === 'left') {
browserWindow.webContents.goBack();
}
- if (direction === 'left') {
+ if (direction === 'right') {
browserWindow.webContents.goForward();
}
});
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index e5ee52210866..454857981834 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 68492e0fef1f..34341662d137 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.29
+ 1.4.31
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.29.0
+ 1.4.31.2
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 48d1576c6d21..38073f64d814 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.29
+ 1.4.31
CFBundleSignature
????
CFBundleVersion
- 1.4.29.0
+ 1.4.31.2
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index dfff67d38e21..8550e23db7b1 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -3,9 +3,9 @@
CFBundleShortVersionString
- 1.4.29
+ 1.4.31
CFBundleVersion
- 1.4.29.0
+ 1.4.31.2
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 4985b0f7edc9..776dcb544ee6 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1158,7 +1158,7 @@ PODS:
- react-native-airship (15.3.1):
- AirshipFrameworkProxy (= 2.1.1)
- React-Core
- - react-native-blob-util (0.19.6):
+ - react-native-blob-util (0.19.4):
- React-Core
- react-native-cameraroll (5.4.0):
- React-Core
@@ -1901,7 +1901,7 @@ SPEC CHECKSUMS:
React-logger: 66b168e2b2bee57bd8ce9e69f739d805732a5570
React-Mapbuffer: 9ee041e1d7be96da6d76a251f92e72b711c651d6
react-native-airship: 6ded22e4ca54f2f80db80b7b911c2b9b696d9335
- react-native-blob-util: d8fa1a7f726867907a8e43163fdd8b441d4489ea
+ react-native-blob-util: 30a6c9fd067aadf9177e61a998f2c7efb670598d
react-native-cameraroll: 8ffb0af7a5e5de225fd667610e2979fc1f0c2151
react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e
react-native-document-picker: 69ca2094d8780cfc1e7e613894d15290fdc54bba
diff --git a/package-lock.json b/package-lock.json
index 433e4dcf4de2..f05837e853ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.29-0",
+ "version": "1.4.31-2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.29-0",
+ "version": "1.4.31-2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -76,7 +76,7 @@
"react-map-gl": "^7.1.3",
"react-native": "0.73.2",
"react-native-android-location-enabler": "^1.2.2",
- "react-native-blob-util": "^0.19.6",
+ "react-native-blob-util": "0.19.4",
"react-native-collapsible": "^1.6.1",
"react-native-config": "^1.4.5",
"react-native-dev-menu": "^4.1.1",
@@ -44743,90 +44743,32 @@
}
},
"node_modules/react-native-blob-util": {
- "version": "0.19.6",
- "resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.19.6.tgz",
- "integrity": "sha512-62yMJdgOMEu2Ir9V177FbvOYZskFu92XkT/hUWMMdu3G7UhqnetwtyVwqo4jfNrr+Y3Lp0gXYx60vxjMaov1pQ==",
+ "version": "0.19.4",
+ "resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.19.4.tgz",
+ "integrity": "sha512-qQ2l3ZmclxA8Ht9NyI4qVJ94j8R4wIUaNP40ZMGjDMXelzvk3tTBeuEYkx7a2R84TGpaXCpKDQmee7+x9Bmo7A==",
"dependencies": {
"base-64": "0.1.0",
- "glob": "^10.3.10"
+ "glob": "^7.2.3"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
- "node_modules/react-native-blob-util/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/react-native-blob-util/node_modules/foreground-child": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
- "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
- "dependencies": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/react-native-blob-util/node_modules/glob": {
- "version": "10.3.10",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
- "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
- "minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/react-native-blob-util/node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/react-native-blob-util/node_modules/minipass": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
- "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/react-native-blob-util/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "engines": {
- "node": ">=14"
+ "node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -85794,60 +85736,26 @@
}
},
"react-native-blob-util": {
- "version": "0.19.6",
- "resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.19.6.tgz",
- "integrity": "sha512-62yMJdgOMEu2Ir9V177FbvOYZskFu92XkT/hUWMMdu3G7UhqnetwtyVwqo4jfNrr+Y3Lp0gXYx60vxjMaov1pQ==",
+ "version": "0.19.4",
+ "resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.19.4.tgz",
+ "integrity": "sha512-qQ2l3ZmclxA8Ht9NyI4qVJ94j8R4wIUaNP40ZMGjDMXelzvk3tTBeuEYkx7a2R84TGpaXCpKDQmee7+x9Bmo7A==",
"requires": {
"base-64": "0.1.0",
- "glob": "^10.3.10"
+ "glob": "^7.2.3"
},
"dependencies": {
- "brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "requires": {
- "balanced-match": "^1.0.0"
- }
- },
- "foreground-child": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
- "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
- "requires": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^4.0.1"
- }
- },
"glob": {
- "version": "10.3.10",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
- "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
- "requires": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
- "minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
- }
- },
- "minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
- "brace-expansion": "^2.0.1"
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
}
- },
- "minipass": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
- "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ=="
- },
- "signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="
}
}
},
diff --git a/package.json b/package.json
index 2cb65adaf67b..2ba358b438e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.29-0",
+ "version": "1.4.31-2",
"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.",
@@ -124,7 +124,7 @@
"react-map-gl": "^7.1.3",
"react-native": "0.73.2",
"react-native-android-location-enabler": "^1.2.2",
- "react-native-blob-util": "^0.19.6",
+ "react-native-blob-util": "0.19.4",
"react-native-collapsible": "^1.6.1",
"react-native-config": "^1.4.5",
"react-native-dev-menu": "^4.1.1",
diff --git a/src/CONST.ts b/src/CONST.ts
index 0b10e5767328..dbf80c6929de 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -440,6 +440,14 @@ const CONST = {
},
CURRENCY: {
USD: 'USD',
+ AUD: 'AUD',
+ CAD: 'CAD',
+ GBP: 'GBP',
+ NZD: 'NZD',
+ EUR: 'EUR',
+ },
+ get DIRECT_REIMBURSEMENT_CURRENCIES() {
+ return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.NZD, this.CURRENCY.EUR];
},
EXAMPLE_PHONE_NUMBER: '+15005550006',
CONCIERGE_CHAT_NAME: 'Concierge',
@@ -967,6 +975,7 @@ const CONST = {
SMALL_EMOJI_PICKER_SIZE: {
WIDTH: '100%',
},
+ MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM: 83,
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300,
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT_WEB: 200,
EMOJI_PICKER_ITEM_HEIGHT: 32,
@@ -977,7 +986,6 @@ const CONST = {
SUGGESTER_INNER_PADDING: 8,
SUGGESTION_ROW_HEIGHT: 40,
SMALL_CONTAINER_HEIGHT_FACTOR: 2.5,
- MIN_AMOUNT_OF_SUGGESTIONS: 3,
MAX_AMOUNT_OF_SUGGESTIONS: 20,
MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5,
HERE_TEXT: '@here',
@@ -1298,6 +1306,11 @@ const CONST = {
CUSTOM_UNIT_RATE_BASE_OFFSET: 100,
OWNER_EMAIL_FAKE: '_FAKE_',
OWNER_ACCOUNT_ID_FAKE: 0,
+ REIMBURSEMENT_CHOICES: {
+ REIMBURSEMENT_YES: 'reimburseYes',
+ REIMBURSEMENT_NO: 'reimburseNo',
+ REIMBURSEMENT_MANUAL: 'reimburseManual',
+ },
ID_FAKE: '_FAKE_',
},
@@ -3131,7 +3144,20 @@ const CONST = {
REPORT: 'REPORT',
},
+ INTRO_CHOICES: {
+ TRACK: 'newDotTrack',
+ SUBMIT: 'newDotSubmit',
+ MANAGE_TEAM: 'newDotManageTeam',
+ CHAT_SPLIT: 'newDotSplitChat',
+ },
+
MINI_CONTEXT_MENU_MAX_ITEMS: 4,
+
+ REPORT_FIELD_TITLE_FIELD_ID: 'text_title',
} as const;
+type Country = keyof typeof CONST.ALL_COUNTRIES;
+
+export type {Country};
+
export default CONST;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 40a43d8195de..26424af8056c 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -110,6 +110,12 @@ const ONYXKEYS = {
/** This NVP holds to most recent waypoints that a person has used when creating a distance request */
NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints',
+ /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */
+ NVP_HAS_DISMISSED_IDLE_PANEL: 'hasDismissedIdlePanel',
+
+ /** This NVP contains the choice that the user made on the engagement modal */
+ NVP_INTRO_SELECTED: 'introSelected',
+
/** Does this user have push notifications enabled for this device? */
PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',
@@ -350,6 +356,8 @@ const ONYXKEYS = {
REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft',
GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm',
GET_PHYSICAL_CARD_FORM_DRAFT: 'getPhysicalCardFormDraft',
+ POLICY_REPORT_FIELD_EDIT_FORM: 'policyReportFieldEditForm',
+ POLICY_REPORT_FIELD_EDIT_FORM_DRAFT: 'policyReportFieldEditFormDraft',
},
} as const;
@@ -393,6 +401,8 @@ type OnyxValues = {
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
[ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record;
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
+ [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean;
+ [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected;
[ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
[ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData;
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
@@ -442,7 +452,7 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers;
[ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
- [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportField;
+ [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record;
@@ -461,7 +471,7 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
[ONYXKEYS.COLLECTION.SELECTED_TAB]: string;
- [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations;
+ [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolation[];
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;
@@ -528,6 +538,8 @@ type OnyxValues = {
[ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form | undefined;
+ [ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM]: OnyxTypes.Form;
+ [ONYXKEYS.FORMS.POLICY_REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form | undefined;
};
type OnyxKeyValue = OnyxEntry;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 37003a09a0cd..42123aa9b4a4 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -29,6 +29,10 @@ const ROUTES = {
route: 'a/:accountID',
getRoute: (accountID: string | number, backTo?: string) => getUrlWithBackToParam(`a/${accountID}`, backTo),
},
+ PROFILE_AVATAR: {
+ route: 'a/:accountID/avatar',
+ getRoute: (accountID: string) => `a/${accountID}/avatar` as const,
+ },
TRANSITION_BETWEEN_APPS: 'transition',
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
@@ -140,6 +144,7 @@ const ROUTES = {
getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo),
},
SETTINGS_STATUS: 'settings/profile/status',
+
SETTINGS_STATUS_CLEAR_AFTER: 'settings/profile/status/clear-after',
SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date',
SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time',
@@ -155,6 +160,10 @@ const ROUTES = {
route: 'r/:reportID?/:reportActionID?',
getRoute: (reportID: string) => `r/${reportID}` as const,
},
+ REPORT_AVATAR: {
+ route: 'r/:reportID/avatar',
+ getRoute: (reportID: string) => `r/${reportID}/avatar` as const,
+ },
EDIT_REQUEST: {
route: 'r/:threadReportID/edit/:field',
getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}` as const,
@@ -163,6 +172,10 @@ const ROUTES = {
route: 'r/:threadReportID/edit/currency',
getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}` as const,
},
+ EDIT_REPORT_FIELD_REQUEST: {
+ route: 'r/:reportID/edit/policyField/:policyID/:fieldID',
+ getRoute: (reportID: string, policyID: string, fieldID: string) => `r/${reportID}/edit/policyField/${policyID}/${fieldID}` as const,
+ },
REPORT_WITH_ID_DETAILS_SHARE_CODE: {
route: 'r/:reportID/details/shareCode',
getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const,
@@ -434,6 +447,10 @@ const ROUTES = {
route: 'workspace/:policyID/settings',
getRoute: (policyID: string) => `workspace/${policyID}/settings` as const,
},
+ WORKSPACE_AVATAR: {
+ route: 'workspace/:policyID/avatar',
+ getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const,
+ },
WORKSPACE_SETTINGS_CURRENCY: {
route: 'workspace/:policyID/settings/currency',
getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 703cb309d641..960991eb277b 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -13,6 +13,9 @@ const PROTECTED_SCREENS = {
const SCREENS = {
...PROTECTED_SCREENS,
REPORT: 'Report',
+ PROFILE_AVATAR: 'ProfileAvatar',
+ WORKSPACE_AVATAR: 'WorkspaceAvatar',
+ REPORT_AVATAR: 'ReportAvatar',
NOT_FOUND: 'not-found',
TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps',
VALIDATE_LOGIN: 'ValidateLogin',
@@ -208,6 +211,7 @@ const SCREENS = {
EDIT_REQUEST: {
ROOT: 'EditRequest_Root',
CURRENCY: 'EditRequest_Currency',
+ REPORT_FIELD: 'EditRequest_ReportField',
},
NEW_CHAT: {
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index bd8d535e540f..4988c33ed8ce 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -22,17 +22,21 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import useNativeDriver from '@libs/useNativeDriver';
import reportPropTypes from '@pages/reportPropTypes';
+import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import AttachmentCarousel from './Attachments/AttachmentCarousel';
import AttachmentView from './Attachments/AttachmentView';
+import BlockingView from './BlockingViews/BlockingView';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
+import FullScreenLoadingIndicator from './FullscreenLoadingIndicator';
import HeaderGap from './HeaderGap';
import HeaderWithBackButton from './HeaderWithBackButton';
import * as Expensicons from './Icon/Expensicons';
+import * as Illustrations from './Icon/Illustrations';
import sourcePropTypes from './Image/sourcePropTypes';
import Modal from './Modal';
import SafeAreaConsumer from './SafeAreaConsumer';
@@ -61,6 +65,9 @@ const propTypes = {
/** Optional callback to fire when we want to do something after modal hide. */
onModalHide: PropTypes.func,
+ /** Trigger when we explicity click close button in ProfileAttachment modal */
+ onModalClose: PropTypes.func,
+
/** Optional callback to fire when we want to do something after attachment carousel changes. */
onCarouselAttachmentChange: PropTypes.func,
@@ -85,6 +92,12 @@ const propTypes = {
/** The transaction associated with the receipt attachment, if any */
transaction: transactionPropTypes,
+ /** The data is loading or not */
+ isLoading: PropTypes.bool,
+
+ /** Should display not found page or not */
+ shouldShowNotFoundPage: PropTypes.bool,
+
...withLocalizePropTypes,
...windowDimensionsPropTypes,
@@ -114,6 +127,9 @@ const defaultProps = {
onModalHide: () => {},
onCarouselAttachmentChange: () => {},
isWorkspaceAvatar: false,
+ onModalClose: () => {},
+ isLoading: false,
+ shouldShowNotFoundPage: false,
isReceiptAttachment: false,
canEditReceipt: false,
};
@@ -352,7 +368,13 @@ function AttachmentModal(props) {
*/
const closeModal = useCallback(() => {
setIsModalOpen(false);
- }, []);
+
+ if (typeof props.onModalClose === 'function') {
+ props.onModalClose();
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.onModalClose]);
/**
* open the modal
@@ -458,9 +480,22 @@ function AttachmentModal(props) {
shouldShowThreeDotsButton={shouldShowThreeDotsButton}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)}
threeDotsMenuItems={threeDotsMenuItems}
- shouldOverlay
+ shouldOverlayDots
/>
+ {props.isLoading && }
+ {props.shouldShowNotFoundPage && !props.isLoading && (
+ Navigation.dismissModal()}
+ />
+ )}
{!_.isEmpty(props.report) && !props.isReceiptAttachment ? (
) : (
Boolean(sourceForAttachmentView) &&
- shouldLoadAttachment && (
+ shouldLoadAttachment &&
+ !props.isLoading &&
+ !props.shouldShowNotFoundPage && (
{
+ if (typeof onViewPhotoPress !== 'function') {
+ show();
+ return;
+ }
+ onViewPhotoPress();
+ },
});
}
diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx
index d60a41e0f263..ade1513c8613 100644
--- a/src/components/Composer/index.android.tsx
+++ b/src/components/Composer/index.android.tsx
@@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import type {TextInput} from 'react-native';
import {StyleSheet} from 'react-native';
import RNTextInput from '@components/RNTextInput';
+import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ComposerUtils from '@libs/ComposerUtils';
@@ -28,6 +29,7 @@ function Composer(
ref: ForwardedRef,
) {
const textInput = useRef(null);
+ const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const styles = useThemeStyles();
const theme = useTheme();
@@ -89,6 +91,12 @@ function Composer(
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
readOnly={isDisabled}
+ onBlur={(e) => {
+ if (!isFocused) {
+ shouldResetFocus.current = true; // detect the input is blurred when the page is hidden
+ }
+ props?.onBlur?.(e);
+ }}
/>
);
}
diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx
index b1357fef9a46..07736e5ddcba 100644
--- a/src/components/Composer/index.ios.tsx
+++ b/src/components/Composer/index.ios.tsx
@@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import type {TextInput} from 'react-native';
import {StyleSheet} from 'react-native';
import RNTextInput from '@components/RNTextInput';
+import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ComposerUtils from '@libs/ComposerUtils';
@@ -28,7 +29,7 @@ function Composer(
ref: ForwardedRef,
) {
const textInput = useRef(null);
-
+ const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const styles = useThemeStyles();
const theme = useTheme();
@@ -84,6 +85,12 @@ function Composer(
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
readOnly={isDisabled}
+ onBlur={(e) => {
+ if (!isFocused) {
+ shouldResetFocus.current = true; // detect the input is blurred when the page is hidden
+ }
+ props?.onBlur?.(e);
+ }}
/>
);
}
diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.tsx
similarity index 62%
rename from src/components/CountrySelector.js
rename to src/components/CountrySelector.tsx
index 68a6486bce48..589530cd7879 100644
--- a/src/components/CountrySelector.js
+++ b/src/components/CountrySelector.tsx
@@ -1,39 +1,30 @@
-import PropTypes from 'prop-types';
-import React, {useEffect} from 'react';
+import React, {forwardRef, useEffect} from 'react';
+import type {ForwardedRef} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
+import type {Country} from '@src/CONST';
import ROUTES from '@src/ROUTES';
import FormHelpMessage from './FormHelpMessage';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
-import refPropTypes from './refPropTypes';
-const propTypes = {
+type CountrySelectorProps = {
/** Form error text. e.g when no country is selected */
- errorText: PropTypes.string,
+ errorText?: string;
/** Callback called when the country changes. */
- onInputChange: PropTypes.func.isRequired,
+ onInputChange: (value?: string) => void;
/** Current selected country */
- value: PropTypes.string,
+ value?: Country;
/** inputID used by the Form component */
// eslint-disable-next-line react/no-unused-prop-types
- inputID: PropTypes.string.isRequired,
-
- /** React ref being forwarded to the MenuItemWithTopDescription */
- forwardedRef: refPropTypes,
-};
-
-const defaultProps = {
- errorText: '',
- value: undefined,
- forwardedRef: () => {},
+ inputID: string;
};
-function CountrySelector({errorText, value: countryCode, onInputChange, forwardedRef}) {
+function CountrySelector({errorText = '', value: countryCode, onInputChange}: CountrySelectorProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -51,12 +42,12 @@ function CountrySelector({errorText, value: countryCode, onInputChange, forwarde
{
const activeRoute = Navigation.getActiveRouteWithoutParams();
- Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.getRoute(countryCode, activeRoute));
+ Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute));
}}
/>
@@ -66,18 +57,6 @@ function CountrySelector({errorText, value: countryCode, onInputChange, forwarde
);
}
-CountrySelector.propTypes = propTypes;
-CountrySelector.defaultProps = defaultProps;
CountrySelector.displayName = 'CountrySelector';
-const CountrySelectorWithRef = React.forwardRef((props, ref) => (
-
-));
-
-CountrySelectorWithRef.displayName = 'CountrySelectorWithRef';
-
-export default CountrySelectorWithRef;
+export default forwardRef(CountrySelector);
diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts
index b0959d43aa96..2e6f36d5cc07 100644
--- a/src/components/DisplayNames/types.ts
+++ b/src/components/DisplayNames/types.ts
@@ -12,7 +12,7 @@ type DisplayNameWithTooltip = {
login?: string;
/** The avatar for the tooltip fallback */
- avatar: AvatarSource;
+ avatar?: AvatarSource;
};
type DisplayNamesProps = {
diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js
index e627119270dd..832715e3214c 100644
--- a/src/components/EmojiPicker/EmojiPickerButton.js
+++ b/src/components/EmojiPicker/EmojiPickerButton.js
@@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import getButtonState from '@libs/getButtonState';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
+import CONST from '@src/CONST';
const propTypes = {
/** Flag to disable the emoji picker button */
@@ -22,6 +23,9 @@ const propTypes = {
/** Unique id for emoji picker */
emojiPickerID: PropTypes.string,
+ /** Emoji popup anchor offset shift vertical */
+ shiftVertical: PropTypes.number,
+
...withLocalizePropTypes,
};
@@ -29,6 +33,7 @@ const defaultProps = {
isDisabled: false,
id: '',
emojiPickerID: '',
+ shiftVertical: 0,
};
function EmojiPickerButton(props) {
@@ -49,7 +54,18 @@ function EmojiPickerButton(props) {
return;
}
if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) {
- EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor, undefined, () => {}, props.emojiPickerID);
+ EmojiPickerAction.showEmojiPicker(
+ props.onModalHide,
+ props.onEmojiSelected,
+ emojiPopoverAnchor,
+ {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ shiftVertical: props.shiftVertical,
+ },
+ () => {},
+ props.emojiPickerID,
+ );
} else {
EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker();
}
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 6cbfde0645de..a0f24b06db7f 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {Keyboard, View} from 'react-native';
+import {Keyboard, StyleSheet, View} from 'react-native';
import AvatarWithDisplayName from '@components/AvatarWithDisplayName';
import Header from '@components/Header';
import Icon from '@components/Icon';
@@ -52,6 +52,7 @@ function HeaderWithBackButton({
threeDotsMenuItems = [],
shouldEnableDetailPageNavigation = false,
children = null,
+ shouldOverlayDots = false,
shouldOverlay = false,
singleExecution = (func) => func,
shouldNavigateToTopMostReport = false,
@@ -69,7 +70,7 @@ function HeaderWithBackButton({
// Hover on some part of close icons will not work on Electron if dragArea is true
// https://github.com/Expensify/App/issues/29598
dataSet={{dragArea: false}}
- style={[styles.headerBar, shouldShowBorderBottom && styles.borderBottom, shouldShowBackButton && styles.pl2]}
+ style={[styles.headerBar, shouldShowBorderBottom && styles.borderBottom, shouldShowBackButton && styles.pl2, shouldOverlay && StyleSheet.absoluteFillObject]}
>
{shouldShowBackButton && (
@@ -163,7 +164,7 @@ function HeaderWithBackButton({
menuItems={threeDotsMenuItems}
onIconPress={onThreeDotsButtonPress}
anchorPosition={threeDotsAnchorPosition}
- shouldOverlay={shouldOverlay}
+ shouldOverlay={shouldOverlayDots}
/>
)}
{shouldShowCloseButton && (
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 8ac21698c32b..725d14e041a7 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -108,6 +108,9 @@ type HeaderWithBackButtonProps = Partial & {
/** Whether we should enable detail page navigation */
shouldEnableDetailPageNavigation?: boolean;
+
+ /** Whether we should overlay the 3 dots menu */
+ shouldOverlayDots?: boolean;
};
export type {ThreeDotsMenuItem};
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 797e6f34fc75..364fb03a2055 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -107,6 +107,7 @@ import ReceiptSearch from '@assets/images/receipt-search.svg';
import Receipt from '@assets/images/receipt.svg';
import Rotate from '@assets/images/rotate-image.svg';
import RotateLeft from '@assets/images/rotate-left.svg';
+import Scan from '@assets/images/scan.svg';
import Send from '@assets/images/send.svg';
import Shield from '@assets/images/shield.svg';
import AppleLogo from '@assets/images/signIn/apple-logo.svg';
@@ -243,6 +244,7 @@ export {
ReceiptSearch,
Rotate,
RotateLeft,
+ Scan,
Send,
Shield,
Sync,
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index 04c8397bc33b..89cceadc0fb0 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -1,7 +1,6 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
-import type {SyntheticEvent} from 'react';
import {Dimensions} from 'react-native';
-import type {EmitterSubscription, NativeTouchEvent} from 'react-native';
+import type {EmitterSubscription, GestureResponderEvent} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
@@ -68,8 +67,8 @@ function KYCWall({
walletTerms,
shouldShowPersonalBankAccountOption = false,
}: BaseKYCWallProps) {
- const anchorRef = useRef(null);
- const transferBalanceButtonRef = useRef(null);
+ const anchorRef = useRef(null);
+ const transferBalanceButtonRef = useRef(null);
const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
@@ -146,7 +145,7 @@ function KYCWall({
*
*/
const continueAction = useCallback(
- (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => {
+ (event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: TransferMethod) => {
const currentSource = walletTerms?.source ?? source;
/**
@@ -161,7 +160,7 @@ function KYCWall({
}
// Use event target as fallback if anchorRef is null for safety
- const targetElement = anchorRef.current ?? (event?.nativeEvent.target as HTMLDivElement);
+ const targetElement = anchorRef.current ?? (event?.currentTarget as HTMLElement);
transferBalanceButtonRef.current = targetElement;
@@ -203,7 +202,7 @@ function KYCWall({
Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');
- onSuccessfulKYC(currentSource, iouPaymentType);
+ onSuccessfulKYC(iouPaymentType, currentSource);
},
[
bankAccountList,
diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts
index aee5b569cc46..1fe16d4a17c3 100644
--- a/src/components/KYCWall/types.ts
+++ b/src/components/KYCWall/types.ts
@@ -1,5 +1,5 @@
-import type {ForwardedRef, SyntheticEvent} from 'react';
-import type {NativeTouchEvent} from 'react-native';
+import type {ForwardedRef} from 'react';
+import type {GestureResponderEvent} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
@@ -64,10 +64,10 @@ type KYCWallProps = {
shouldShowPersonalBankAccountOption?: boolean;
/** Callback for the end of the onContinue trigger on option selection */
- onSuccessfulKYC: (currentSource?: Source, iouPaymentType?: TransferMethod) => void;
+ onSuccessfulKYC: (iouPaymentType?: TransferMethod, currentSource?: Source) => void;
/** Children to build the KYC */
- children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void;
+ children: (continueAction: (event: GestureResponderEvent | KeyboardEvent | undefined, method: TransferMethod) => void, anchorRef: ForwardedRef) => void;
};
export type {AnchorPosition, KYCWallProps, PaymentMethod, TransferMethod, DomRect};
diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js
deleted file mode 100644
index 4c8b0e1102b9..000000000000
--- a/src/components/LHNOptionsList/LHNOptionsList.js
+++ /dev/null
@@ -1,222 +0,0 @@
-import {FlashList} from '@shopify/flash-list';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useCallback} from 'react';
-import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import participantPropTypes from '@components/participantPropTypes';
-import transactionPropTypes from '@components/transactionPropTypes';
-import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID';
-import usePermissions from '@hooks/usePermissions';
-import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import {transactionViolationsPropType} from '@libs/Violations/propTypes';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
-import reportPropTypes from '@pages/reportPropTypes';
-import stylePropTypes from '@styles/stylePropTypes';
-import variables from '@styles/variables';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import OptionRowLHNData from './OptionRowLHNData';
-
-const propTypes = {
- /** Wrapper style for the section list */
- style: stylePropTypes,
-
- /** Extra styles for the section list container */
- contentContainerStyles: stylePropTypes.isRequired,
-
- /** Sections for the section list */
- data: PropTypes.arrayOf(PropTypes.string).isRequired,
-
- /** Callback to fire when a row is selected */
- onSelectRow: PropTypes.func.isRequired,
-
- /** Toggle between compact and default view of the option */
- optionMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)).isRequired,
-
- /** Whether to allow option focus or not */
- shouldDisableFocusOptions: PropTypes.bool,
-
- /** The policy which the user has access to and which the report could be tied to */
- policy: PropTypes.shape({
- /** The ID of the policy */
- id: PropTypes.string,
- /** Name of the policy */
- name: PropTypes.string,
- /** Avatar of the policy */
- avatar: PropTypes.string,
- }),
-
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
-
- /** Array of report actions for this report */
- reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
-
- /** Indicates which locale the user currently has selected */
- preferredLocale: PropTypes.string,
-
- /** List of users' personal details */
- personalDetails: PropTypes.objectOf(participantPropTypes),
-
- /** The transaction from the parent report action */
- transactions: PropTypes.objectOf(transactionPropTypes),
-
- /** List of draft comments */
- draftComments: PropTypes.objectOf(PropTypes.string),
-
- /** The list of transaction violations */
- transactionViolations: transactionViolationsPropType,
-
- ...withCurrentReportIDPropTypes,
-};
-
-const defaultProps = {
- style: undefined,
- shouldDisableFocusOptions: false,
- reportActions: {},
- reports: {},
- policy: {},
- preferredLocale: CONST.LOCALES.DEFAULT,
- personalDetails: {},
- transactions: {},
- draftComments: {},
- transactionViolations: {},
- ...withCurrentReportIDDefaultProps,
-};
-
-const keyExtractor = (item) => `report_${item}`;
-
-function LHNOptionsList({
- style,
- contentContainerStyles,
- data,
- onSelectRow,
- optionMode,
- shouldDisableFocusOptions,
- reports,
- reportActions,
- policy,
- preferredLocale,
- personalDetails,
- transactions,
- draftComments,
- currentReportID,
- transactionViolations,
-}) {
- const styles = useThemeStyles();
- const {canUseViolations} = usePermissions();
- /**
- * Function which renders a row in the list
- *
- * @param {Object} params
- * @param {Object} params.item
- *
- * @return {Component}
- */
- const renderItem = useCallback(
- ({item: reportID}) => {
- const itemFullReport = reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {};
- const itemReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
- const itemParentReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`] || {};
- const itemParentReportAction = itemParentReportActions[itemFullReport.parentReportActionID] || {};
- const itemPolicy = policy[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport.policyID}`] || {};
- const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], '');
- const itemTransaction = transactionID ? transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {};
- const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || '';
- const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport.ownerAccountID, itemParentReportAction.actorAccountID];
-
- const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails);
-
- return (
-
- );
- },
- [
- currentReportID,
- draftComments,
- onSelectRow,
- optionMode,
- personalDetails,
- policy,
- preferredLocale,
- reportActions,
- reports,
- shouldDisableFocusOptions,
- transactions,
- transactionViolations,
- canUseViolations,
- ],
- );
-
- return (
-
-
-
- );
-}
-
-LHNOptionsList.propTypes = propTypes;
-LHNOptionsList.defaultProps = defaultProps;
-LHNOptionsList.displayName = 'LHNOptionsList';
-
-export default compose(
- withCurrentReportID,
- withOnyx({
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- reportActions: {
- key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
- },
- policy: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- preferredLocale: {
- key: ONYXKEYS.NVP_PREFERRED_LOCALE,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- transactions: {
- key: ONYXKEYS.COLLECTION.TRANSACTION,
- },
- draftComments: {
- key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT,
- },
- transactionViolations: {
- key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
- },
- }),
-)(LHNOptionsList);
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
new file mode 100644
index 000000000000..ecf320807b48
--- /dev/null
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -0,0 +1,141 @@
+import {FlashList} from '@shopify/flash-list';
+import type {ReactElement} from 'react';
+import React, {useCallback} from 'react';
+import {StyleSheet, View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import withCurrentReportID from '@components/withCurrentReportID';
+import usePermissions from '@hooks/usePermissions';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import OptionRowLHNData from './OptionRowLHNData';
+import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types';
+
+const keyExtractor = (item: string) => `report_${item}`;
+
+function LHNOptionsList({
+ style,
+ contentContainerStyles,
+ data,
+ onSelectRow,
+ optionMode,
+ shouldDisableFocusOptions = false,
+ reports = {},
+ reportActions = {},
+ policy = {},
+ preferredLocale = CONST.LOCALES.DEFAULT,
+ personalDetails = {},
+ transactions = {},
+ currentReportID = '',
+ draftComments = {},
+ transactionViolations = {},
+}: LHNOptionsListProps) {
+ const styles = useThemeStyles();
+ const {canUseViolations} = usePermissions();
+ /**
+ * Function which renders a row in the list
+ */
+ const renderItem = useCallback(
+ ({item: reportID}: RenderItemProps): ReactElement => {
+ const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null;
+ const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null;
+ const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null;
+ const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null;
+ const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null;
+ const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : '';
+ const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null;
+ const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? '';
+ const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID];
+ const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails);
+
+ return (
+
+ );
+ },
+ [
+ currentReportID,
+ draftComments,
+ onSelectRow,
+ optionMode,
+ personalDetails,
+ policy,
+ preferredLocale,
+ reportActions,
+ reports,
+ shouldDisableFocusOptions,
+ transactions,
+ transactionViolations,
+ canUseViolations,
+ ],
+ );
+
+ return (
+
+
+
+ );
+}
+
+LHNOptionsList.displayName = 'LHNOptionsList';
+
+export default withCurrentReportID(
+ withOnyx({
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ reportActions: {
+ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ },
+ policy: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+ preferredLocale: {
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ },
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ transactions: {
+ key: ONYXKEYS.COLLECTION.TRANSACTION,
+ },
+ draftComments: {
+ key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT,
+ },
+ transactionViolations: {
+ key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
+ },
+ })(LHNOptionsList),
+);
+
+export type {LHNOptionsListProps};
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.tsx
similarity index 66%
rename from src/components/LHNOptionsList/OptionRowLHN.js
rename to src/components/LHNOptionsList/OptionRowLHN.tsx
index fc4f05eefd22..b085625c2914 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -1,9 +1,7 @@
import {useFocusEffect} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback, useRef, useState} from 'react';
+import type {GestureResponderEvent, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
-import _ from 'underscore';
import DisplayNames from '@components/DisplayNames';
import Hoverable from '@components/Hoverable';
import Icon from '@components/Icon';
@@ -27,51 +25,18 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag
import * as ReportUtils from '@libs/ReportUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {OptionRowLHNProps} from './types';
-const propTypes = {
- /** Style for hovered state */
- // eslint-disable-next-line react/forbid-prop-types
- hoverStyle: PropTypes.object,
-
- /** The ID of the report that the option is for */
- reportID: PropTypes.string.isRequired,
-
- /** Whether this option is currently in focus so we can modify its style */
- isFocused: PropTypes.bool,
-
- /** A function that is called when an option is selected. Selected option is passed as a param */
- onSelectRow: PropTypes.func,
-
- /** Toggle between compact and default view */
- viewMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)),
-
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
- /** The item that should be rendered */
- // eslint-disable-next-line react/forbid-prop-types
- optionItem: PropTypes.object,
-};
-
-const defaultProps = {
- hoverStyle: undefined,
- viewMode: 'default',
- onSelectRow: () => {},
- style: null,
- optionItem: null,
- isFocused: false,
-};
-
-function OptionRowLHN(props) {
+function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style}: OptionRowLHNProps) {
const theme = useTheme();
const styles = useThemeStyles();
+ const popoverAnchor = useRef(null);
const StyleUtils = useStyleUtils();
- const popoverAnchor = useRef(null);
const isFocusedRef = useRef(true);
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
-
- const optionItem = props.optionItem;
const [isContextMenuActive, setIsContextMenuActive] = useState(false);
useFocusEffect(
@@ -87,42 +52,36 @@ function OptionRowLHN(props) {
return null;
}
- const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
- if (isHidden && !props.isFocused && !optionItem.isPinned) {
+ const isHidden = optionItem?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+ if (isHidden && !isFocused && !optionItem?.isPinned) {
return null;
}
- const textStyle = props.isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
- const textUnreadStyle = optionItem.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
- const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], props.style);
- const alternateTextStyle = StyleUtils.combineStyles(
- props.viewMode === CONST.OPTION_MODE.COMPACT
- ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2]
- : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting],
- props.style,
- );
- const contentContainerStyles =
- props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1];
- const sidebarInnerRowStyle = StyleSheet.flatten(
- props.viewMode === CONST.OPTION_MODE.COMPACT
+ const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT;
+ const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
+ const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
+ const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style];
+ const alternateTextStyle = isInFocusMode
+ ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style]
+ : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style];
+
+ const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1];
+ const sidebarInnerRowStyle = StyleSheet.flatten(
+ isInFocusMode
? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter]
: [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter],
);
- const hoveredBackgroundColor =
- (props.hoverStyle || styles.sidebarLinkHover) && (props.hoverStyle || styles.sidebarLinkHover).backgroundColor
- ? (props.hoverStyle || styles.sidebarLinkHover).backgroundColor
- : theme.sidebar;
+ const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction);
-
/**
* Show the ReportActionContextMenu modal popover.
*
- * @param {Object} [event] - A press event.
+ * @param [event] - A press event.
*/
- const showPopover = (event) => {
+ const showPopover = (event: MouseEvent | GestureResponderEvent) => {
if (!isFocusedRef.current && isSmallScreenWidth) {
return;
}
@@ -131,33 +90,32 @@ function OptionRowLHN(props) {
CONST.CONTEXT_MENU_TYPES.REPORT,
event,
'',
- popoverAnchor,
- props.reportID,
+ popoverAnchor.current,
+ reportID,
'0',
- props.reportID,
+ reportID,
undefined,
() => {},
() => setIsContextMenuActive(false),
false,
false,
optionItem.isPinned,
- optionItem.isUnread,
+ !!optionItem.isUnread,
);
};
- const emojiCode = lodashGet(optionItem, 'status.emojiCode', '');
- const statusText = lodashGet(optionItem, 'status.text', '');
- const statusClearAfterDate = lodashGet(optionItem, 'status.clearAfter', '');
+ const emojiCode = optionItem.status?.emojiCode ?? '';
+ const statusText = optionItem.status?.text ?? '';
+ const statusClearAfterDate = optionItem.status?.clearAfter ?? '';
const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate);
const statusContent = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText;
- const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(ReportUtils.getReport(optionItem.reportID));
+ const report = ReportUtils.getReport(optionItem.reportID ?? '');
+ const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null);
- const isGroupChat =
- optionItem.type === CONST.REPORT.TYPE.CHAT && _.isEmpty(optionItem.chatType) && !optionItem.isThread && lodashGet(optionItem, 'displayNamesWithTooltips.length', 0) > 2;
- const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem.reportID)) : optionItem.text;
-
- const subscriptAvatarBorderColor = props.isFocused ? focusedBackgroundColor : theme.sidebar;
+ const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2;
+ const fullTitle = isGroupChat ? getGroupChatName(!isEmptyObject(report) ? report : null) : optionItem.text;
+ const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar;
return (
(
{
- if (e) {
- e.preventDefault();
- }
+ onPress={(event) => {
+ event?.preventDefault();
// Enable Composer to focus on clicking the same chat after opening the context menu.
ReportActionComposeFocusManager.focus();
- props.onSelectRow(optionItem, popoverAnchor);
+ onSelectRow(optionItem, popoverAnchor);
}}
- onMouseDown={(e) => {
+ onMouseDown={(event) => {
// Allow composer blur on right click
- if (!e) {
+ if (!event) {
return;
}
// Prevent composer blur on left click
- e.preventDefault();
+ event.preventDefault();
}}
testID={optionItem.reportID}
- onSecondaryInteraction={(e) => {
- showPopover(e);
+ onSecondaryInteraction={(event) => {
+ showPopover(event);
// Ensure that we blur the composer when opening context menu, so that only one component is focused at a time
if (DomUtils.getActiveElement()) {
- DomUtils.getActiveElement().blur();
+ (DomUtils.getActiveElement() as HTMLElement | null)?.blur();
}
}}
withoutFocusOnSecondaryInteraction
@@ -203,32 +159,32 @@ function OptionRowLHN(props) {
styles.sidebarLink,
styles.sidebarLinkInnerLHN,
StyleUtils.getBackgroundColorStyle(theme.sidebar),
- props.isFocused ? styles.sidebarLinkActive : null,
- (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle || styles.sidebarLinkHover : null,
+ isFocused ? styles.sidebarLinkActive : null,
+ (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null,
]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.navigatesToChat')}
- needsOffscreenAlphaCompositing={props.optionItem.icons.length >= 2}
+ needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2}
>
- {!_.isEmpty(optionItem.icons) &&
+ {(optionItem.icons?.length ?? 0) > 0 &&
(optionItem.shouldShowSubscript ? (
) : (
@@ -237,13 +193,17 @@ function OptionRowLHN(props) {
{isStatusVisible && (
@@ -265,7 +225,7 @@ function OptionRowLHN(props) {
) : null}
- {optionItem.descriptiveText ? (
+ {optionItem?.descriptiveText ? (
{optionItem.descriptiveText}
@@ -324,10 +284,6 @@ function OptionRowLHN(props) {
);
}
-OptionRowLHN.propTypes = propTypes;
-OptionRowLHN.defaultProps = defaultProps;
OptionRowLHN.displayName = 'OptionRowLHN';
export default React.memo(OptionRowLHN);
-
-export {propTypes, defaultProps};
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.tsx
similarity index 56%
rename from src/components/LHNOptionsList/OptionRowLHNData.js
rename to src/components/LHNOptionsList/OptionRowLHNData.tsx
index 8bdf065a94fd..dca74e880169 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.js
+++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx
@@ -1,65 +1,14 @@
import {deepEqual} from 'fast-equals';
-import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useRef} from 'react';
-import _ from 'underscore';
-import participantPropTypes from '@components/participantPropTypes';
-import transactionPropTypes from '@components/transactionPropTypes';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import SidebarUtils from '@libs/SidebarUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
-import {transactionViolationsPropType} from '@libs/Violations/propTypes';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
-import OptionRowLHN, {defaultProps as baseDefaultProps, propTypes as basePropTypes} from './OptionRowLHN';
-
-const propTypes = {
- /** Whether row should be focused */
- isFocused: PropTypes.bool,
-
- /** List of users' personal details */
- personalDetails: PropTypes.objectOf(participantPropTypes),
-
- /** The preferred language for the app */
- preferredLocale: PropTypes.string,
-
- /** The full data of the report */
- // eslint-disable-next-line react/forbid-prop-types
- fullReport: PropTypes.object,
-
- /** The policy which the user has access to and which the report could be tied to */
- policy: PropTypes.shape({
- /** The ID of the policy */
- id: PropTypes.string,
- /** Name of the policy */
- name: PropTypes.string,
- /** Avatar of the policy */
- avatar: PropTypes.string,
- }),
-
- /** The action from the parent report */
- parentReportAction: PropTypes.shape(reportActionPropTypes),
-
- /** The transaction from the parent report action */
- transaction: transactionPropTypes,
-
- /** Any violations associated with the transaction */
- transactionViolations: transactionViolationsPropType,
-
- ...basePropTypes,
-};
-
-const defaultProps = {
- isFocused: false,
- personalDetails: {},
- fullReport: {},
- policy: {},
- parentReportAction: {},
- transaction: {},
- preferredLocale: CONST.LOCALES.DEFAULT,
- ...baseDefaultProps,
-};
+import type {OptionData} from '@src/libs/ReportUtils';
+import OptionRowLHN from './OptionRowLHN';
+import type {OptionRowLHNDataProps} from './types';
/*
* This component gets the data from onyx for the actual
@@ -68,11 +17,11 @@ const defaultProps = {
* re-render if the data really changed.
*/
function OptionRowLHNData({
- isFocused,
+ isFocused = false,
fullReport,
reportActions,
- personalDetails,
- preferredLocale,
+ personalDetails = {},
+ preferredLocale = CONST.LOCALES.DEFAULT,
comment,
policy,
receiptTransactions,
@@ -81,18 +30,18 @@ function OptionRowLHNData({
transactionViolations,
canUseViolations,
...propsToForward
-}) {
+}: OptionRowLHNDataProps) {
const reportID = propsToForward.reportID;
- const optionItemRef = useRef();
+ const optionItemRef = useRef();
const linkedTransaction = useMemo(() => {
const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions);
- const lastReportAction = _.first(sortedReportActions);
+ const lastReportAction = sortedReportActions[0];
return TransactionUtils.getLinkedTransaction(lastReportAction);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fullReport.reportID, receiptTransactions, reportActions]);
+ }, [fullReport?.reportID, receiptTransactions, reportActions]);
- const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction);
+ const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null);
const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
@@ -100,15 +49,17 @@ function OptionRowLHNData({
report: fullReport,
reportActions,
personalDetails,
- preferredLocale,
+ preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT,
policy,
parentReportAction,
- hasViolations,
+ hasViolations: !!hasViolations,
});
if (deepEqual(item, optionItemRef.current)) {
return optionItemRef.current;
}
+
optionItemRef.current = item;
+
return item;
// Listen parentReportAction to update title of thread report when parentReportAction changed
// Listen to transaction to update title of transaction report when transaction changed
@@ -116,7 +67,7 @@ function OptionRowLHNData({
}, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction, transactionViolations, canUseViolations]);
useEffect(() => {
- if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) {
+ if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) {
return;
}
Report.setReportWithDraft(reportID, true);
@@ -133,8 +84,6 @@ function OptionRowLHNData({
);
}
-OptionRowLHNData.propTypes = propTypes;
-OptionRowLHNData.defaultProps = defaultProps;
OptionRowLHNData.displayName = 'OptionRowLHNData';
/**
diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts
new file mode 100644
index 000000000000..24cebb8e3da2
--- /dev/null
+++ b/src/components/LHNOptionsList/types.ts
@@ -0,0 +1,124 @@
+import type {ContentStyle} from '@shopify/flash-list';
+import type {RefObject} from 'react';
+import type {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import type {CurrentReportIDContextValue} from '@components/withCurrentReportID';
+import type CONST from '@src/CONST';
+import type {OptionData} from '@src/libs/ReportUtils';
+import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx';
+
+type OptionMode = ValueOf;
+
+type LHNOptionsListOnyxProps = {
+ /** The policy which the user has access to and which the report could be tied to */
+ policy: OnyxCollection;
+
+ /** All reports shared with the user */
+ reports: OnyxCollection;
+
+ /** Array of report actions for this report */
+ reportActions: OnyxCollection;
+
+ /** Indicates which locale the user currently has selected */
+ preferredLocale: OnyxEntry;
+
+ /** List of users' personal details */
+ personalDetails: OnyxEntry;
+
+ /** The transaction from the parent report action */
+ transactions: OnyxCollection;
+
+ /** List of draft comments */
+ draftComments: OnyxCollection;
+
+ /** The list of transaction violations */
+ transactionViolations: OnyxCollection;
+};
+
+type CustomLHNOptionsListProps = {
+ /** Wrapper style for the section list */
+ style?: StyleProp;
+
+ /** Extra styles for the section list container */
+ contentContainerStyles?: StyleProp;
+
+ /** Sections for the section list */
+ data: string[];
+
+ /** Callback to fire when a row is selected */
+ onSelectRow: (reportID: string) => void;
+
+ /** Toggle between compact and default view of the option */
+ optionMode: OptionMode;
+
+ /** Whether to allow option focus or not */
+ shouldDisableFocusOptions?: boolean;
+};
+
+type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps;
+
+type OptionRowLHNDataProps = {
+ /** Whether row should be focused */
+ isFocused?: boolean;
+
+ /** List of users' personal details */
+ personalDetails?: PersonalDetailsList;
+
+ /** The preferred language for the app */
+ preferredLocale?: OnyxEntry;
+
+ /** The full data of the report */
+ fullReport: OnyxEntry;
+
+ /** The policy which the user has access to and which the report could be tied to */
+ policy?: OnyxEntry;
+
+ /** The action from the parent report */
+ parentReportAction?: OnyxEntry;
+
+ /** The transaction from the parent report action */
+ transaction: OnyxEntry;
+
+ /** Comment added to report */
+ comment: string;
+
+ /** The receipt transaction from the parent report action */
+ receiptTransactions: OnyxCollection;
+
+ /** The reportID of the report */
+ reportID: string;
+
+ /** Array of report actions for this report */
+ reportActions: OnyxEntry;
+
+ /** List of transaction violation */
+ transactionViolations: OnyxCollection;
+
+ /** Whether the user can use violations */
+ canUseViolations: boolean | undefined;
+};
+
+type OptionRowLHNProps = {
+ /** The ID of the report that the option is for */
+ reportID: string;
+
+ /** Whether this option is currently in focus so we can modify its style */
+ isFocused?: boolean;
+
+ /** A function that is called when an option is selected. Selected option is passed as a param */
+ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void;
+
+ /** Toggle between compact and default view */
+ viewMode?: OptionMode;
+
+ /** Additional style props */
+ style?: StyleProp;
+
+ /** The item that should be rendered */
+ optionItem?: OptionData;
+};
+
+type RenderItemProps = {item: string};
+
+export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps};
diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js
index 59465e34eec0..8614736d200f 100644
--- a/src/components/MagicCodeInput.js
+++ b/src/components/MagicCodeInput.js
@@ -61,6 +61,9 @@ const propTypes = {
/** Last pressed digit on BigDigitPad */
lastPressedDigit: PropTypes.string,
+
+ /** TestID for test */
+ testID: PropTypes.string,
};
const defaultProps = {
@@ -77,6 +80,7 @@ const defaultProps = {
maxLength: CONST.MAGIC_CODE_LENGTH,
isDisableKeyboard: false,
lastPressedDigit: '',
+ testID: '',
};
/**
@@ -394,6 +398,7 @@ function MagicCodeInput(props) {
role={CONST.ACCESSIBILITY_ROLE.TEXT}
style={[styles.inputTransparent]}
textInputContainerStyles={[styles.borderNone]}
+ testID={props.testID}
/>
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index 6e5b4eddae9e..a250e21c0021 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -9,6 +9,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import ComposerFocusManager from '@libs/ComposerFocusManager';
+import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay';
import useNativeDriver from '@libs/useNativeDriver';
import variables from '@styles/variables';
import * as Modal from '@userActions/Modal';
@@ -38,6 +39,7 @@ function BaseModal(
onLayout,
avoidKeyboard = false,
children,
+ shouldUseCustomBackdrop = false,
}: BaseModalProps,
ref: React.ForwardedRef,
) {
@@ -185,7 +187,7 @@ function BaseModal(
swipeDirection={swipeDirection}
isVisible={isVisible}
backdropColor={theme.overlay}
- backdropOpacity={hideBackdrop ? 0 : variables.overlayOpacity}
+ backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity}
backdropTransitionOutTiming={0}
hasBackdrop={fullscreen}
coverScreen={fullscreen}
@@ -201,6 +203,7 @@ function BaseModal(
statusBarTranslucent={statusBarTranslucent}
onLayout={onLayout}
avoidKeyboard={avoidKeyboard}
+ customBackdrop={shouldUseCustomBackdrop ? : undefined}
>
& {
* See: https://github.com/react-native-modal/react-native-modal/pull/116
* */
hideModalContentWhileAnimating?: boolean;
+
+ /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */
+ shouldUseCustomBackdrop?: boolean;
};
export default BaseModalProps;
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index afdc62218f95..7043173b3641 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -57,6 +57,7 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const policyType = policy?.type;
const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(moneyRequestReport, policy);
const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport);
const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && session?.accountID === moneyRequestReport.managerID;
const isPayer = isPaidGroupPolicy
@@ -65,8 +66,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
const shouldShowPayButton = useMemo(
- () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport),
- [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport],
+ () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport) && !isAutoReimbursable,
+ [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport, isAutoReimbursable],
);
const shouldShowApproveButton = useMemo(() => {
if (!isPaidGroupPolicy) {
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index f66e73a2ef02..590154b48bca 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -36,6 +36,7 @@ import Image from './Image';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import optionPropTypes from './optionPropTypes';
import OptionsSelector from './OptionsSelector';
+import ReceiptEmptyState from './ReceiptEmptyState';
import SettlementButton from './SettlementButton';
import ShowMoreButton from './ShowMoreButton';
import Switch from './Switch';
@@ -604,7 +605,7 @@ function MoneyRequestConfirmationList(props) {
)}
- {(receiptImage || receiptThumbnail) && (
+ {receiptImage || receiptThumbnail ? (
+ ) : (
+ // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
+ PolicyUtils.isPaidGroupPolicy(props.policy) &&
+ !props.isDistanceRequest &&
+ props.iouType === CONST.IOU.TYPE.REQUEST && (
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ props.iouType,
+ transaction.transactionID,
+ props.reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
+ )
+ }
+ />
+ )
)}
{props.shouldShowSmartScanFields && (
)}
- {(receiptImage || receiptThumbnail) && (
+ {receiptImage || receiptThumbnail ? (
+ ) : (
+ // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
+ PolicyUtils.isPaidGroupPolicy(policy) &&
+ !isDistanceRequest &&
+ iouType === CONST.IOU.TYPE.REQUEST && (
+
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ )
+ }
+ />
+ )
)}
{shouldShowSmartScanFields && (
diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx
index 678bb6f06403..1bee95532104 100644
--- a/src/components/Picker/BasePicker.tsx
+++ b/src/components/Picker/BasePicker.tsx
@@ -49,10 +49,6 @@ function BasePicker(
// reference to @react-native-picker/picker
const picker = useRef(null);
- // Windows will reuse the text color of the select for each one of the options
- // so we might need to color accordingly so it doesn't blend with the background.
- const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: theme.text} : {};
-
useEffect(() => {
if (!!value || !items || items.length !== 1 || !onInputChange) {
return;
@@ -152,6 +148,10 @@ function BasePicker(
return theme.text;
}, [theme]);
+ // Windows will reuse the text color of the select for each one of the options
+ // so we might need to color accordingly so it doesn't blend with the background.
+ const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: itemColor} : {};
+
const hasError = !!errorText;
if (isDisabled) {
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index b1a6ebb0c5c0..a738d1f9798a 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -95,9 +95,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
closePopover();
};
- document.addEventListener('scroll', listener, true);
+ document.addEventListener('wheel', listener, true);
return () => {
- document.removeEventListener('scroll', listener, true);
+ document.removeEventListener('wheel', listener, true);
};
}, [closePopover]);
diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
index f41a6b389001..1df56093d6a6 100644
--- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
+++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx
@@ -68,7 +68,7 @@ function GenericPressable(
if (shouldUseDisabledCursor) {
return styles.cursorDisabled;
}
- if ([rest.accessibilityRole, rest.role].includes('text')) {
+ if ([rest.accessibilityRole, rest.role].includes(CONST.ROLE.PRESENTATION)) {
return styles.cursorText;
}
return styles.cursorPointer;
diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx
new file mode 100644
index 000000000000..a8cab171ffca
--- /dev/null
+++ b/src/components/PurposeForUsingExpensifyModal.tsx
@@ -0,0 +1,178 @@
+import {useNavigation} from '@react-navigation/native';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {ScrollView, View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as Report from '@userActions/Report';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import HeaderWithBackButton from './HeaderWithBackButton';
+import * as Expensicons from './Icon/Expensicons';
+import Lottie from './Lottie';
+import LottieAnimations from './LottieAnimations';
+import type {MenuItemProps} from './MenuItem';
+import MenuItemList from './MenuItemList';
+import Modal from './Modal';
+import Text from './Text';
+
+// This is not translated because it is a message coming from concierge, which only supports english
+const messageCopy = {
+ [CONST.INTRO_CHOICES.TRACK]:
+ 'Great! To track your expenses, I suggest you create a workspace to keep everything contained:\n' +
+ '\n' +
+ '1. Press your avatar icon\n' +
+ '2. Choose Workspaces\n' +
+ '3. Choose New Workspace\n' +
+ '4. Name your workspace something meaningful (eg, "My Business Expenses")\n' +
+ '\n' +
+ 'Once you have your workspace set up, you can add expenses to it as follows:\n' +
+ '\n' +
+ '1. Choose My Business Expenses (or whatever you named it) in the list of chat rooms\n' +
+ '2. Choose the + button in the chat compose window\n' +
+ '3. Choose Request money\n' +
+ "4. Choose what kind of expense you'd like to log, whether a manual expense, scanned receipt, or tracked distance.\n" +
+ '\n' +
+ "That'll be stored in your My Business Expenses room for your later access. Thanks for asking, and let me know how it goes!",
+ [CONST.INTRO_CHOICES.SUBMIT]:
+ 'Hi there, to submit expenses for reimbursement, please:\n' +
+ '\n' +
+ '1. Press the big green + button\n' +
+ '2. Choose Request money\n' +
+ '3. Indicate how much to request, either manually, by scanning a receipt, or by tracking distance\n' +
+ '4. Enter the email address or phone number of your boss\n' +
+ '\n' +
+ "And we'll take it from there to get you paid back. Please give it a shot and let me know how it goes!",
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]:
+ "Great! To manage your team's expenses, create a workspace to keep everything contained:\n" +
+ '\n' +
+ '1. Press your avatar icon\n' +
+ '2. Choose Workspaces\n' +
+ '3. Choose New Workspace\n' +
+ '4. Name your workspace something meaningful (eg, "Galaxy Food Inc.")\n' +
+ '\n' +
+ 'Once you have your workspace set up, you can invite your team to it via the Members pane and connect a business bank account to reimburse them!',
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]:
+ 'Hi there, to split an expense such as with a friend, please:\n' +
+ '\n' +
+ 'Press the big green + button\n' +
+ 'Choose *Request money*\n' +
+ 'Indicate how much was spent, either manually, by scanning a receipt, or by tracking distance\n' +
+ 'Enter the email address or phone number of your friend\n' +
+ 'Press *Split* next to their name\n' +
+ 'Repeat as many times as you like for each of your friends\n' +
+ 'Press *Add to split* when done adding friends\n' +
+ 'Press Split to split the bill\n' +
+ '\n' +
+ "This will send a money request to each of your friends for however much they owe you, and we'll take care of getting you paid back. Thanks for asking, and let me know how it goes!",
+};
+
+const menuIcons = {
+ [CONST.INTRO_CHOICES.TRACK]: Expensicons.ReceiptSearch,
+ [CONST.INTRO_CHOICES.SUBMIT]: Expensicons.Scan,
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]: Expensicons.MoneyBag,
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: Expensicons.Briefcase,
+};
+
+function PurposeForUsingExpensifyModal() {
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
+ const navigation = useNavigation();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const theme = useTheme();
+
+ useEffect(() => {
+ const navigationState = navigation.getState();
+ const routes = navigationState.routes;
+ const currentRoute = routes[navigationState.index];
+ if (currentRoute && NAVIGATORS.CENTRAL_PANE_NAVIGATOR !== currentRoute.name && currentRoute.name !== SCREENS.HOME) {
+ return;
+ }
+
+ Welcome.show(routes, () => setIsModalOpen(true));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const closeModal = useCallback(() => {
+ Report.dismissEngagementModal();
+ setIsModalOpen(false);
+ }, []);
+
+ const completeModalAndClose = useCallback((message: string, choice: ValueOf) => {
+ Report.completeEngagementModal(message, choice);
+ setIsModalOpen(false);
+ Report.navigateToConciergeChat();
+ }, []);
+
+ const menuItems: MenuItemProps[] = useMemo(
+ () =>
+ Object.values(CONST.INTRO_CHOICES).map((choice) => {
+ const translationKey = `purposeForExpensify.${choice}` as const;
+ return {
+ key: translationKey,
+ title: translate(translationKey),
+ icon: menuIcons[choice],
+ iconRight: Expensicons.ArrowRight,
+ onPress: () => completeModalAndClose(messageCopy[choice], choice),
+ shouldShowRightIcon: true,
+ numberOfLinesTitle: 2,
+ };
+ }),
+ [completeModalAndClose, translate],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {translate('purposeForExpensify.welcomeMessage')}
+
+ {translate('purposeForExpensify.welcomeSubtitle')}
+
+
+
+
+
+ );
+}
+
+PurposeForUsingExpensifyModal.displayName = 'PurposeForUsingExpensifyModal';
+
+export default PurposeForUsingExpensifyModal;
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 4fcca3e518a5..3d1710de1432 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -14,9 +14,11 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
+import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import variables from '@styles/variables';
+import ROUTES from '@src/ROUTES';
import type {PolicyReportField, Report} from '@src/types/onyx';
type MoneyReportViewProps = {
@@ -73,9 +75,9 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}:
{}}
+ onPress={() => Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))}
shouldShowRightIcon
- disabled={false}
+ disabled={ReportUtils.isReportFieldOfTypeTitle(reportField)}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 4c727439b876..3121328138ee 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -350,7 +350,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
isPayer && !isDraftExpenseReport && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled,
- [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, props.iouReport],
+ () => isPayer && !isDraftExpenseReport && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled && !isAutoReimbursable,
+ [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, props.iouReport],
);
const shouldShowApproveButton = useMemo(() => {
if (!isPaidGroupPolicy) {
diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js
index a3394fe71539..64cc9ac7abf3 100644
--- a/src/components/RoomHeaderAvatars.js
+++ b/src/components/RoomHeaderAvatars.js
@@ -4,9 +4,9 @@ import {View} from 'react-native';
import _ from 'underscore';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as UserUtils from '@libs/UserUtils';
+import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
-import AttachmentModal from './AttachmentModal';
+import ROUTES from '@src/ROUTES';
import Avatar from './Avatar';
import avatarPropTypes from './avatarPropTypes';
import PressableWithoutFocus from './Pressable/PressableWithoutFocus';
@@ -14,13 +14,23 @@ import Text from './Text';
const propTypes = {
icons: PropTypes.arrayOf(avatarPropTypes),
+ reportID: PropTypes.string,
};
const defaultProps = {
icons: [],
+ reportID: '',
};
function RoomHeaderAvatars(props) {
+ const navigateToAvatarPage = (icon) => {
+ if (icon.type === CONST.ICON_TYPE_WORKSPACE) {
+ Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(props.reportID));
+ return;
+ }
+ Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id));
+ };
+
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
if (!props.icons.length) {
@@ -29,31 +39,21 @@ function RoomHeaderAvatars(props) {
if (props.icons.length === 1) {
return (
- navigateToAvatarPage(props.icons[0])}
+ accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+ accessibilityLabel={props.icons[0].name}
>
- {({show}) => (
-
-
-
- )}
-
+
+
);
}
@@ -73,31 +73,21 @@ function RoomHeaderAvatars(props) {
key={`${icon.id}${index}`}
style={[styles.justifyContentCenter, styles.alignItemsCenter]}
>
- navigateToAvatarPage(icon)}
+ accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
+ accessibilityLabel={icon.name}
>
- {({show}) => (
-
-
-
- )}
-
+
+
{index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && (
<>
({
});
type ScrollViewWithContextProps = {
- onScroll: (event: NativeSyntheticEvent) => void;
+ onScroll?: (event: NativeSyntheticEvent) => void;
children?: React.ReactNode;
- scrollEventThrottle: number;
-} & Partial;
+ scrollEventThrottle?: number;
+} & Partial;
/*
* is a wrapper around that provides a ref to the .
@@ -54,7 +54,7 @@ function ScrollViewWithContextWithRef({onScroll, scrollEventThrottle, children,
{...restProps}
ref={scrollViewRef}
onScroll={setContextScrollPosition}
- scrollEventThrottle={scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE}
+ scrollEventThrottle={scrollEventThrottle ?? MIN_SMOOTH_SCROLL_EVENT_THROTTLE}
>
{children}
diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.tsx
similarity index 91%
rename from src/components/SelectionList/BaseListItem.js
rename to src/components/SelectionList/BaseListItem.tsx
index 6a067ea0fe3d..71845931ba52 100644
--- a/src/components/SelectionList/BaseListItem.js
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -1,4 +1,3 @@
-import lodashGet from 'lodash/get';
import React from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
@@ -12,10 +11,10 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import RadioListItem from './RadioListItem';
-import {baseListItemPropTypes} from './selectionListPropTypes';
+import type {BaseListItemProps, RadioItem, User} from './types';
import UserListItem from './UserListItem';
-function BaseListItem({
+function BaseListItem({
item,
isFocused = false,
isDisabled = false,
@@ -26,12 +25,12 @@ function BaseListItem({
onDismissError = () => {},
rightHandSideComponent,
keyForList,
-}) {
+}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
- const isUserItem = lodashGet(item, 'icons.length', 0) > 0;
+ const isUserItem = 'icons' in item && item?.icons?.length && item.icons.length > 0;
const ListItem = isUserItem ? UserListItem : RadioListItem;
const rightHandSideComponentRender = () => {
@@ -49,8 +48,8 @@ function BaseListItem({
return (
onDismissError(item)}
- pendingAction={item.pendingAction}
- errors={item.errors}
+ pendingAction={isUserItem ? item.pendingAction : undefined}
+ errors={isUserItem ? item.errors : undefined}
errorRowStyles={styles.ph5}
>
)}
+
onSelectRow(item)}
showTooltip={showTooltip}
/>
+
{!canSelectMultiple && item.isSelected && !rightHandSideComponent && (
- {Boolean(item.invitedSecondaryLogin) && (
+ {isUserItem && item.invitedSecondaryLogin && (
{translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})}
@@ -140,6 +141,5 @@ function BaseListItem({
}
BaseListItem.displayName = 'BaseListItem';
-BaseListItem.propTypes = baseListItemPropTypes;
export default BaseListItem;
diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.tsx
similarity index 76%
rename from src/components/SelectionList/BaseSelectionList.js
rename to src/components/SelectionList/BaseSelectionList.tsx
index 960618808fd9..d97c47c84ee7 100644
--- a/src/components/SelectionList/BaseSelectionList.js
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -1,8 +1,8 @@
import {useFocusEffect, useIsFocused} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
import {View} from 'react-native';
-import _ from 'underscore';
+import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native';
import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager';
import Button from '@components/Button';
import Checkbox from '@components/Checkbox';
@@ -13,69 +13,61 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer';
import SectionList from '@components/SectionList';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState';
import useActiveElementRole from '@hooks/useActiveElementRole';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useLocalize from '@hooks/useLocalize';
-import useStyleUtils from '@hooks/useStyleUtils';
-import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Log from '@libs/Log';
import variables from '@styles/variables';
import CONST from '@src/CONST';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import BaseListItem from './BaseListItem';
-import {propTypes as selectionListPropTypes} from './selectionListPropTypes';
-
-const propTypes = {
- ...keyboardStatePropTypes,
- ...selectionListPropTypes,
-};
-
-function BaseSelectionList({
- sections,
- canSelectMultiple = false,
- onSelectRow,
- onSelectAll,
- onDismissError,
- textInputLabel = '',
- textInputPlaceholder = '',
- textInputValue = '',
- textInputHint = '',
- textInputMaxLength,
- inputMode = CONST.INPUT_MODE.TEXT,
- onChangeText,
- initiallyFocusedOptionKey = '',
- onScroll,
- onScrollBeginDrag,
- headerMessage = '',
- confirmButtonText = '',
- onConfirm,
- headerContent,
- footerContent,
- showScrollIndicator = false,
- showLoadingPlaceholder = false,
- showConfirmButton = false,
- shouldPreventDefaultFocusOnSelectRow = false,
- isKeyboardShown = false,
- containerStyle = [],
- disableInitialFocusOptionStyle = false,
- inputRef = null,
- disableKeyboardShortcuts = false,
- children,
- shouldStopPropagation = false,
- shouldShowTooltips = true,
- shouldUseDynamicMaxToRenderPerBatch = false,
- rightHandSideComponent,
-}) {
- const theme = useTheme();
+import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, RadioItem, Section, SectionListDataType, User} from './types';
+
+function BaseSelectionList(
+ {
+ sections,
+ canSelectMultiple = false,
+ onSelectRow,
+ onSelectAll,
+ onDismissError,
+ textInputLabel = '',
+ textInputPlaceholder = '',
+ textInputValue = '',
+ textInputHint,
+ textInputMaxLength,
+ inputMode = CONST.INPUT_MODE.TEXT,
+ onChangeText,
+ initiallyFocusedOptionKey = '',
+ onScroll,
+ onScrollBeginDrag,
+ headerMessage = '',
+ confirmButtonText = '',
+ onConfirm = () => {},
+ headerContent,
+ footerContent,
+ showScrollIndicator = false,
+ showLoadingPlaceholder = false,
+ showConfirmButton = false,
+ shouldPreventDefaultFocusOnSelectRow = false,
+ containerStyle,
+ isKeyboardShown = false,
+ disableKeyboardShortcuts = false,
+ children,
+ shouldStopPropagation = false,
+ shouldShowTooltips = true,
+ shouldUseDynamicMaxToRenderPerBatch = false,
+ rightHandSideComponent,
+ }: BaseSelectionListProps,
+ inputRef: ForwardedRef,
+) {
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
- const listRef = useRef(null);
- const textInputRef = useRef(null);
- const focusTimeoutRef = useRef(null);
- const shouldShowTextInput = Boolean(textInputLabel);
- const shouldShowSelectAll = Boolean(onSelectAll);
+ const listRef = useRef>>(null);
+ const textInputRef = useRef(null);
+ const focusTimeoutRef = useRef(null);
+ const shouldShowTextInput = !!textInputLabel;
+ const shouldShowSelectAll = !!onSelectAll;
const activeElementRole = useActiveElementRole();
const isFocused = useIsFocused();
const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT);
@@ -87,26 +79,24 @@ function BaseSelectionList({
* - `disabledOptionsIndexes`: Contains the indexes of all the disabled items in the list, to be used by the ArrowKeyFocusManager
* - `itemLayouts`: Contains the layout information for each item, header and footer in the list,
* so we can calculate the position of any given item when scrolling programmatically
- *
- * @return {{itemLayouts: [{offset: number, length: number}], disabledOptionsIndexes: *[], allOptions: *[]}}
*/
- const flattenedSections = useMemo(() => {
- const allOptions = [];
+ const flattenedSections = useMemo>(() => {
+ const allOptions: TItem[] = [];
- const disabledOptionsIndexes = [];
+ const disabledOptionsIndexes: number[] = [];
let disabledIndex = 0;
let offset = 0;
const itemLayouts = [{length: 0, offset}];
- const selectedOptions = [];
+ const selectedOptions: TItem[] = [];
- _.each(sections, (section, sectionIndex) => {
+ sections.forEach((section, sectionIndex) => {
const sectionHeaderHeight = variables.optionsListSectionHeaderHeight;
itemLayouts.push({length: sectionHeaderHeight, offset});
offset += sectionHeaderHeight;
- _.each(section.data, (item, optionIndex) => {
+ section.data?.forEach((item, optionIndex) => {
// Add item to the general flattened array
allOptions.push({
...item,
@@ -115,7 +105,7 @@ function BaseSelectionList({
});
// If disabled, add to the disabled indexes array
- if (section.isDisabled || item.isDisabled) {
+ if (!!section.isDisabled || item.isDisabled) {
disabledOptionsIndexes.push(disabledIndex);
}
disabledIndex += 1;
@@ -155,34 +145,34 @@ function BaseSelectionList({
}, [canSelectMultiple, sections]);
// If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member
- const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey));
+ const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey));
// Disable `Enter` shortcut if the active element is a button or checkbox
- const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole);
+ const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles);
/**
* Scrolls to the desired item index in the section list
*
- * @param {Number} index - the index of the item to scroll to
- * @param {Boolean} animated - whether to animate the scroll
+ * @param index - the index of the item to scroll to
+ * @param animated - whether to animate the scroll
*/
const scrollToIndex = useCallback(
- (index, animated = true) => {
+ (index: number, animated = true) => {
const item = flattenedSections.allOptions[index];
if (!listRef.current || !item) {
return;
}
- const itemIndex = item.index;
- const sectionIndex = item.sectionIndex;
+ const itemIndex = item.index ?? -1;
+ const sectionIndex = item.sectionIndex ?? -1;
// Note: react-native's SectionList automatically strips out any empty sections.
// So we need to reduce the sectionIndex to remove any empty sections in front of the one we're trying to scroll to.
// Otherwise, it will cause an index-out-of-bounds error and crash the app.
let adjustedSectionIndex = sectionIndex;
for (let i = 0; i < sectionIndex; i++) {
- if (_.isEmpty(lodashGet(sections, `[${i}].data`))) {
+ if (sections[i].data) {
adjustedSectionIndex--;
}
}
@@ -197,10 +187,10 @@ function BaseSelectionList({
/**
* Logic to run when a row is selected, either with click/press or keyboard hotkeys.
*
- * @param {Object} item - the list item
- * @param {Boolean} shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard)
+ * @param item - the list item
+ * @param shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard)
*/
- const selectRow = (item, shouldUnfocusRow = false) => {
+ const selectRow = (item: TItem, shouldUnfocusRow = false) => {
// In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item
if (canSelectMultiple) {
if (sections.length > 1) {
@@ -233,15 +223,15 @@ function BaseSelectionList({
};
const selectAllRow = () => {
- onSelectAll();
+ onSelectAll?.();
+
if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) {
textInputRef.current.focus();
}
};
- const selectFocusedOption = (e) => {
- const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']);
- const focusedOption = focusedItemKey ? _.find(flattenedSections.allOptions, (option) => option.keyForList === focusedItemKey) : flattenedSections.allOptions[focusedIndex];
+ const selectFocusedOption = () => {
+ const focusedOption = flattenedSections.allOptions[focusedIndex];
if (!focusedOption || focusedOption.isDisabled) {
return;
@@ -254,8 +244,8 @@ function BaseSelectionList({
* This function is used to compute the layout of any given item in our list.
* We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList.
*
- * @param {Array} data - This is the same as the data we pass into the component
- * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks:
+ * @param data - This is the same as the data we pass into the component
+ * @param flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks:
*
* 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those.
* 2. Each section includes a header, even if we don't provide/render one.
@@ -263,10 +253,8 @@ function BaseSelectionList({
* For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this:
*
* [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}]
- *
- * @returns {Object}
*/
- const getItemLayout = (data, flatDataArrayIndex) => {
+ const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => {
const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex];
if (!targetItem) {
@@ -284,8 +272,8 @@ function BaseSelectionList({
};
};
- const renderSectionHeader = ({section}) => {
- if (!section.title || _.isEmpty(section.data)) {
+ const renderSectionHeader = ({section}: {section: SectionListDataType}) => {
+ if (!section.title || isEmptyObject(section.data)) {
return null;
}
@@ -300,9 +288,10 @@ function BaseSelectionList({
);
};
- const renderItem = ({item, index, section}) => {
- const normalizedIndex = index + lodashGet(section, 'indexOffset', 0);
- const isDisabled = section.isDisabled || item.isDisabled;
+ const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => {
+ const indexOffset = section.indexOffset ? section.indexOffset : 0;
+ const normalizedIndex = index + indexOffset;
+ const isDisabled = !!section.isDisabled || item.isDisabled;
const isItemFocused = !isDisabled && focusedIndex === normalizedIndex;
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
const showTooltip = shouldShowTooltips && normalizedIndex < 10;
@@ -312,11 +301,9 @@ function BaseSelectionList({
item={item}
isFocused={isItemFocused}
isDisabled={isDisabled}
- isHide={!maxToRenderPerBatch}
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
onSelectRow={() => selectRow(item, true)}
- disableIsFocusStyle={disableInitialFocusOptionStyle}
onDismissError={onDismissError}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
@@ -326,11 +313,10 @@ function BaseSelectionList({
};
const scrollToFocusedIndexOnFirstRender = useCallback(
- ({nativeEvent}) => {
+ (nativeEvent: LayoutChangeEvent) => {
if (shouldUseDynamicMaxToRenderPerBatch) {
- const listHeight = lodashGet(nativeEvent, 'layout.height', 0);
- const itemHeight = lodashGet(nativeEvent, 'layout.y', 0);
-
+ const listHeight = nativeEvent.nativeEvent.layout.height;
+ const itemHeight = nativeEvent.nativeEvent.layout.y;
setMaxToRenderPerBatch((Math.ceil(listHeight / itemHeight) || 0) + CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT);
}
@@ -344,7 +330,7 @@ function BaseSelectionList({
);
const updateAndScrollToFocusedIndex = useCallback(
- (newFocusedIndex) => {
+ (newFocusedIndex: number) => {
setFocusedIndex(newFocusedIndex);
scrollToIndex(newFocusedIndex, true);
},
@@ -355,7 +341,12 @@ function BaseSelectionList({
useFocusEffect(
useCallback(() => {
if (shouldShowTextInput) {
- focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION);
+ focusTimeoutRef.current = setTimeout(() => {
+ if (!textInputRef.current) {
+ return;
+ }
+ textInputRef.current.focus();
+ }, CONST.ANIMATED_TRANSITION);
}
return () => {
if (!focusTimeoutRef.current) {
@@ -382,7 +373,7 @@ function BaseSelectionList({
/** Selects row when pressing Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
captureOnInputs: true,
- shouldBubble: () => !flattenedSections.allOptions[focusedIndex],
+ shouldBubble: !flattenedSections.allOptions[focusedIndex],
shouldStopPropagation,
isActive: !disableKeyboardShortcuts && !disableEnterShortcut && isFocused,
});
@@ -390,8 +381,8 @@ function BaseSelectionList({
/** Calls confirm action when pressing CTRL (CMD) + Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, {
captureOnInputs: true,
- shouldBubble: () => !flattenedSections.allOptions[focusedIndex],
- isActive: !disableKeyboardShortcuts && Boolean(onConfirm) && isFocused,
+ shouldBubble: !flattenedSections.allOptions[focusedIndex],
+ isActive: !disableKeyboardShortcuts && !!onConfirm && isFocused,
});
return (
@@ -401,19 +392,22 @@ function BaseSelectionList({
maxIndex={flattenedSections.allOptions.length - 1}
onFocusedIndexChanged={updateAndScrollToFocusedIndex}
>
- {/* */}
{({safeAreaPaddingBottomStyle}) => (
-
+
{shouldShowTextInput && (
{
- if (inputRef) {
- // eslint-disable-next-line no-param-reassign
- inputRef.current = el;
+ ref={(element) => {
+ textInputRef.current = element as RNTextInput;
+
+ if (!inputRef) {
+ return;
+ }
+
+ if (typeof inputRef === 'function') {
+ inputRef(element as RNTextInput);
}
- textInputRef.current = el;
}}
label={textInputLabel}
accessibilityLabel={textInputLabel}
@@ -427,16 +421,16 @@ function BaseSelectionList({
selectTextOnFocus
spellCheck={false}
onSubmitEditing={selectFocusedOption}
- blurOnSubmit={Boolean(flattenedSections.allOptions.length)}
+ blurOnSubmit={!!flattenedSections.allOptions.length}
/>
)}
- {Boolean(headerMessage) && (
+ {!!headerMessage && (
{headerMessage}
)}
- {Boolean(headerContent) && headerContent}
+ {!!headerContent && headerContent}
{flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? (
) : (
@@ -474,7 +468,7 @@ function BaseSelectionList({
onScrollBeginDrag={onScrollBeginDrag}
keyExtractor={(item) => item.keyForList}
extraData={focusedIndex}
- indicatorStyle={theme.white}
+ indicatorStyle="white"
keyboardShouldPersistTaps="always"
showsVerticalScrollIndicator={showScrollIndicator}
initialNumToRender={12}
@@ -500,7 +494,7 @@ function BaseSelectionList({
/>
)}
- {Boolean(footerContent) && {footerContent}}
+ {!!footerContent && {footerContent}}
)}
@@ -509,6 +503,5 @@ function BaseSelectionList({
}
BaseSelectionList.displayName = 'BaseSelectionList';
-BaseSelectionList.propTypes = propTypes;
-export default withKeyboardState(BaseSelectionList);
+export default forwardRef(BaseSelectionList);
diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.tsx
similarity index 87%
rename from src/components/SelectionList/RadioListItem.js
rename to src/components/SelectionList/RadioListItem.tsx
index 2de0c96932ea..769eaa80df4b 100644
--- a/src/components/SelectionList/RadioListItem.js
+++ b/src/components/SelectionList/RadioListItem.tsx
@@ -3,10 +3,11 @@ import {View} from 'react-native';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import useThemeStyles from '@hooks/useThemeStyles';
-import {radioListItemPropTypes} from './selectionListPropTypes';
+import type {RadioListItemProps} from './types';
-function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}) {
+function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}: RadioListItemProps) {
const styles = useThemeStyles();
+
return (
- {Boolean(item.alternateText) && (
+ {!!item.alternateText && (
- {Boolean(item.icons) && (
+ {!!item.icons && (
)}
@@ -26,19 +23,19 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style
text={item.text}
>
{item.text}
- {Boolean(item.alternateText) && (
+ {!!item.alternateText && (
{item.alternateText}
@@ -46,12 +43,11 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style
)}
- {Boolean(item.rightElement) && item.rightElement}
+ {!!item.rightElement && item.rightElement}
>
);
}
UserListItem.displayName = 'UserListItem';
-UserListItem.propTypes = userListItemPropTypes;
export default UserListItem;
diff --git a/src/components/SelectionList/index.android.js b/src/components/SelectionList/index.android.js
deleted file mode 100644
index 53d5b6bbce06..000000000000
--- a/src/components/SelectionList/index.android.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React, {forwardRef} from 'react';
-import {Keyboard} from 'react-native';
-import BaseSelectionList from './BaseSelectionList';
-
-const SelectionList = forwardRef((props, ref) => (
- Keyboard.dismiss()}
- />
-));
-
-SelectionList.displayName = 'SelectionList';
-
-export default SelectionList;
diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.android.tsx
new file mode 100644
index 000000000000..8487c6e2cc67
--- /dev/null
+++ b/src/components/SelectionList/index.android.tsx
@@ -0,0 +1,22 @@
+import React, {forwardRef} from 'react';
+import type {ForwardedRef} from 'react';
+import {Keyboard} from 'react-native';
+import type {TextInput} from 'react-native';
+import BaseSelectionList from './BaseSelectionList';
+import type {BaseSelectionListProps, RadioItem, User} from './types';
+
+function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
+ return (
+ Keyboard.dismiss()}
+ />
+ );
+}
+
+SelectionList.displayName = 'SelectionList';
+
+export default forwardRef(SelectionList);
diff --git a/src/components/SelectionList/index.ios.js b/src/components/SelectionList/index.ios.js
deleted file mode 100644
index 7f2a282aeb89..000000000000
--- a/src/components/SelectionList/index.ios.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React, {forwardRef} from 'react';
-import {Keyboard} from 'react-native';
-import BaseSelectionList from './BaseSelectionList';
-
-const SelectionList = forwardRef((props, ref) => (
- Keyboard.dismiss()}
- />
-));
-
-SelectionList.displayName = 'SelectionList';
-
-export default SelectionList;
diff --git a/src/components/SelectionList/index.ios.tsx b/src/components/SelectionList/index.ios.tsx
new file mode 100644
index 000000000000..9c32d38314e2
--- /dev/null
+++ b/src/components/SelectionList/index.ios.tsx
@@ -0,0 +1,21 @@
+import React, {forwardRef} from 'react';
+import type {ForwardedRef} from 'react';
+import {Keyboard} from 'react-native';
+import type {TextInput} from 'react-native';
+import BaseSelectionList from './BaseSelectionList';
+import type {BaseSelectionListProps, RadioItem, User} from './types';
+
+function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
+ return (
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...props}
+ ref={ref}
+ onScrollBeginDrag={() => Keyboard.dismiss()}
+ />
+ );
+}
+
+SelectionList.displayName = 'SelectionList';
+
+export default forwardRef(SelectionList);
diff --git a/src/components/SelectionList/index.js b/src/components/SelectionList/index.tsx
similarity index 82%
rename from src/components/SelectionList/index.js
rename to src/components/SelectionList/index.tsx
index 24ea60d29be5..93754926cacb 100644
--- a/src/components/SelectionList/index.js
+++ b/src/components/SelectionList/index.tsx
@@ -1,9 +1,12 @@
import React, {forwardRef, useEffect, useState} from 'react';
+import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
+import type {TextInput} from 'react-native';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import BaseSelectionList from './BaseSelectionList';
+import type {BaseSelectionListProps, RadioItem, User} from './types';
-const SelectionList = forwardRef((props, ref) => {
+function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
const [isScreenTouched, setIsScreenTouched] = useState(false);
const touchStart = () => setIsScreenTouched(true);
@@ -39,8 +42,8 @@ const SelectionList = forwardRef((props, ref) => {
}}
/>
);
-});
+}
SelectionList.displayName = 'SelectionList';
-export default SelectionList;
+export default forwardRef(SelectionList);
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
new file mode 100644
index 000000000000..a82ddef6febb
--- /dev/null
+++ b/src/components/SelectionList/types.ts
@@ -0,0 +1,265 @@
+import type {ReactElement, ReactNode} from 'react';
+import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native';
+import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+
+type CommonListItemProps = {
+ /** Whether this item is focused (for arrow key controls) */
+ isFocused?: boolean;
+
+ /** Style to be applied to Text */
+ textStyles?: StyleProp;
+
+ /** Style to be applied on the alternate text */
+ alternateTextStyles?: StyleProp;
+
+ /** Whether this item is disabled */
+ isDisabled?: boolean;
+
+ /** Whether this item should show Tooltip */
+ showTooltip: boolean;
+
+ /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */
+ canSelectMultiple?: boolean;
+
+ /** Callback to fire when the item is pressed */
+ onSelectRow: (item: TItem) => void;
+
+ /** Callback to fire when an error is dismissed */
+ onDismissError?: (item: TItem) => void;
+
+ /** Component to display on the right side */
+ rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null;
+};
+
+type User = {
+ /** Text to display */
+ text: string;
+
+ /** Alternate text to display */
+ alternateText?: string;
+
+ /** Key used internally by React */
+ keyForList: string;
+
+ /** Whether this option is selected */
+ isSelected?: boolean;
+
+ /** Whether this option is disabled for selection */
+ isDisabled?: boolean;
+
+ /** User accountID */
+ accountID?: number;
+
+ /** User login */
+ login?: string;
+
+ /** Element to show on the right side of the item */
+ rightElement?: ReactElement;
+
+ /** Icons for the user (can be multiple if it's a Workspace) */
+ icons?: Icon[];
+
+ /** Errors that this user may contain */
+ errors?: Errors;
+
+ /** The type of action that's pending */
+ pendingAction?: PendingAction;
+
+ invitedSecondaryLogin?: string;
+
+ /** Represents the index of the section it came from */
+ sectionIndex?: number;
+
+ /** Represents the index of the option within the section it came from */
+ index?: number;
+};
+
+type UserListItemProps = CommonListItemProps & {
+ /** The section list item */
+ item: User;
+
+ /** Additional styles to apply to text */
+ style?: StyleProp;
+};
+
+type RadioItem = {
+ /** Text to display */
+ text: string;
+
+ /** Alternate text to display */
+ alternateText?: string;
+
+ /** Key used internally by React */
+ keyForList: string;
+
+ /** Whether this option is selected */
+ isSelected?: boolean;
+
+ /** Whether this option is disabled for selection */
+ isDisabled?: boolean;
+
+ /** Represents the index of the section it came from */
+ sectionIndex?: number;
+
+ /** Represents the index of the option within the section it came from */
+ index?: number;
+};
+
+type RadioListItemProps = CommonListItemProps & {
+ /** The section list item */
+ item: RadioItem;
+};
+
+type BaseListItemProps = CommonListItemProps & {
+ item: TItem;
+ shouldPreventDefaultFocusOnSelectRow?: boolean;
+ keyForList?: string;
+};
+
+type Section = {
+ /** Title of the section */
+ title?: string;
+
+ /** The initial index of this section given the total number of options in each section's data array */
+ indexOffset?: number;
+
+ /** Array of options */
+ data?: TItem[];
+
+ /** Whether this section items disabled for selection */
+ isDisabled?: boolean;
+};
+
+type BaseSelectionListProps = Partial & {
+ /** Sections for the section list */
+ sections: Array>>;
+
+ /** Whether this is a multi-select list */
+ canSelectMultiple?: boolean;
+
+ /** Callback to fire when a row is pressed */
+ onSelectRow: (item: TItem) => void;
+
+ /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */
+ onSelectAll?: () => void;
+
+ /** Callback to fire when an error is dismissed */
+ onDismissError?: () => void;
+
+ /** Label for the text input */
+ textInputLabel?: string;
+
+ /** Placeholder for the text input */
+ textInputPlaceholder?: string;
+
+ /** Hint for the text input */
+ textInputHint?: string;
+
+ /** Value for the text input */
+ textInputValue?: string;
+
+ /** Max length for the text input */
+ textInputMaxLength?: number;
+
+ /** Callback to fire when the text input changes */
+ onChangeText?: (text: string) => void;
+
+ /** Input mode for the text input */
+ inputMode?: InputModeOptions;
+
+ /** Item `keyForList` to focus initially */
+ initiallyFocusedOptionKey?: string;
+
+ /** Callback to fire when the list is scrolled */
+ onScroll?: () => void;
+
+ /** Callback to fire when the list is scrolled and the user begins dragging */
+ onScrollBeginDrag?: () => void;
+
+ /** Message to display at the top of the list */
+ headerMessage?: string;
+
+ /** Text to display on the confirm button */
+ confirmButtonText?: string;
+
+ /** Callback to fire when the confirm button is pressed */
+ onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void;
+
+ /** Whether to show the vertical scroll indicator */
+ showScrollIndicator?: boolean;
+
+ /** Whether to show the loading placeholder */
+ showLoadingPlaceholder?: boolean;
+
+ /** Whether to show the default confirm button */
+ showConfirmButton?: boolean;
+
+ /** Whether tooltips should be shown */
+ shouldShowTooltips?: boolean;
+
+ /** Whether to stop automatic form submission on pressing enter key or not */
+ shouldStopPropagation?: boolean;
+
+ /** Whether to prevent default focusing of options and focus the textinput when selecting an option */
+ shouldPreventDefaultFocusOnSelectRow?: boolean;
+
+ /** Custom content to display in the header */
+ headerContent?: ReactNode;
+
+ /** Custom content to display in the footer */
+ footerContent?: ReactNode;
+
+ /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
+ shouldUseDynamicMaxToRenderPerBatch?: boolean;
+
+ /** Whether keyboard shortcuts should be disabled */
+ disableKeyboardShortcuts?: boolean;
+
+ /** Whether to disable initial styling for focused option */
+ disableInitialFocusOptionStyle?: boolean;
+
+ /** Styles to apply to SelectionList container */
+ containerStyle?: ViewStyle;
+
+ /** Whether keyboard is visible on the screen */
+ isKeyboardShown?: boolean;
+
+ /** Whether focus event should be delayed */
+ shouldDelayFocus?: boolean;
+
+ /** Component to display on the right side of each child */
+ rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null;
+};
+
+type ItemLayout = {
+ length: number;
+ offset: number;
+};
+
+type FlattenedSectionsReturn = {
+ allOptions: TItem[];
+ selectedOptions: TItem[];
+ disabledOptionsIndexes: number[];
+ itemLayouts: ItemLayout[];
+ allSelected: boolean;
+};
+
+type ButtonOrCheckBoxRoles = 'button' | 'checkbox';
+
+type SectionListDataType = SectionListData>;
+
+export type {
+ BaseSelectionListProps,
+ CommonListItemProps,
+ UserListItemProps,
+ Section,
+ RadioListItemProps,
+ BaseListItemProps,
+ User,
+ RadioItem,
+ FlattenedSectionsReturn,
+ ItemLayout,
+ ButtonOrCheckBoxRoles,
+ SectionListDataType,
+};
diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx
index 00cf248ad838..bcbca6e2958b 100644
--- a/src/components/SubscriptAvatar.tsx
+++ b/src/components/SubscriptAvatar.tsx
@@ -4,35 +4,17 @@ import type {ValueOf} from 'type-fest';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import type {AvatarSource} from '@libs/UserUtils';
import CONST from '@src/CONST';
-import type {AvatarType} from '@src/types/onyx/OnyxCommon';
+import type {Icon} from '@src/types/onyx/OnyxCommon';
import Avatar from './Avatar';
import UserDetailsTooltip from './UserDetailsTooltip';
-type SubAvatar = {
- /** Avatar source to display */
- source?: AvatarSource;
-
- /** Denotes whether it is an avatar or a workspace avatar */
- type?: AvatarType;
-
- /** Owner of the avatar. If user, displayName. If workspace, policy name */
- name?: string;
-
- /** Avatar id */
- id?: number | string;
-
- /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */
- fallbackIcon?: AvatarSource;
-};
-
type SubscriptAvatarProps = {
/** Avatar URL or icon */
- mainAvatar?: SubAvatar;
+ mainAvatar?: Icon;
/** Subscript avatar URL or icon */
- secondaryAvatar?: SubAvatar;
+ secondaryAvatar?: Icon;
/** Set the size of avatars */
size?: ValueOf;
@@ -47,7 +29,7 @@ type SubscriptAvatarProps = {
showTooltip?: boolean;
};
-function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AVATAR_SIZE.DEFAULT, backgroundColor, noMargin = false, showTooltip = true}: SubscriptAvatarProps) {
+function SubscriptAvatar({mainAvatar, secondaryAvatar, size = CONST.AVATAR_SIZE.DEFAULT, backgroundColor, noMargin = false, showTooltip = true}: SubscriptAvatarProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -59,23 +41,23 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV
@@ -104,3 +86,4 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV
SubscriptAvatar.displayName = 'SubscriptAvatar';
export default memo(SubscriptAvatar);
+export type {SubscriptAvatarProps};
diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index 7c0b3a6e33e1..410bd5081e25 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -9,8 +9,7 @@ import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
-import type NetworkOnyx from '@src/types/onyx/Network';
-import type UserOnyx from '@src/types/onyx/User';
+import type {Network as NetworkOnyx, User as UserOnyx} from '@src/types/onyx';
import Button from './Button';
import {withNetwork} from './OnyxProvider';
import Switch from './Switch';
@@ -26,7 +25,6 @@ type TestToolMenuProps = TestToolMenuOnyxProps & {
/** Network object in Onyx */
network: OnyxEntry;
};
-
const USER_DEFAULT: UserOnyx = {shouldUseStagingServer: undefined, isSubscribedToNewsletter: false, validated: false, isFromPublicDomain: false, isUsingExpensifyCard: false};
function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js
index a9b4566a390c..4664251ca765 100644
--- a/src/components/TimePicker/TimePicker.js
+++ b/src/components/TimePicker/TimePicker.js
@@ -8,6 +8,7 @@ import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
import refPropTypes from '@components/refPropTypes';
import Text from '@components/Text';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -127,6 +128,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
const hourInputRef = useRef(null);
const minuteInputRef = useRef(null);
+ const {inputCallbackRef} = useAutoFocusInput();
+
const focusMinuteInputOnFirstCharacter = useCallback(() => setCursorPosition(0, minuteInputRef, setSelectionMinute), []);
const focusHourInputOnLastCharacter = useCallback(() => setCursorPosition(2, hourInputRef, setSelectionHour), []);
@@ -492,6 +495,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
minuteInputRef.current = {hourRef: hourInputRef.current, minuteInputRef: ref};
}
minuteInputRef.current = ref;
+ inputCallbackRef(ref);
}}
onSelectionChange={(e) => {
setSelectionMinute(e.nativeEvent.selection);
diff --git a/src/components/UpdateAppModal/BaseUpdateAppModal.js b/src/components/UpdateAppModal/BaseUpdateAppModal.tsx
similarity index 56%
rename from src/components/UpdateAppModal/BaseUpdateAppModal.js
rename to src/components/UpdateAppModal/BaseUpdateAppModal.tsx
index 07b446172286..2f01f4ff72e0 100755
--- a/src/components/UpdateAppModal/BaseUpdateAppModal.js
+++ b/src/components/UpdateAppModal/BaseUpdateAppModal.tsx
@@ -1,24 +1,25 @@
-import React, {memo, useState} from 'react';
+import React, {useState} from 'react';
import ConfirmModal from '@components/ConfirmModal';
-import withLocalize from '@components/withLocalize';
-import {defaultProps, propTypes} from './updateAppModalPropTypes';
+import useLocalize from '@hooks/useLocalize';
+import type UpdateAppModalProps from './types';
-function BaseUpdateAppModal({translate, onSubmit}) {
+function BaseUpdateAppModal({onSubmit}: UpdateAppModalProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
+ const {translate} = useLocalize();
/**
* Execute the onSubmit callback and close the modal.
*/
- function submitAndClose() {
- onSubmit();
+ const submitAndClose = () => {
+ onSubmit?.();
setIsModalOpen(false);
- }
+ };
return (
submitAndClose()}
+ onConfirm={submitAndClose}
onCancel={() => setIsModalOpen(false)}
prompt={translate('baseUpdateAppModal.updatePrompt')}
confirmText={translate('baseUpdateAppModal.updateApp')}
@@ -27,7 +28,6 @@ function BaseUpdateAppModal({translate, onSubmit}) {
);
}
-BaseUpdateAppModal.propTypes = propTypes;
-BaseUpdateAppModal.defaultProps = defaultProps;
+BaseUpdateAppModal.displayName = 'BaseUpdateAppModal';
-export default memo(withLocalize(BaseUpdateAppModal));
+export default React.memo(BaseUpdateAppModal);
diff --git a/src/components/UpdateAppModal/index.desktop.js b/src/components/UpdateAppModal/index.desktop.tsx
similarity index 66%
rename from src/components/UpdateAppModal/index.desktop.js
rename to src/components/UpdateAppModal/index.desktop.tsx
index 397ce2c75ea3..4779e07a5df8 100644
--- a/src/components/UpdateAppModal/index.desktop.js
+++ b/src/components/UpdateAppModal/index.desktop.tsx
@@ -1,17 +1,16 @@
import React from 'react';
import ELECTRON_EVENTS from '../../../desktop/ELECTRON_EVENTS';
import BaseUpdateAppModal from './BaseUpdateAppModal';
-import {propTypes} from './updateAppModalPropTypes';
+import type UpdateAppModalProps from './types';
-function UpdateAppModal(props) {
+function UpdateAppModal({onSubmit}: UpdateAppModalProps) {
const updateApp = () => {
- if (props.onSubmit) {
- props.onSubmit();
- }
+ onSubmit?.();
window.electron.send(ELECTRON_EVENTS.START_UPDATE);
};
return ;
}
-UpdateAppModal.propTypes = propTypes;
+
UpdateAppModal.displayName = 'UpdateAppModal';
+
export default UpdateAppModal;
diff --git a/src/components/UpdateAppModal/index.js b/src/components/UpdateAppModal/index.tsx
similarity index 71%
rename from src/components/UpdateAppModal/index.js
rename to src/components/UpdateAppModal/index.tsx
index 488f69f66385..d596ae0686fe 100644
--- a/src/components/UpdateAppModal/index.js
+++ b/src/components/UpdateAppModal/index.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import BaseUpdateAppModal from './BaseUpdateAppModal';
-import {propTypes} from './updateAppModalPropTypes';
+import type UpdateAppModalProps from './types';
-function UpdateAppModal(props) {
+function UpdateAppModal(props: UpdateAppModalProps) {
return (
);
}
-UpdateAppModal.propTypes = propTypes;
+
UpdateAppModal.displayName = 'UpdateAppModal';
+
export default UpdateAppModal;
diff --git a/src/components/UpdateAppModal/types.ts b/src/components/UpdateAppModal/types.ts
new file mode 100644
index 000000000000..ae57de671db6
--- /dev/null
+++ b/src/components/UpdateAppModal/types.ts
@@ -0,0 +1,6 @@
+type UpdateAppModalProps = {
+ /** Callback to fire when we want to trigger the update. */
+ onSubmit?: () => void;
+};
+
+export default UpdateAppModalProps;
diff --git a/src/components/UpdateAppModal/updateAppModalPropTypes.js b/src/components/UpdateAppModal/updateAppModalPropTypes.js
deleted file mode 100644
index 37c112bf7598..000000000000
--- a/src/components/UpdateAppModal/updateAppModalPropTypes.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import PropTypes from 'prop-types';
-
-const propTypes = {
- /** Callback to fire when we want to trigger the update. */
- onSubmit: PropTypes.func,
-};
-
-const defaultProps = {
- onSubmit: null,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/hooks/useResetComposerFocus.ts b/src/hooks/useResetComposerFocus.ts
new file mode 100644
index 000000000000..e9f88ed93346
--- /dev/null
+++ b/src/hooks/useResetComposerFocus.ts
@@ -0,0 +1,19 @@
+import {useIsFocused} from '@react-navigation/native';
+import type {MutableRefObject} from 'react';
+import {useEffect, useRef} from 'react';
+import type {TextInput} from 'react-native';
+
+export default function useResetComposerFocus(inputRef: MutableRefObject) {
+ const isFocused = useIsFocused();
+ const shouldResetFocus = useRef(false);
+
+ useEffect(() => {
+ if (!isFocused || !shouldResetFocus.current) {
+ return;
+ }
+ inputRef.current?.focus(); // focus input again
+ shouldResetFocus.current = false;
+ }, [isFocused, inputRef]);
+
+ return {isFocused, shouldResetFocus};
+}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index b6da38df21a0..bb22c7a7856a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1,6 +1,7 @@
import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import Str from 'expensify-common/lib/str';
import CONST from '@src/CONST';
+import type {Country} from '@src/CONST';
import type {
AddressLineParams,
AlreadySignedInParams,
@@ -106,7 +107,7 @@ type StateValue = {
type States = Record;
-type AllCountries = Record;
+type AllCountries = Record;
/* eslint-disable max-len */
export default {
@@ -2073,6 +2074,14 @@ export default {
},
copyReferralLink: 'Copy invite link',
},
+ purposeForExpensify: {
+ [CONST.INTRO_CHOICES.TRACK]: 'Track business spend for taxes',
+ [CONST.INTRO_CHOICES.SUBMIT]: 'Get paid back by my employer',
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends',
+ welcomeMessage: 'Welcome to Expensify',
+ welcomeSubtitle: 'What would you like to do?',
+ },
violations: {
allTagLevelsRequired: 'All tags required',
autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 2478c8ba8bd2..858fe29a8faf 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2561,6 +2561,14 @@ export default {
},
copyReferralLink: 'Copiar enlace de invitación',
},
+ purposeForExpensify: {
+ [CONST.INTRO_CHOICES.TRACK]: 'Seguimiento de los gastos de empresa para fines fiscales',
+ [CONST.INTRO_CHOICES.SUBMIT]: 'Reclamar gastos a mi empleador',
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo',
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chatea y divide gastos con tus amigos',
+ welcomeMessage: 'Bienvenido a Expensify',
+ welcomeSubtitle: '¿Qué te gustaría hacer?',
+ },
violations: {
allTagLevelsRequired: 'Todas las etiquetas son obligatorias',
autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`,
diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.ts
similarity index 78%
rename from src/libs/ComposerFocusManager.js
rename to src/libs/ComposerFocusManager.ts
index 569e165da962..b66bbe92599e 100644
--- a/src/libs/ComposerFocusManager.js
+++ b/src/libs/ComposerFocusManager.ts
@@ -1,18 +1,20 @@
let isReadyToFocusPromise = Promise.resolve();
-let resolveIsReadyToFocus;
+let resolveIsReadyToFocus: (value: void | PromiseLike) => void;
function resetReadyToFocus() {
isReadyToFocusPromise = new Promise((resolve) => {
resolveIsReadyToFocus = resolve;
});
}
+
function setReadyToFocus() {
if (!resolveIsReadyToFocus) {
return;
}
resolveIsReadyToFocus();
}
-function isReadyToFocus() {
+
+function isReadyToFocus(): Promise {
return isReadyToFocusPromise;
}
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 1a10eb03a00e..526769723531 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -5,9 +5,12 @@ import {
eachDayOfInterval,
eachMonthOfInterval,
endOfDay,
+ endOfMonth,
endOfWeek,
format,
formatDistanceToNow,
+ getDate,
+ getDay,
getDayOfYear,
isAfter,
isBefore,
@@ -730,6 +733,25 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone {
};
}
+/**
+ * Returns the last business day of given date month
+ *
+ * param {Date} inputDate
+ * returns {number}
+ */
+function getLastBusinessDayOfMonth(inputDate: Date): number {
+ let currentDate = endOfMonth(inputDate);
+ const dayOfWeek = getDay(currentDate);
+
+ if (dayOfWeek === 0) {
+ currentDate = subDays(currentDate, 2);
+ } else if (dayOfWeek === 6) {
+ currentDate = subDays(currentDate, 1);
+ }
+
+ return getDate(currentDate);
+}
+
const DateUtils = {
formatToDayOfWeek,
formatToLongDateWithWeekday,
@@ -774,6 +796,7 @@ const DateUtils = {
getWeekEndsOn,
isTimeAtLeastOneMinuteInFuture,
formatToSupportedTimezone,
+ getLastBusinessDayOfMonth,
};
export default DateUtils;
diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts
index 26b3665ca4ce..5d925ae1c684 100644
--- a/src/libs/GroupChatUtils.ts
+++ b/src/libs/GroupChatUtils.ts
@@ -1,11 +1,12 @@
+import type {OnyxEntry} from 'react-native-onyx';
import type {Report} from '@src/types/onyx';
import * as ReportUtils from './ReportUtils';
/**
* Returns the report name if the report is a group chat
*/
-function getGroupChatName(report: Report): string | undefined {
- const participants = report.participantAccountIDs ?? [];
+function getGroupChatName(report: OnyxEntry): string | undefined {
+ const participants = report?.participantAccountIDs ?? [];
const isMultipleParticipantReport = participants.length > 1;
return participants
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index ff5ad9327191..548751cbd8d1 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -118,7 +118,8 @@ function getForReportAction(reportAction: ReportAction): string {
const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage;
if (hasModifiedAmount) {
const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? '';
- const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency);
+ const oldAmountValue = reportActionOriginalMessage?.oldAmount ?? 0;
+ const oldAmount = oldAmountValue > 0 ? CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency) : '';
const currency = reportActionOriginalMessage?.currency ?? '';
const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency);
@@ -187,8 +188,8 @@ function getForReportAction(reportAction: ReportAction): string {
const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage;
if (hasModifiedTag) {
buildMessageFragmentForValue(
- reportActionOriginalMessage?.tag ?? '',
- reportActionOriginalMessage?.oldTag ?? '',
+ PolicyUtils.getCleanedTagName(reportActionOriginalMessage?.tag ?? ''),
+ PolicyUtils.getCleanedTagName(reportActionOriginalMessage?.oldTag ?? ''),
policyTagListName,
true,
setFragments,
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 03a3612d4566..e9fcb57df1da 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -59,6 +59,9 @@ const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScre
const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default as React.ComponentType;
const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default as React.ComponentType;
const loadConciergePage = () => require('../../../pages/ConciergePage').default as React.ComponentType;
+const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default as React.ComponentType;
+const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default as React.ComponentType;
+const loadReportAvatar = () => require('../../../pages/ReportAvatar').default as React.ComponentType;
let timezone: Timezone | null;
let currentAccountID = -1;
@@ -298,6 +301,33 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f
getComponent={loadReportAttachments}
listeners={modalScreenListeners}
/>
+
+
+
({
[SCREENS.EDIT_REQUEST.ROOT]: () => require('../../../pages/EditRequestPage').default as React.ComponentType,
[SCREENS.EDIT_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType,
+ [SCREENS.EDIT_REQUEST.REPORT_FIELD]: () => require('../../../pages/EditReportFieldPage').default as React.ComponentType,
});
const PrivateNotesModalStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
index a3fe1c657f34..5462b6c0ce4e 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
@@ -27,19 +27,21 @@ function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
we have 30px draggable ba at the top and the rest of the dimmed area is clickable. On other devices,
everything behaves normally like one big pressable */}
diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts
index 1a495e92eb80..0951b41d78b5 100644
--- a/src/libs/Navigation/linkingConfig.ts
+++ b/src/libs/Navigation/linkingConfig.ts
@@ -29,6 +29,9 @@ const linkingConfig: LinkingOptions = {
[SCREENS.SAML_SIGN_IN]: ROUTES.SAML_SIGN_IN,
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT,
[SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route,
+ [SCREENS.PROFILE_AVATAR]: ROUTES.PROFILE_AVATAR.route,
+ [SCREENS.WORKSPACE_AVATAR]: ROUTES.WORKSPACE_AVATAR.route,
+ [SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route,
// Sidebar
[SCREENS.HOME]: {
@@ -478,6 +481,7 @@ const linkingConfig: LinkingOptions = {
screens: {
[SCREENS.EDIT_REQUEST.ROOT]: ROUTES.EDIT_REQUEST.route,
[SCREENS.EDIT_REQUEST.CURRENCY]: ROUTES.EDIT_CURRENCY_REQUEST.route,
+ [SCREENS.EDIT_REQUEST.REPORT_FIELD]: ROUTES.EDIT_REPORT_FIELD_REQUEST.route,
},
},
[SCREENS.RIGHT_MODAL.SIGN_IN]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index a1cbb562997e..dd5a7720f00d 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -323,7 +323,9 @@ type SignInNavigatorParamList = {
};
type ReferralDetailsNavigatorParamList = {
- [SCREENS.REFERRAL_DETAILS]: undefined;
+ [SCREENS.REFERRAL_DETAILS]: {
+ contentType: ValueOf;
+ };
};
type ProcessMoneyRequestHoldNavigatorParamList = {
@@ -416,6 +418,15 @@ type AuthScreensParamList = {
reportID: string;
source: string;
};
+ [SCREENS.PROFILE_AVATAR]: {
+ accountID: string;
+ };
+ [SCREENS.WORKSPACE_AVATAR]: {
+ policyID: string;
+ };
+ [SCREENS.REPORT_AVATAR]: {
+ reportID: string;
+ };
[SCREENS.NOT_FOUND]: undefined;
[NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
index 547ecb1de5b2..9767210b3479 100644
--- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
+++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts
@@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import Visibility from '@libs/Visibility';
+import * as Modal from '@userActions/Modal';
import ROUTES from '@src/ROUTES';
import backgroundRefresh from './backgroundRefresh';
import PushNotification from './index';
@@ -26,22 +27,25 @@ export default function subscribeToReportCommentPushNotifications() {
Navigation.isNavigationReady()
.then(Navigation.waitForProtectedRoutes)
.then(() => {
- try {
- // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back
- if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) {
- Navigation.goBack(ROUTES.HOME);
- }
+ // The attachment modal remains open when navigating to the report so we need to close it
+ Modal.close(() => {
+ try {
+ // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back
+ if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) {
+ Navigation.goBack(ROUTES.HOME);
+ }
- Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID});
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID)));
- } catch (error) {
- let errorMessage = String(error);
- if (error instanceof Error) {
- errorMessage = error.message;
- }
+ Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID});
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID)));
+ } catch (error) {
+ let errorMessage = String(error);
+ if (error instanceof Error) {
+ errorMessage = error.message;
+ }
- Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage});
- }
+ Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage});
+ }
+ });
});
});
}
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index e86c9daacb42..2973228af51f 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -17,6 +17,7 @@ import Navigation from './Navigation/Navigation';
import Permissions from './Permissions';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PhoneNumber from './PhoneNumber';
+import * as PolicyUtils from './PolicyUtils';
import * as ReportActionUtils from './ReportActionsUtils';
import * as ReportUtils from './ReportUtils';
import * as TaskUtils from './TaskUtils';
@@ -945,19 +946,23 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt
* @returns {Array