diff --git a/android/app/build.gradle b/android/app/build.gradle
index 162147aeff0c..4088f69cf008 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001042407
- versionName "1.4.24-7"
+ versionCode 1001042501
+ versionName "1.4.25-1"
}
flavorDimensions "default"
diff --git a/assets/animations/Coin.lottie b/assets/animations/Coin.lottie
new file mode 100644
index 000000000000..e426f7efdc3c
Binary files /dev/null and b/assets/animations/Coin.lottie differ
diff --git a/assets/images/new-expensify.svg b/assets/images/new-expensify.svg
index 38276ecd9385..89102ecbc5e4 100644
--- a/assets/images/new-expensify.svg
+++ b/assets/images/new-expensify.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md
index 6e02cae677bb..186d7def3423 100644
--- a/contributingGuides/CONTRIBUTING.md
+++ b/contributingGuides/CONTRIBUTING.md
@@ -5,7 +5,7 @@ Welcome! Thanks for checking out the New Expensify app and taking the time to co
If you would like to become an Expensify contributor, the first step is to read this document in its **entirety**. The second step is to review the README guidelines [here](https://github.com/Expensify/App/blob/main/README.md) to understand our coding philosophy and for a general overview of the code repository (i.e. how to run the app locally, testing, storage, our app philosophy, etc). Please read both documents before asking questions, as it may be covered within the documentation.
#### Test Accounts
-You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar.
+You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. Do use Expensify employee or customer accounts for testing.
**Notes**:
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 7081805db569..813c136f3c2c 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.24
+ 1.4.25
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.24.7
+ 1.4.25.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 20d4ea1a4820..dfa278adacc5 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.24
+ 1.4.25
CFBundleSignature
????
CFBundleVersion
- 1.4.24.7
+ 1.4.25.1
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index f941edc1100e..73420efed711 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -3,9 +3,9 @@
CFBundleShortVersionString
- 1.4.24
+ 1.4.25
CFBundleVersion
- 1.4.24.7
+ 1.4.25.1
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index acc8720dafce..379194a70fd9 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1176,7 +1176,7 @@ PODS:
- React-Core
- react-native-key-command (1.0.6):
- React-Core
- - react-native-netinfo (11.1.0):
+ - react-native-netinfo (11.2.1):
- React-Core
- react-native-pager-view (6.2.2):
- React-Core
@@ -1909,7 +1909,7 @@ SPEC CHECKSUMS:
react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56
react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b
react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5
- react-native-netinfo: 3aa5637c18834966e0c932de8ae1ae56fea20a97
+ react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d
react-native-pager-view: 02a5c4962530f7efc10dd51ee9cdabeff5e6c631
react-native-pdf: 79aa75e39a80c1d45ffe58aa500f3cf08f267a2e
react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886
@@ -1967,7 +1967,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7
- Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047
+ Yoga: 13c8ef87792450193e117976337b8527b49e8c03
PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2
diff --git a/package-lock.json b/package-lock.json
index 8df6ff2ebf09..b530468d7725 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.24-7",
+ "version": "1.4.25-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.24-7",
+ "version": "1.4.25-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -27,7 +27,7 @@
"@react-native-camera-roll/camera-roll": "5.4.0",
"@react-native-clipboard/clipboard": "^1.12.1",
"@react-native-community/geolocation": "^3.0.6",
- "@react-native-community/netinfo": "11.1.0",
+ "@react-native-community/netinfo": "11.2.1",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
"@react-native-firebase/crashlytics": "^12.3.0",
@@ -51,7 +51,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
"expo": "^50.0.0-preview.7",
"expo-image": "1.10.1",
"fbjs": "^3.0.2",
@@ -94,7 +94,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.118",
+ "react-native-onyx": "1.0.126",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "^6.7.4",
"react-native-performance": "^5.1.0",
@@ -9608,9 +9608,9 @@
}
},
"node_modules/@react-native-community/netinfo": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz",
- "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==",
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz",
+ "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==",
"peerDependencies": {
"react-native": ">=0.59"
}
@@ -26153,10 +26153,9 @@
}
},
"node_modules/clipboard": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
- "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
- "license": "MIT",
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+ "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
@@ -28303,8 +28302,7 @@
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
- "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
- "license": "MIT"
+ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/delegates": {
"version": "1.0.0",
@@ -31594,37 +31592,26 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
- "integrity": "sha512-H7UrLgWIr8mCoPc1oxbeYW2RwLzUWI6jdjbV6cRnrlp8cDW3IyZISF+BQSPFDj7bMhNAbczQPtEOE1gld21Cvg==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
+ "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
- "clipboard": "2.0.4",
- "html-entities": "^2.3.3",
+ "clipboard": "2.0.11",
+ "html-entities": "^2.4.0",
"jquery": "3.6.0",
"localforage": "^1.10.0",
"lodash": "4.17.21",
- "prop-types": "15.7.2",
+ "prop-types": "15.8.1",
"react": "16.12.0",
"react-dom": "16.12.0",
- "semver": "^7.3.5",
+ "semver": "^7.5.2",
"simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
- "string.prototype.replaceall": "^1.0.6",
+ "string.prototype.replaceall": "^1.0.8",
"ua-parser-js": "^1.0.35",
"underscore": "1.13.6"
}
},
- "node_modules/expensify-common/node_modules/prop-types": {
- "version": "15.7.2",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
- "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.8.1"
- }
- },
"node_modules/expensify-common/node_modules/react": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
@@ -33539,7 +33526,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
- "license": "MIT",
"dependencies": {
"delegate": "^3.1.2"
}
@@ -34284,9 +34270,19 @@
}
},
"node_modules/html-entities": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz",
- "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg=="
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz",
+ "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ]
},
"node_modules/html-escaper": {
"version": "2.0.2",
@@ -47038,17 +47034,17 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.118",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz",
- "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==",
+ "version": "1.0.126",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz",
+ "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
"underscore": "^1.13.6"
},
"engines": {
- "node": ">=16.15.1 <=20.9.0",
- "npm": ">=8.11.0 <=10.1.0"
+ "node": "20.9.0",
+ "npm": "10.1.0"
},
"peerDependencies": {
"idb-keyval": "^6.2.1",
@@ -49759,8 +49755,7 @@
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
- "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
- "license": "MIT"
+ "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
},
"node_modules/select-hose": {
"version": "2.0.0",
@@ -51347,14 +51342,15 @@
}
},
"node_modules/string.prototype.replaceall": {
- "version": "1.0.6",
- "license": "MIT",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz",
+ "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "has-symbols": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
"is-regex": "^1.1.4"
},
"funding": {
@@ -62634,9 +62630,9 @@
"requires": {}
},
"@react-native-community/netinfo": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz",
- "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==",
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz",
+ "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==",
"requires": {}
},
"@react-native-firebase/analytics": {
@@ -74710,9 +74706,9 @@
"dev": true
},
"clipboard": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz",
- "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==",
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+ "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
@@ -78671,36 +78667,26 @@
}
},
"expensify-common": {
- "version": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
- "integrity": "sha512-H7UrLgWIr8mCoPc1oxbeYW2RwLzUWI6jdjbV6cRnrlp8cDW3IyZISF+BQSPFDj7bMhNAbczQPtEOE1gld21Cvg==",
- "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
+ "version": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
+ "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==",
+ "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
"requires": {
"classnames": "2.3.1",
- "clipboard": "2.0.4",
- "html-entities": "^2.3.3",
+ "clipboard": "2.0.11",
+ "html-entities": "^2.4.0",
"jquery": "3.6.0",
"localforage": "^1.10.0",
"lodash": "4.17.21",
- "prop-types": "15.7.2",
+ "prop-types": "15.8.1",
"react": "16.12.0",
"react-dom": "16.12.0",
- "semver": "^7.3.5",
+ "semver": "^7.5.2",
"simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
- "string.prototype.replaceall": "^1.0.6",
+ "string.prototype.replaceall": "^1.0.8",
"ua-parser-js": "^1.0.35",
"underscore": "1.13.6"
},
"dependencies": {
- "prop-types": {
- "version": "15.7.2",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
- "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
- "requires": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.8.1"
- }
- },
"react": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
@@ -80625,9 +80611,9 @@
}
},
"html-entities": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz",
- "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg=="
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz",
+ "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ=="
},
"html-escaper": {
"version": "2.0.2",
@@ -89716,9 +89702,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.118",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz",
- "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==",
+ "version": "1.0.126",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz",
+ "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -92752,13 +92738,15 @@
}
},
"string.prototype.replaceall": {
- "version": "1.0.6",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz",
+ "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==",
"requires": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "has-symbols": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
"is-regex": "^1.1.4"
}
},
diff --git a/package.json b/package.json
index 2494716d55f5..a5823e18e357 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.24-7",
+ "version": "1.4.25-1",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -75,7 +75,7 @@
"@react-native-camera-roll/camera-roll": "5.4.0",
"@react-native-clipboard/clipboard": "^1.12.1",
"@react-native-community/geolocation": "^3.0.6",
- "@react-native-community/netinfo": "11.1.0",
+ "@react-native-community/netinfo": "11.2.1",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
"@react-native-firebase/crashlytics": "^12.3.0",
@@ -99,7 +99,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44",
"expo": "^50.0.0-preview.7",
"expo-image": "1.10.1",
"fbjs": "^3.0.2",
@@ -142,7 +142,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.118",
+ "react-native-onyx": "1.0.126",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "^6.7.4",
"react-native-performance": "^5.1.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index b1a6b6895de7..f0f7ab736b78 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -479,7 +479,9 @@ const CONST = {
ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/',
// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
- EXPENSIFY_INBOX_URL: 'https://www.expensify.com/inbox',
+ OLDDOT_URLS: {
+ INBOX: 'inbox',
+ },
SIGN_IN_FORM_WIDTH: 300,
@@ -527,6 +529,7 @@ const CONST = {
TASKCOMPLETED: 'TASKCOMPLETED',
TASKEDITED: 'TASKEDITED',
TASKREOPENED: 'TASKREOPENED',
+ ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER',
POLICYCHANGELOG: {
ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE',
ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET',
@@ -600,6 +603,12 @@ const CONST = {
},
THREAD_DISABLED: ['CREATED'],
},
+ CANCEL_PAYMENT_REASONS: {
+ ADMIN: 'CANCEL_REASON_ADMIN',
+ },
+ ACTIONABLE_MENTION_WHISPER_RESOLUTION: {
+ INVITE: 'invited',
+ },
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
@@ -2725,7 +2734,7 @@ const CONST = {
EXPECTED_OUTPUT: 'FCFA 123,457',
},
- PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg'],
+ PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg', 'docs/index.html'],
// Test tool menu parameters
TEST_TOOL: {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 7538a16d1a2c..37003a09a0cd 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -286,10 +286,6 @@ const ROUTES = {
route: ':iouType/new/merchant/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const,
},
- MONEY_REQUEST_WAYPOINT: {
- route: ':iouType/new/waypoint/:waypointIndex',
- getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}` as const,
- },
MONEY_REQUEST_RECEIPT: {
route: ':iouType/new/receipt/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
@@ -298,10 +294,6 @@ const ROUTES = {
route: ':iouType/new/address/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const,
},
- MONEY_REQUEST_EDIT_WAYPOINT: {
- route: 'r/:threadReportID/edit/distance/:transactionID/waypoint/:waypointIndex',
- getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}` as const,
- },
MONEY_REQUEST_DISTANCE_TAB: {
route: ':iouType/new/:reportID?/distance',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const,
@@ -378,9 +370,9 @@ const ROUTES = {
getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_WAYPOINT: {
- route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') =>
- getUrlWithBackToParam(`create/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo),
+ route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') =>
+ getUrlWithBackToParam(`${action}/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo),
},
// This URL is used as a redirect to one of the create tabs below. This is so that we can message users with a link
// straight to those flows without needing to have optimistic transaction and report IDs.
diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
similarity index 52%
rename from src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
rename to src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
index 6161ba140726..df8a0a30b129 100644
--- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js
+++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx
@@ -1,6 +1,6 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import AttachmentView from '@components/Attachments/AttachmentView';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext';
@@ -10,59 +10,52 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as Download from '@userActions/Download';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import {defaultProps as anchorForAttachmentsOnlyDefaultProps, propTypes as anchorForAttachmentsOnlyPropTypes} from './anchorForAttachmentsOnlyPropTypes';
-
-const propTypes = {
- /** Press in handler for the link */
- onPressIn: PropTypes.func,
-
- /** Press out handler for the link */
- onPressOut: PropTypes.func,
+import type {Download as OnyxDownload} from '@src/types/onyx';
+import type AnchorForAttachmentsOnlyProps from './types';
+type BaseAnchorForAttachmentsOnlyOnyxProps = {
/** If a file download is happening */
- download: PropTypes.shape({
- isDownloading: PropTypes.bool.isRequired,
- }),
-
- ...anchorForAttachmentsOnlyPropTypes,
+ download: OnyxEntry;
};
-const defaultProps = {
- onPressIn: undefined,
- onPressOut: undefined,
- download: {isDownloading: false},
- ...anchorForAttachmentsOnlyDefaultProps,
-};
+type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps &
+ BaseAnchorForAttachmentsOnlyOnyxProps & {
+ /** Press in handler for the link */
+ onPressIn?: () => void;
+
+ /** Press out handler for the link */
+ onPressOut?: () => void;
+ };
-function BaseAnchorForAttachmentsOnly(props) {
- const sourceURL = props.source;
- const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL);
- const sourceID = (sourceURL.match(CONST.REGEX.ATTACHMENT_ID) || [])[1];
- const fileName = props.displayName;
+function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', download, onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) {
+ const sourceURLWithAuth = addEncryptedAuthTokenToURL(source);
+ const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1];
- const isDownloading = props.download && props.download.isDownloading;
+ const isDownloading = download?.isDownloading ?? false;
return (
{({anchor, report, action, checkIfContextMenuActive}) => (
{
if (isDownloading) {
return;
}
Download.setDownload(sourceID, true);
- fileDownload(sourceURLWithAuth, fileName).then(() => Download.setDownload(sourceID, false));
+ fileDownload(sourceURLWithAuth, displayName).then(() => Download.setDownload(sourceID, false));
}}
- onPressIn={props.onPressIn}
- onPressOut={props.onPressOut}
+ onPressIn={onPressIn}
+ onPressOut={onPressOut}
+ // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript.
onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
- accessibilityLabel={fileName}
+ accessibilityLabel={displayName}
role={CONST.ROLE.BUTTON}
>
@@ -73,13 +66,11 @@ function BaseAnchorForAttachmentsOnly(props) {
}
BaseAnchorForAttachmentsOnly.displayName = 'BaseAnchorForAttachmentsOnly';
-BaseAnchorForAttachmentsOnly.propTypes = propTypes;
-BaseAnchorForAttachmentsOnly.defaultProps = defaultProps;
-export default withOnyx({
+export default withOnyx({
download: {
key: ({source}) => {
- const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) || [])[1];
+ const sourceID = (source?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1];
return `${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`;
},
},
diff --git a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js b/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js
deleted file mode 100644
index 9452e615d31c..000000000000
--- a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import PropTypes from 'prop-types';
-import stylePropTypes from '@styles/stylePropTypes';
-
-const propTypes = {
- /** The URL of the attachment */
- source: PropTypes.string,
-
- /** Filename for attachments. */
- displayName: PropTypes.string,
-
- /** Any additional styles to apply */
- style: stylePropTypes,
-};
-
-const defaultProps = {
- source: '',
- style: {},
- displayName: '',
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/AnchorForAttachmentsOnly/index.native.js b/src/components/AnchorForAttachmentsOnly/index.native.tsx
similarity index 62%
rename from src/components/AnchorForAttachmentsOnly/index.native.js
rename to src/components/AnchorForAttachmentsOnly/index.native.tsx
index 3277d51ec058..2e0e94bc0b88 100644
--- a/src/components/AnchorForAttachmentsOnly/index.native.js
+++ b/src/components/AnchorForAttachmentsOnly/index.native.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes';
import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly';
+import type AnchorForAttachmentsOnlyProps from './types';
-function AnchorForAttachmentsOnly(props) {
+function AnchorForAttachmentsOnly(props: AnchorForAttachmentsOnlyProps) {
const styles = useThemeStyles();
return (
;
+};
+
+export default AnchorForAttachmentsOnlyProps;
diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
index f22b1f0c2209..0d554baabeda 100644
--- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
+++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
@@ -4,6 +4,7 @@ import {View} from 'react-native';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ReportUtils from '@libs/ReportUtils';
import DisplayNamesTooltipItem from './DisplayNamesTooltipItem';
import type DisplayNamesProps from './types';
@@ -48,12 +49,12 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit
return (
// Tokenization of string only support prop numberOfLines on Web
{shouldUseFullTitle
- ? fullTitle
+ ? ReportUtils.formatReportLastMessageText(fullTitle)
: displayNamesWithTooltips.map(({displayName, accountID, avatar, login}, index) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js
index 72be7c2b8873..b63ce337a1d9 100644
--- a/src/components/DistanceRequest/index.js
+++ b/src/components/DistanceRequest/index.js
@@ -24,6 +24,7 @@ import variables from '@styles/variables';
import * as MapboxToken from '@userActions/MapboxToken';
import * as Transaction from '@userActions/Transaction';
import * as TransactionEdit from '@userActions/TransactionEdit';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import DistanceRequestFooter from './DistanceRequestFooter';
@@ -170,7 +171,9 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe
* @param {Number} index of the waypoint to edit
*/
const navigateToWaypointEditPage = (index) => {
- Navigation.navigate(isEditingRequest ? ROUTES.MONEY_REQUEST_EDIT_WAYPOINT.getRoute(report.reportID, transactionID, index) : ROUTES.MONEY_REQUEST_WAYPOINT.getRoute('request', index));
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transactionID, report.reportID, index, Navigation.getActiveRouteWithoutParams()),
+ );
};
const getError = () => {
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
similarity index 55%
rename from src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
rename to src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
index 46d04ca9404d..690f2fc6883a 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
@@ -1,27 +1,19 @@
-import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
-import {defaultHTMLElementModels, RenderHTMLConfigProvider, TRenderEngineProvider} from 'react-native-render-html';
-import _ from 'underscore';
+import type {TextProps} from 'react-native';
+import {HTMLContentModel, HTMLElementModel, RenderHTMLConfigProvider, TRenderEngineProvider} from 'react-native-render-html';
import useThemeStyles from '@hooks/useThemeStyles';
import convertToLTR from '@libs/convertToLTR';
import FontUtils from '@styles/utils/FontUtils';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
import * as HTMLEngineUtils from './htmlEngineUtils';
import htmlRenderers from './HTMLRenderers';
-const propTypes = {
+type BaseHTMLEngineProviderProps = ChildrenProps & {
/** Whether text elements should be selectable */
- textSelectable: PropTypes.bool,
+ textSelectable?: boolean;
/** Handle line breaks according to the HTML standard (default on web) */
- enableExperimentalBRCollapsing: PropTypes.bool,
-
- children: PropTypes.node,
-};
-
-const defaultProps = {
- textSelectable: false,
- children: null,
- enableExperimentalBRCollapsing: false,
+ enableExperimentalBRCollapsing?: boolean;
};
// We are using the explicit composite architecture for performance gains.
@@ -29,52 +21,62 @@ const defaultProps = {
// context to RenderHTMLSource components. See https://git.io/JRcZb
// Beware that each prop should be referentialy stable between renders to avoid
// costly invalidations and commits.
-function BaseHTMLEngineProvider(props) {
+function BaseHTMLEngineProvider({textSelectable = false, children, enableExperimentalBRCollapsing = false}: BaseHTMLEngineProviderProps) {
const styles = useThemeStyles();
// Declare nonstandard tags and their content model here
+ /* eslint-disable @typescript-eslint/naming-convention */
const customHTMLElementModels = useMemo(
() => ({
- edited: defaultHTMLElementModels.span.extend({
+ edited: HTMLElementModel.fromCustomModel({
tagName: 'edited',
+ contentModel: HTMLContentModel.textual,
}),
- 'alert-text': defaultHTMLElementModels.div.extend({
+ 'alert-text': HTMLElementModel.fromCustomModel({
tagName: 'alert-text',
mixedUAStyles: {...styles.formError, ...styles.mb0},
+ contentModel: HTMLContentModel.block,
}),
- 'muted-text': defaultHTMLElementModels.div.extend({
+ 'muted-text': HTMLElementModel.fromCustomModel({
tagName: 'muted-text',
mixedUAStyles: {...styles.colorMuted, ...styles.mb0},
+ contentModel: HTMLContentModel.block,
}),
- comment: defaultHTMLElementModels.div.extend({
+ comment: HTMLElementModel.fromCustomModel({
tagName: 'comment',
mixedUAStyles: {whiteSpace: 'pre'},
+ contentModel: HTMLContentModel.block,
}),
- 'email-comment': defaultHTMLElementModels.div.extend({
+ 'email-comment': HTMLElementModel.fromCustomModel({
tagName: 'email-comment',
mixedUAStyles: {whiteSpace: 'normal'},
+ contentModel: HTMLContentModel.block,
}),
- strong: defaultHTMLElementModels.span.extend({
+ strong: HTMLElementModel.fromCustomModel({
tagName: 'strong',
mixedUAStyles: {whiteSpace: 'pre'},
+ contentModel: HTMLContentModel.textual,
}),
- 'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}),
- 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}),
- 'next-step': defaultHTMLElementModels.span.extend({
+ 'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}),
+ 'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}),
+ 'next-step': HTMLElementModel.fromCustomModel({
tagName: 'next-step',
mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16},
+ contentModel: HTMLContentModel.textual,
}),
- 'next-step-email': defaultHTMLElementModels.span.extend({tagName: 'next-step-email'}),
- video: defaultHTMLElementModels.div.extend({
+ 'next-step-email': HTMLElementModel.fromCustomModel({tagName: 'next-step-email', contentModel: HTMLContentModel.textual}),
+ video: HTMLElementModel.fromCustomModel({
tagName: 'video',
mixedUAStyles: {whiteSpace: 'pre'},
+ contentModel: HTMLContentModel.block,
}),
}),
[styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16],
);
+ /* eslint-enable @typescript-eslint/naming-convention */
// We need to memoize this prop to make it referentially stable.
- const defaultTextProps = useMemo(() => ({selectable: props.textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [props.textSelectable]);
+ const defaultTextProps: TextProps = useMemo(() => ({selectable: textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [textSelectable]);
const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
return (
(text.data = convertToLTR(text.data)),
@@ -91,18 +93,17 @@ function BaseHTMLEngineProvider(props) {
- {props.children}
+ {children}
);
}
BaseHTMLEngineProvider.displayName = 'BaseHTMLEngineProvider';
-BaseHTMLEngineProvider.propTypes = propTypes;
-BaseHTMLEngineProvider.defaultProps = defaultProps;
export default BaseHTMLEngineProvider;
diff --git a/src/components/HTMLEngineProvider/htmlEnginePropTypes.js b/src/components/HTMLEngineProvider/htmlEnginePropTypes.js
deleted file mode 100644
index 6c8537c8d228..000000000000
--- a/src/components/HTMLEngineProvider/htmlEnginePropTypes.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import PropTypes from 'prop-types';
-
-const propTypes = {
- children: PropTypes.node,
-
- /** Optional debug flag. Prints the TRT in the console when true. */
- debug: PropTypes.bool,
-};
-
-const defaultProps = {
- children: null,
- debug: false,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/HTMLEngineProvider/htmlEngineUtils.js b/src/components/HTMLEngineProvider/htmlEngineUtils.ts
similarity index 57%
rename from src/components/HTMLEngineProvider/htmlEngineUtils.js
rename to src/components/HTMLEngineProvider/htmlEngineUtils.ts
index 4495cb8ff136..5f082424a565 100644
--- a/src/components/HTMLEngineProvider/htmlEngineUtils.js
+++ b/src/components/HTMLEngineProvider/htmlEngineUtils.ts
@@ -1,4 +1,6 @@
-import lodashGet from 'lodash/get';
+import type {TNode} from 'react-native-render-html';
+
+type Predicate = (node: TNode) => boolean;
const MAX_IMG_DIMENSIONS = 512;
@@ -7,12 +9,12 @@ const MAX_IMG_DIMENSIONS = 512;
* is used by the HTML component in the default renderer for img tags to scale
* down images that would otherwise overflow horizontally.
*
- * @param {string} tagName - The name of the tag for which max width should be constrained.
- * @param {number} contentWidth - The content width provided to the HTML
+ * @param contentWidth - The content width provided to the HTML
* component.
- * @returns {number} The minimum between contentWidth and MAX_IMG_DIMENSIONS
+ * @param tagName - The name of the tag for which max width should be constrained.
+ * @returns The minimum between contentWidth and MAX_IMG_DIMENSIONS
*/
-function computeEmbeddedMaxWidth(tagName, contentWidth) {
+function computeEmbeddedMaxWidth(contentWidth: number, tagName: string): number {
if (tagName === 'img') {
return Math.min(MAX_IMG_DIMENSIONS, contentWidth);
}
@@ -22,21 +24,15 @@ function computeEmbeddedMaxWidth(tagName, contentWidth) {
/**
* Check if tagName is equal to any of our custom tags wrapping chat comments.
*
- * @param {string} tagName
- * @returns {Boolean}
*/
-function isCommentTag(tagName) {
+function isCommentTag(tagName: string): boolean {
return tagName === 'email-comment' || tagName === 'comment';
}
/**
* Check if there is an ancestor node for which the predicate returns true.
- *
- * @param {TNode} tnode
- * @param {Function} predicate
- * @returns {Boolean}
*/
-function isChildOfNode(tnode, predicate) {
+function isChildOfNode(tnode: TNode, predicate: Predicate): boolean {
let currentNode = tnode.parent;
while (currentNode) {
if (predicate(currentNode)) {
@@ -50,21 +46,17 @@ function isChildOfNode(tnode, predicate) {
/**
* Check if there is an ancestor node with name 'comment'.
* Finding node with name 'comment' flags that we are rendering a comment.
- * @param {TNode} tnode
- * @returns {Boolean}
*/
-function isChildOfComment(tnode) {
- return isChildOfNode(tnode, (node) => isCommentTag(lodashGet(node, 'domNode.name', '')));
+function isChildOfComment(tnode: TNode): boolean {
+ return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && isCommentTag(node.domNode.name));
}
/**
* Check if there is an ancestor node with the name 'h1'.
* Finding a node with the name 'h1' flags that we are rendering inside an h1 element.
- * @param {TNode} tnode
- * @returns {Boolean}
*/
-function isChildOfH1(tnode) {
- return isChildOfNode(tnode, (node) => lodashGet(node, 'domNode.name', '').toLowerCase() === 'h1');
+function isChildOfH1(tnode: TNode): boolean {
+ return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'h1');
}
export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1};
diff --git a/src/components/HTMLEngineProvider/index.js b/src/components/HTMLEngineProvider/index.js
deleted file mode 100755
index 8a8e96269411..000000000000
--- a/src/components/HTMLEngineProvider/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import withWindowDimensions from '@components/withWindowDimensions';
-import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';
-import {defaultProps, propTypes} from './htmlEnginePropTypes';
-
-function HTMLEngineProvider(props) {
- return (
-
- {props.children}
-
- );
-}
-
-HTMLEngineProvider.displayName = 'HTMLEngineProvider';
-HTMLEngineProvider.propTypes = propTypes;
-HTMLEngineProvider.defaultProps = defaultProps;
-
-export default withWindowDimensions(HTMLEngineProvider);
diff --git a/src/components/HTMLEngineProvider/index.native.js b/src/components/HTMLEngineProvider/index.native.js
deleted file mode 100755
index f760a5a36649..000000000000
--- a/src/components/HTMLEngineProvider/index.native.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';
-import {defaultProps, propTypes} from './htmlEnginePropTypes';
-
-function HTMLEngineProvider(props) {
- return (
-
- {props.children}
-
- );
-}
-
-HTMLEngineProvider.displayName = 'HTMLEngineProvider';
-HTMLEngineProvider.propTypes = propTypes;
-HTMLEngineProvider.defaultProps = defaultProps;
-
-export default HTMLEngineProvider;
diff --git a/src/components/HTMLEngineProvider/index.native.tsx b/src/components/HTMLEngineProvider/index.native.tsx
new file mode 100755
index 000000000000..c77bcaf7c5e3
--- /dev/null
+++ b/src/components/HTMLEngineProvider/index.native.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';
+
+function HTMLEngineProvider({children}: ChildrenProps) {
+ return {children};
+}
+
+HTMLEngineProvider.displayName = 'HTMLEngineProvider';
+
+export default HTMLEngineProvider;
diff --git a/src/components/HTMLEngineProvider/index.tsx b/src/components/HTMLEngineProvider/index.tsx
new file mode 100755
index 000000000000..9addb549d13a
--- /dev/null
+++ b/src/components/HTMLEngineProvider/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';
+
+function HTMLEngineProvider({children}: ChildrenProps) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+
+ return {children};
+}
+
+HTMLEngineProvider.displayName = 'HTMLEngineProvider';
+
+export default HTMLEngineProvider;
diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx
index 0d2cac253135..d42d471eba5e 100644
--- a/src/components/LottieAnimations/index.tsx
+++ b/src/components/LottieAnimations/index.tsx
@@ -51,6 +51,11 @@ const DotLottieAnimations: Record = {
w: 853,
h: 480,
},
+ Coin: {
+ file: require('@assets/animations/Coin.lottie'),
+ w: 375,
+ h: 240,
+ },
};
export default DotLottieAnimations;
diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js
index 5b59fca6cdae..ce1c9611c733 100644
--- a/src/components/MoneyReportHeader.js
+++ b/src/components/MoneyReportHeader.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useMemo} from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -24,7 +24,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import Button from './Button';
+import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
+import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import participantPropTypes from './participantPropTypes';
import SettlementButton from './SettlementButton';
@@ -94,6 +96,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
isPolicyAdmin && (isApproved || isManager)
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
+ const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
+
+ const cancelPayment = useCallback(() => {
+ IOU.cancelPayment(moneyRequestReport, chatReport);
+ setIsConfirmModalVisible(false);
+ }, [moneyRequestReport, chatReport]);
+
const shouldShowPayButton = useMemo(
() => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport),
[isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport],
@@ -120,6 +129,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
);
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
+ if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) {
+ threeDotsMenuItems.push({
+ icon: Expensicons.Trashcan,
+ text: translate('iou.cancelPayment'),
+ onSelected: () => setIsConfirmModalVisible(true),
+ });
+ }
if (!ReportUtils.isArchivedRoom(chatReport)) {
threeDotsMenuItems.push({
icon: ZoomIcon,
@@ -217,6 +233,16 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
)}
+ setIsConfirmModalVisible(false)}
+ prompt={translate('iou.cancelPaymentConfirmation')}
+ confirmText={translate('iou.cancelPayment')}
+ cancelText={translate('common.dismiss')}
+ danger
+ />
);
}
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 13dce9337673..f66e73a2ef02 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -657,7 +657,7 @@ function MoneyRequestConfirmationList(props) {
/>
{!shouldShowAllFields && (
)}
diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx
index 79b72b378e46..dd8cd115e13f 100644
--- a/src/components/OptionRow.tsx
+++ b/src/components/OptionRow.tsx
@@ -268,8 +268,8 @@ function OptionRow({
onSelectedStatePressed(option)}
disabled={isDisabled}
- role={CONST.ROLE.CHECKBOX}
- accessibilityLabel={CONST.ROLE.CHECKBOX}
+ role={CONST.ROLE.BUTTON}
+ accessibilityLabel={CONST.ROLE.BUTTON}
>
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index 792073b72613..197829bb1ea9 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -80,8 +80,12 @@ class BaseOptionsSelector extends Component {
this.incrementPage = this.incrementPage.bind(this);
this.sliceSections = this.sliceSections.bind(this);
this.calculateAllVisibleOptionsCount = this.calculateAllVisibleOptionsCount.bind(this);
+ this.handleFocusIn = this.handleFocusIn.bind(this);
+ this.handleFocusOut = this.handleFocusOut.bind(this);
this.debouncedUpdateSearchValue = _.debounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
this.relatedTarget = null;
+ this.accessibilityRoles = _.values(CONST.ROLE);
+ this.isWebOrDesktop = [CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform());
const allOptions = this.flattenSections();
const sections = this.sliceSections();
@@ -95,12 +99,15 @@ class BaseOptionsSelector extends Component {
shouldShowReferralModal: false,
errorMessage: '',
paginationPage: 1,
+ disableEnterShortCut: false,
value: '',
};
}
componentDidMount() {
- this.subscribeToKeyboardShortcut();
+ this.subscribeToEnterShortcut();
+ this.subscribeToCtrlEnterShortcut();
+ this.subscribeActiveElement();
if (this.props.isFocused && this.props.autoFocus && this.textInput) {
this.focusTimeout = setTimeout(() => {
@@ -112,9 +119,18 @@ class BaseOptionsSelector extends Component {
}
componentDidUpdate(prevProps, prevState) {
+ if (prevState.disableEnterShortCut !== this.state.disableEnterShortCut) {
+ if (this.state.disableEnterShortCut) {
+ this.unsubscribeEnter();
+ } else {
+ this.subscribeToEnterShortcut();
+ }
+ }
+
if (prevProps.isFocused !== this.props.isFocused) {
if (this.props.isFocused) {
- this.subscribeToKeyboardShortcut();
+ this.subscribeToEnterShortcut();
+ this.subscribeToCtrlEnterShortcut();
} else {
this.unSubscribeFromKeyboardShortcut();
}
@@ -123,7 +139,7 @@ class BaseOptionsSelector extends Component {
// Screen coming back into focus, for example
// when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K.
// Only applies to platforms that support keyboard shortcuts
- if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) {
+ if (this.isWebOrDesktop && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) {
setTimeout(() => {
this.textInput.focus();
}, CONST.ANIMATED_TRANSITION);
@@ -259,7 +275,36 @@ class BaseOptionsSelector extends Component {
this.setState((prevState) => ({shouldShowReferralModal: !prevState.shouldShowReferralModal}));
}
- subscribeToKeyboardShortcut() {
+ handleFocusIn() {
+ const activeElement = document.activeElement;
+ this.setState({
+ disableEnterShortCut: activeElement && this.accessibilityRoles.includes(activeElement.role) && activeElement.role !== CONST.ROLE.PRESENTATION,
+ });
+ }
+
+ handleFocusOut() {
+ this.setState({
+ disableEnterShortCut: false,
+ });
+ }
+
+ subscribeActiveElement() {
+ if (!this.isWebOrDesktop) {
+ return;
+ }
+ document.addEventListener('focusin', this.handleFocusIn);
+ document.addEventListener('focusout', this.handleFocusOut);
+ }
+
+ unSubscribeActiveElement() {
+ if (!this.isWebOrDesktop) {
+ return;
+ }
+ document.removeEventListener('focusin', this.handleFocusIn);
+ document.removeEventListener('focusout', this.handleFocusOut);
+ }
+
+ subscribeToEnterShortcut() {
const enterConfig = CONST.KEYBOARD_SHORTCUTS.ENTER;
this.unsubscribeEnter = KeyboardShortcut.subscribe(
enterConfig.shortcutKey,
@@ -269,7 +314,9 @@ class BaseOptionsSelector extends Component {
true,
() => !this.state.allOptions[this.state.focusedIndex],
);
+ }
+ subscribeToCtrlEnterShortcut() {
const CTRLEnterConfig = CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER;
this.unsubscribeCTRLEnter = KeyboardShortcut.subscribe(
CTRLEnterConfig.shortcutKey,
diff --git a/src/components/ReportActionItem/ActionableItemButtons.tsx b/src/components/ReportActionItem/ActionableItemButtons.tsx
new file mode 100644
index 000000000000..d1f169d2f409
--- /dev/null
+++ b/src/components/ReportActionItem/ActionableItemButtons.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {TranslationPaths} from '@src/languages/types';
+
+type ActionableItem = {
+ isPrimary?: boolean;
+ key: string;
+ onPress: () => void;
+ text: TranslationPaths;
+};
+
+type ActionableItemButtonsProps = {
+ items: ActionableItem[];
+};
+
+function ActionableItemButtons(props: ActionableItemButtonsProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ return (
+
+ {props.items?.map((item) => (
+
+ ))}
+
+ );
+}
+
+ActionableItemButtons.displayName = 'ActionableItemButtton';
+
+export default ActionableItemButtons;
+export type {ActionableItem};
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 036b64af1e4b..7c7998c24c95 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -432,7 +432,7 @@ export default compose(
return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
- transactionViolation: {
+ transactionViolations: {
key: ({report}) => {
const parentReportAction = ReportActionsUtils.getParentReportAction(report);
const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0);
diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js
index c2e3fd2887cd..960618808fd9 100644
--- a/src/components/SelectionList/BaseSelectionList.js
+++ b/src/components/SelectionList/BaseSelectionList.js
@@ -63,6 +63,7 @@ function BaseSelectionList({
disableKeyboardShortcuts = false,
children,
shouldStopPropagation = false,
+ shouldShowTooltips = true,
shouldUseDynamicMaxToRenderPerBatch = false,
rightHandSideComponent,
}) {
@@ -304,7 +305,7 @@ function BaseSelectionList({
const isDisabled = section.isDisabled || item.isDisabled;
const isItemFocused = !isDisabled && focusedIndex === normalizedIndex;
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
- const showTooltip = normalizedIndex < 10;
+ const showTooltip = shouldShowTooltips && normalizedIndex < 10;
return (
{Boolean(item.icons) && (
@@ -24,7 +26,7 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip}) {
text={item.text}
>
{item.text}
@@ -36,7 +38,7 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip}) {
text={item.alternateText}
>
{item.alternateText}
diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js
index 2e380040bd91..f5178112a4c3 100644
--- a/src/components/SelectionList/selectionListPropTypes.js
+++ b/src/components/SelectionList/selectionListPropTypes.js
@@ -190,6 +190,9 @@ const propTypes = {
/** Custom content to display in the footer */
footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+ /** Whether to show the toolip text */
+ shouldShowTooltips: PropTypes.bool,
+
/** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
shouldUseDynamicMaxToRenderPerBatch: PropTypes.bool,
diff --git a/src/components/ValuePicker/ValueSelectorModal.js b/src/components/ValuePicker/ValueSelectorModal.js
index 79608edb3ef4..e45ba873d8a3 100644
--- a/src/components/ValuePicker/ValueSelectorModal.js
+++ b/src/components/ValuePicker/ValueSelectorModal.js
@@ -26,6 +26,9 @@ const propTypes = {
/** Function to call when the user closes the modal */
onClose: PropTypes.func,
+
+ /** Whether to show the toolip text */
+ shouldShowTooltips: PropTypes.bool,
};
const defaultProps = {
@@ -34,9 +37,10 @@ const defaultProps = {
label: '',
onClose: () => {},
onItemSelected: () => {},
+ shouldShowTooltips: true,
};
-function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onItemSelected}) {
+function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onItemSelected, shouldShowTooltips}) {
const styles = useThemeStyles();
const [sectionsData, setSectionsData] = useState([]);
@@ -69,6 +73,7 @@ function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onI
onSelectRow={onItemSelected}
initiallyFocusedOptionKey={selectedItem.value}
shouldStopPropagation
+ shouldShowTooltips={shouldShowTooltips}
/>
diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js
index a21402b9993f..d90529114af4 100644
--- a/src/components/ValuePicker/index.js
+++ b/src/components/ValuePicker/index.js
@@ -34,6 +34,9 @@ const propTypes = {
/** A ref to forward to MenuItemWithTopDescription */
forwardedRef: refPropTypes,
+
+ /** Whether to show the toolip text */
+ shouldShowTooltips: PropTypes.bool,
};
const defaultProps = {
@@ -45,9 +48,10 @@ const defaultProps = {
errorText: '',
furtherDetails: undefined,
onInputChange: () => {},
+ shouldShowTooltips: true,
};
-function ValuePicker({value, label, items, placeholder, errorText, onInputChange, furtherDetails, forwardedRef}) {
+function ValuePicker({value, label, items, placeholder, errorText, onInputChange, furtherDetails, shouldShowTooltips, forwardedRef}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [isPickerVisible, setIsPickerVisible] = useState(false);
@@ -67,7 +71,7 @@ function ValuePicker({value, label, items, placeholder, errorText, onInputChange
hidePickerModal();
};
- const descStyle = value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null;
+ const descStyle = !value || value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null;
const selectedItem = _.find(items, {value});
const selectedLabel = selectedItem ? selectedItem.label : '';
@@ -92,6 +96,7 @@ function ValuePicker({value, label, items, placeholder, errorText, onInputChange
items={items}
onClose={hidePickerModal}
onItemSelected={updateInput}
+ shouldShowTooltips={shouldShowTooltips}
/>
);
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c57b1ce310b5..09fd295cb859 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2,6 +2,7 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import CONST from '@src/CONST';
import type {
AddressLineParams,
+ AdminCanceledRequestParams,
AlreadySignedInParams,
AmountEachParams,
ApprovedAmountParams,
@@ -111,6 +112,7 @@ type AllCountries = Record;
export default {
common: {
cancel: 'Cancel',
+ dismiss: 'Dismiss',
yes: 'Yes',
no: 'No',
ok: 'OK',
@@ -573,6 +575,8 @@ export default {
requestMoney: 'Request money',
sendMoney: 'Send money',
pay: 'Pay',
+ cancelPayment: 'Cancel payment',
+ cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?',
viewDetails: 'View details',
pending: 'Pending',
canceled: 'Canceled',
@@ -609,6 +613,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`,
+ adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `The ${amount} payment has been cancelled by the admin.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
@@ -1966,6 +1971,10 @@ export default {
levelTwoResult: 'Message hidden from channel, plus anonymous warning and message is reported for review.',
levelThreeResult: 'Message removed from channel plus anonymous warning and message is reported for review.',
},
+ actionableMentionWhisperOptions: {
+ invite: 'Invite them',
+ nothing: 'Do nothing',
+ },
teachersUnitePage: {
teachersUnite: 'Teachers Unite',
joinExpensifyOrg: 'Join Expensify.org in eliminating injustice around the world and help teachers split their expenses for classrooms in need!',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 8969ce91a9a5..b977a614ae7e 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,6 +1,7 @@
import CONST from '@src/CONST';
import type {
AddressLineParams,
+ AdminCanceledRequestParams,
AlreadySignedInParams,
AmountEachParams,
ApprovedAmountParams,
@@ -101,6 +102,7 @@ import type {
export default {
common: {
cancel: 'Cancelar',
+ dismiss: 'Descartar',
yes: 'SÃ',
no: 'No',
ok: 'OK',
@@ -566,6 +568,8 @@ export default {
requestMoney: 'Pedir dinero',
sendMoney: 'Enviar dinero',
pay: 'Pagar',
+ cancelPayment: 'Cancelar el pago',
+ cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?',
viewDetails: 'Ver detalles',
pending: 'Pendiente',
canceled: 'Canceló',
@@ -602,6 +606,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `aprobó ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`,
+ adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `El pago de ${amount} ha sido cancelado por el administrador.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`Canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 dÃas.`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
@@ -2430,6 +2435,10 @@ export default {
copy: 'Copiar',
copied: '¡Copiado!',
},
+ actionableMentionWhisperOptions: {
+ invite: 'Invitar',
+ nothing: 'No hacer nada',
+ },
moderation: {
flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.',
chooseAReason: 'Elige abajo un motivo para reportarlo:',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 3185b7a8f6f1..35a5110abf79 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -135,6 +135,8 @@ type WaitingOnBankAccountParams = {submitterDisplayName: string};
type CanceledRequestParams = {amount: string; submitterDisplayName: string};
+type AdminCanceledRequestParams = {amount: string};
+
type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string};
type PaidElsewhereWithAmountParams = {payer?: string; amount: string};
@@ -288,6 +290,7 @@ type TranslationFlatObject = {
};
export type {
+ AdminCanceledRequestParams,
ApprovedAmountParams,
AddressLineParams,
AlreadySignedInParams,
diff --git a/src/libs/DoInteractionTask/index.desktop.ts b/src/libs/DoInteractionTask/index.desktop.ts
new file mode 100644
index 000000000000..73b3cb19ec32
--- /dev/null
+++ b/src/libs/DoInteractionTask/index.desktop.ts
@@ -0,0 +1,10 @@
+import {InteractionManager} from 'react-native';
+
+// For desktop, we should call the callback after all interactions to prevent freezing. See more detail in https://github.com/Expensify/App/issues/28916
+function doInteractionTask(callback: () => void) {
+ return InteractionManager.runAfterInteractions(() => {
+ callback();
+ });
+}
+
+export default doInteractionTask;
diff --git a/src/libs/DoInteractionTask/index.ts b/src/libs/DoInteractionTask/index.ts
new file mode 100644
index 000000000000..dffbb0562b98
--- /dev/null
+++ b/src/libs/DoInteractionTask/index.ts
@@ -0,0 +1,6 @@
+function doInteractionTask(callback: () => void) {
+ callback();
+ return null;
+}
+
+export default doInteractionTask;
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 7286615e6ba6..03a3612d4566 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -51,7 +51,7 @@ type AuthScreensProps = {
isUsingMemoryOnlyKeys: OnyxEntry;
/** The last Onyx update ID was applied to the client */
- lastUpdateIDAppliedToClient: OnyxEntry;
+ initialLastUpdateIDAppliedToClient: OnyxEntry;
};
const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default as React.ComponentType;
@@ -63,6 +63,7 @@ const loadConciergePage = () => require('../../../pages/ConciergePage').default
let timezone: Timezone | null;
let currentAccountID = -1;
let isLoadingApp = false;
+let lastUpdateIDAppliedToClient: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.SESSION,
@@ -112,6 +113,21 @@ Onyx.connect({
},
});
+Onyx.connect({
+ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
+ callback: (value: OnyxEntry) => {
+ lastUpdateIDAppliedToClient = value;
+ },
+});
+
+function handleNetworkReconnect() {
+ if (isLoadingApp) {
+ App.openApp();
+ } else {
+ App.reconnectApp(lastUpdateIDAppliedToClient);
+ }
+}
+
const RootStack = createCustomStackNavigator();
// We want to delay the re-rendering for components(e.g. ReportActionCompose)
// that depends on modal visibility until Modal is completely closed and its focused
@@ -129,7 +145,7 @@ const modalScreenListeners = {
},
};
-function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = false}: AuthScreensProps) {
+function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = false, initialLastUpdateIDAppliedToClient}: AuthScreensProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -156,13 +172,7 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom
}
NetworkConnection.listenForReconnect();
- NetworkConnection.onReconnect(() => {
- if (isLoadingApp) {
- App.openApp();
- } else {
- App.reconnectApp(lastUpdateIDAppliedToClient);
- }
- });
+ NetworkConnection.onReconnect(handleNetworkReconnect);
PusherConnectionManager.init();
Pusher.init({
appKey: CONFIG.PUSHER.APP_KEY,
@@ -180,7 +190,7 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom
if (shouldGetAllData) {
App.openApp();
} else {
- App.reconnectApp(lastUpdateIDAppliedToClient);
+ App.reconnectApp(initialLastUpdateIDAppliedToClient);
}
PriorityMode.autoSwitchToFocusMode();
@@ -329,7 +339,7 @@ export default withOnyx({
isUsingMemoryOnlyKeys: {
key: ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS,
},
- lastUpdateIDAppliedToClient: {
+ initialLastUpdateIDAppliedToClient: {
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
},
})(AuthScreensMemoized);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index ac48c2a379ed..9d4be56ba08f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -104,8 +104,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ADD_DEBIT_CARD]: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
- [SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/NewDistanceRequestWaypointEditorPage').default as React.ComponentType,
- [SCREENS.MONEY_REQUEST.EDIT_WAYPOINT]: () => require('../../../pages/iou/MoneyRequestEditWaypointPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/MoneyRequestWaypointPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.DISTANCE]: () => require('../../../pages/iou/NewDistanceRequestPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../pages/EditRequestReceiptPage').default as React.ComponentType,
});
diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts
index 5f2a607b5f78..1a495e92eb80 100644
--- a/src/libs/Navigation/linkingConfig.ts
+++ b/src/libs/Navigation/linkingConfig.ts
@@ -433,8 +433,6 @@ const linkingConfig: LinkingOptions = {
[SCREENS.MONEY_REQUEST.CATEGORY]: ROUTES.MONEY_REQUEST_CATEGORY.route,
[SCREENS.MONEY_REQUEST.TAG]: ROUTES.MONEY_REQUEST_TAG.route,
[SCREENS.MONEY_REQUEST.MERCHANT]: ROUTES.MONEY_REQUEST_MERCHANT.route,
- [SCREENS.MONEY_REQUEST.WAYPOINT]: ROUTES.MONEY_REQUEST_WAYPOINT.route,
- [SCREENS.MONEY_REQUEST.EDIT_WAYPOINT]: ROUTES.MONEY_REQUEST_EDIT_WAYPOINT.route,
[SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route,
[SCREENS.MONEY_REQUEST.DISTANCE]: ROUTES.MONEY_REQUEST_DISTANCE.route,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 90f5361f11f4..8563db7db172 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -241,12 +241,6 @@ type MoneyRequestNavigatorParamList = {
waypointIndex: string;
threadReportID: number;
};
- [SCREENS.MONEY_REQUEST.EDIT_WAYPOINT]: {
- iouType: string;
- transactionID: string;
- waypointIndex: string;
- threadReportID: number;
- };
[SCREENS.MONEY_REQUEST.DISTANCE]: {
iouType: ValueOf;
reportID: string;
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 988398009dd8..37b7a9424fee 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -420,7 +420,7 @@ function getLastMessageTextForReport(report) {
} else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report);
+ lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
} else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) {
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 1010f8bd82e0..fa68b78fa18f 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
-import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage';
+import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, ReimbursementDeQueuedMessage} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
import type {NotificationPreference} from '@src/types/onyx/Report';
import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
@@ -180,6 +180,11 @@ type OptimisticSubmittedReportAction = Pick<
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;
+type OptimisticCancelPaymentReportAction = Pick<
+ ReportAction,
+ 'actionName' | 'actorAccountID' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
+>;
+
type OptimisticEditedTaskReportAction = Pick<
ReportAction,
'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person'
@@ -1592,9 +1597,13 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry): string {
+function getReimbursementDeQueuedActionMessage(reportAction: OnyxEntry, report: OnyxEntry): string {
+ const amount = CurrencyUtils.convertToDisplayString(Math.abs(report?.total ?? 0), report?.currency);
+ const originalMessage = reportAction?.originalMessage as ReimbursementDeQueuedMessage | undefined;
+ if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN) {
+ return Localize.translateLocal('iou.adminCanceledRequest', {amount});
+ }
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? '';
- const amount = CurrencyUtils.convertToDisplayString(report?.total ?? 0, report?.currency);
return Localize.translateLocal('iou.canceledRequest', {submitterDisplayName, amount});
}
@@ -2877,6 +2886,40 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
};
}
+/**
+ * Builds an optimistic REIMBURSEMENTDEQUEUED report action with a randomly generated reportActionID.
+ *
+ */
+function buildOptimisticCancelPaymentReportAction(expenseReportID: string): OptimisticCancelPaymentReportAction {
+ return {
+ actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED,
+ actorAccountID: currentUserAccountID,
+ message: [
+ {
+ cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN,
+ expenseReportID,
+ type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
+ text: '',
+ },
+ ],
+ originalMessage: {
+ cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN,
+ expenseReportID,
+ },
+ person: [
+ {
+ style: 'strong',
+ text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
+ type: 'TEXT',
+ },
+ ],
+ reportActionID: NumberUtils.rand64(),
+ shouldShow: true,
+ created: DateUtils.getDBTime(),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ };
+}
+
/**
* Builds an optimistic report preview action with a randomly generated reportActionID.
*
@@ -4199,17 +4242,22 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry)
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency) ?? '';
const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true);
- switch (originalMessage.paymentType) {
- case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
- translationKey = 'iou.paidElsewhereWithAmount';
- break;
- case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
- case CONST.IOU.PAYMENT_TYPE.VBBA:
- translationKey = 'iou.paidWithExpensifyWithAmount';
- break;
- default:
- translationKey = 'iou.payerPaidAmount';
- break;
+ // If the payment was cancelled, show the "Owes" message
+ if (!isSettled(IOUReportID)) {
+ translationKey = 'iou.payerOwesAmount';
+ } else {
+ switch (originalMessage.paymentType) {
+ case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
+ translationKey = 'iou.paidElsewhereWithAmount';
+ break;
+ case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
+ case CONST.IOU.PAYMENT_TYPE.VBBA:
+ translationKey = 'iou.paidWithExpensifyWithAmount';
+ break;
+ default:
+ translationKey = 'iou.payerPaidAmount';
+ break;
+ }
}
return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName ?? ''});
}
@@ -4283,7 +4331,8 @@ function hasSmartscanError(reportActions: ReportAction[]) {
if (!ReportActionsUtils.isSplitBillAction(action) && !ReportActionsUtils.isReportPreviewAction(action)) {
return false;
}
- const isReportPreviewError = ReportActionsUtils.isReportPreviewAction(action) && hasMissingSmartscanFields(ReportActionsUtils.getIOUReportIDFromReportActionPreview(action));
+ const IOUReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action);
+ const isReportPreviewError = ReportActionsUtils.isReportPreviewAction(action) && hasMissingSmartscanFields(IOUReportID) && !isSettled(IOUReportID);
const transactionID = (action.originalMessage as IOUMessage).IOUTransactionID ?? '0';
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {};
const isSplitBillError = ReportActionsUtils.isSplitBillAction(action) && TransactionUtils.hasMissingSmartscanFields(transaction as Transaction);
@@ -4435,6 +4484,7 @@ export {
buildOptimisticIOUReportAction,
buildOptimisticReportPreview,
buildOptimisticModifiedExpenseReportAction,
+ buildOptimisticCancelPaymentReportAction,
updateReportPreview,
buildOptimisticTaskReportAction,
buildOptimisticAddCommentReportAction,
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 3a2241bd5494..768dc530cc51 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -398,7 +398,7 @@ function setUpPoliciesAndNavigate(session: OnyxEntry) {
return;
}
if (!isLoggingInAsNewUser && exitTo) {
- Navigation.isNavigationReady()
+ Navigation.waitForProtectedRoutes()
.then(() => {
// We must call goBack() to remove the /transition route from history
Navigation.goBack(ROUTES.HOME);
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 9aa7c52b1ea0..ca572452cc82 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -3328,6 +3328,109 @@ function submitReport(expenseReport) {
);
}
+/**
+ * @param {Object} expenseReport
+ * @param {Object} chatReport
+ */
+function cancelPayment(expenseReport, chatReport) {
+ const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID);
+ const policy = ReportUtils.getPolicy(chatReport.policyID);
+ const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE;
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticReportAction.reportActionID]: {
+ ...optimisticReportAction,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
+ value: {
+ ...expenseReport,
+ lastMessageText: lodashGet(optimisticReportAction, 'message.0.text', ''),
+ lastMessageHtml: lodashGet(optimisticReportAction, 'message.0.html', ''),
+ state: isFree ? CONST.REPORT.STATE.SUBMITTED : CONST.REPORT.STATE.OPEN,
+ stateNum: isFree ? CONST.REPORT.STATE_NUM.PROCESSING : CONST.REPORT.STATE.OPEN,
+ statusNum: isFree ? CONST.REPORT.STATUS.SUBMITTED : CONST.REPORT.STATE.OPEN,
+ },
+ },
+ ...(chatReport.reportID
+ ? [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
+ value: {
+ ...chatReport,
+ hasOutstandingIOU: true,
+ hasOutstandingChildRequest: true,
+ iouReportID: expenseReport.reportID,
+ },
+ },
+ ]
+ : []),
+ ];
+
+ const successData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticReportAction.reportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [expenseReport.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
+ value: {
+ statusNum: CONST.REPORT.STATUS.REIMBURSED,
+ },
+ },
+ ...(chatReport.reportID
+ ? [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
+ value: {
+ hasOutstandingIOU: false,
+ hasOutstandingChildRequest: false,
+ iouReportID: 0,
+ },
+ },
+ ]
+ : []),
+ ];
+
+ API.write(
+ 'CancelPayment',
+ {
+ iouReportID: expenseReport.reportID,
+ chatReportID: chatReport.reportID,
+ managerAccountID: expenseReport.managerID,
+ reportActionID: optimisticReportAction.reportActionID,
+ },
+ {optimisticData, successData, failureData},
+ );
+}
+
/**
* @param {String} paymentType
* @param {Object} chatReport
@@ -3657,4 +3760,5 @@ export {
detachReceipt,
getIOUReportID,
editMoneyRequest,
+ cancelPayment,
};
diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts
index 2fb863467e32..186c9beed970 100644
--- a/src/libs/actions/Link.ts
+++ b/src/libs/actions/Link.ts
@@ -65,7 +65,7 @@ function openOldDotLink(url: string) {
function getInternalNewExpensifyPath(href: string) {
const attrPath = Url.getPathFromURL(href);
return (Url.hasSameExpensifyOrigin(href, CONST.NEW_EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONST.STAGING_NEW_EXPENSIFY_URL) || href.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) &&
- !CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => path === attrPath)
+ !CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => attrPath.startsWith(path))
? attrPath
: '';
}
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 19f6fa3f36b4..b182b7019846 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -341,6 +341,7 @@ function addActions(reportID: string, text = '', file?: File) {
reportComment?: string;
file?: File;
timezone?: string;
+ shouldAllowActionableMentionWhispers?: boolean;
clientCreatedTime?: string;
};
@@ -350,6 +351,7 @@ function addActions(reportID: string, text = '', file?: File) {
commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null,
reportComment: reportCommentText,
file,
+ shouldAllowActionableMentionWhispers: true,
clientCreatedTime: file ? attachmentAction?.created : reportCommentAction?.created,
};
@@ -2585,6 +2587,60 @@ function clearNewRoomFormError() {
});
}
+function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEntry, resolution: ValueOf) {
+ const message = reportAction?.message?.[0];
+ if (!message) {
+ return;
+ }
+
+ const updatedMessage: Message = {
+ ...message,
+ resolution,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportId}`,
+ value: {
+ [reportAction.reportActionID]: {
+ message: [updatedMessage],
+ originalMessage: {
+ resolution,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportId}`,
+ value: {
+ [reportAction.reportActionID]: {
+ message: [message],
+ originalMessage: {
+ resolution: null,
+ },
+ },
+ },
+ },
+ ];
+
+ type ResolveActionableMentionWhisperParams = {
+ reportActionID: string;
+ resolution: ValueOf;
+ };
+
+ const parameters: ResolveActionableMentionWhisperParams = {
+ reportActionID: reportAction.reportActionID,
+ resolution,
+ };
+
+ API.write('ResolveActionableMentionWhisper', parameters, {optimisticData, failureData});
+}
+
export {
searchInServer,
addComment,
@@ -2649,4 +2705,5 @@ export {
getDraftPrivateNote,
updateLastVisitTime,
clearNewRoomFormError,
+ resolveActionableMentionWhisper,
};
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 674d0c000656..430de0557674 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -58,7 +58,7 @@ function addStop(transactionID: string) {
}
function saveWaypoint(transactionID: string, index: string, waypoint: RecentWaypoint | null, isDraft = false) {
- Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
waypoints: {
[`waypoint${index}`]: waypoint,
diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js
index 9609f3f9bd56..5c8a39204467 100644
--- a/src/pages/LogOutPreviousUserPage.js
+++ b/src/pages/LogOutPreviousUserPage.js
@@ -5,10 +5,8 @@ import {Linking} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as SessionUtils from '@libs/SessionUtils';
-import Navigation from '@navigation/Navigation';
import * as Session from '@userActions/Session';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
const propTypes = {
/** The details about the account that the user is signing in with */
@@ -33,6 +31,10 @@ const defaultProps = {
},
};
+// This page is responsible for handling transitions from OldDot. Specifically, it logs the current user
+// out if the transition is for another user.
+//
+// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
function LogOutPreviousUserPage(props) {
useEffect(() => {
Linking.getInitialURL().then((transitionURL) => {
@@ -53,18 +55,11 @@ function LogOutPreviousUserPage(props) {
const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '');
Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
}
-
- const exitTo = lodashGet(props, 'route.params.exitTo', '');
- // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
- // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
- // which is already called when AuthScreens mounts.
- if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !props.account.isLoading && !isLoggingInAsNewUser) {
- Navigation.isNavigationReady().then(() => {
- Navigation.navigate(exitTo);
- });
- }
});
- }, [props]);
+
+ // We only want to run this effect once on mount (when the page first loads after transitioning from OldDot)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
return ;
}
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index 3a58727eddb7..b90ce6bbc247 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
-import {InteractionManager, View} from 'react-native';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
@@ -15,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import doInteractionTask from '@libs/DoInteractionTask';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
import variables from '@styles/variables';
@@ -209,11 +210,16 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i
}, [reports, personalDetails, searchTerm]);
useEffect(() => {
- const interactionTask = InteractionManager.runAfterInteractions(() => {
+ const interactionTask = doInteractionTask(() => {
setDidScreenTransitionEnd(true);
});
- return interactionTask.cancel;
+ return () => {
+ if (!interactionTask) {
+ return;
+ }
+ interactionTask.cancel();
+ };
}, []);
useEffect(() => {
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index f22eda58ce7f..7db39c1ed856 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -285,6 +285,11 @@ export default [
} else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) {
const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportAction);
Clipboard.setString(modifyExpenseMessage);
+ } else if (ReportActionsUtils.isReimbursementDeQueuedAction(reportAction)) {
+ const {expenseReportID} = reportAction.originalMessage;
+ const expenseReport = ReportUtils.getReport(expenseReportID);
+ const displayMessage = ReportUtils.getReimbursementDeQueuedActionMessage(reportAction, expenseReport);
+ Clipboard.setString(displayMessage);
} else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction);
Clipboard.setString(displayMessage);
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index b1130af5d2ff..1f6455ea6630 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -17,6 +17,7 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar
import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes';
import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions';
import RenderHTML from '@components/RenderHTML';
+import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions';
import MoneyReportView from '@components/ReportActionItem/MoneyReportView';
import MoneyRequestAction from '@components/ReportActionItem/MoneyRequestAction';
@@ -37,7 +38,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import ControlSelection from '@libs/ControlSelection';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
@@ -305,6 +305,25 @@ function ReportActionItem(props) {
[props.report, props.action, toggleContextMenuFromActiveReportAction],
);
+ const actionableItemButtons = useMemo(() => {
+ if (!(props.action.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER && !lodashGet(props.action, 'originalMessage.resolution', null))) {
+ return [];
+ }
+ return [
+ {
+ text: 'actionableMentionWhisperOptions.invite',
+ key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
+ onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE),
+ isPrimary: true,
+ },
+ {
+ text: 'actionableMentionWhisperOptions.nothing',
+ key: `${props.action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
+ onPress: () => Report.resolveActionableMentionWhisper(props.report.reportID, props.action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING),
+ },
+ ];
+ }, [props.action, props.report]);
+
/**
* Get the content of ReportActionItem
* @param {Boolean} hovered whether the ReportActionItem is hovered
@@ -420,10 +439,7 @@ function ReportActionItem(props) {
);
} else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) {
- const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID));
- const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency);
-
- children = ;
+ children = ;
} else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
children = ;
} else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) {
@@ -467,6 +483,17 @@ function ReportActionItem(props) {
)}
+ {/**
+ These are the actionable buttons that appear at the bottom of a Concierge message
+ for example: Invite a user mentioned but not a member of the room
+ https://github.com/Expensify/App/issues/32741
+ */}
+ {actionableItemButtons.length > 0 && (
+
+ )}
) : (
- {message}
+ {Str.htmlDecode(message)}
{children}
);
diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx
index 3a71ee8356b3..025b0cbb8b0a 100644
--- a/src/pages/home/report/ReportActionItemMessage.tsx
+++ b/src/pages/home/report/ReportActionItemMessage.tsx
@@ -57,7 +57,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid
const originalMessage = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? action.originalMessage : null;
const iouReportID = originalMessage?.IOUReportID;
if (iouReportID) {
- iouMessage = ReportUtils.getReportPreviewMessage(ReportUtils.getReport(iouReportID), action);
+ iouMessage = ReportUtils.getIOUReportActionDisplayMessage(action);
}
}
diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js
index c11200ccc4db..d1a294881eb9 100644
--- a/src/pages/home/report/ReportActionItemParentAction.js
+++ b/src/pages/home/report/ReportActionItemParentAction.js
@@ -24,6 +24,9 @@ const propTypes = {
/** The id of the report */
reportID: PropTypes.string.isRequired,
+ /** Position index of the report parent action in the overall report FlatList view */
+ index: PropTypes.number.isRequired,
+
/** The id of the parent report */
// eslint-disable-next-line react/no-unused-prop-types
parentReportID: PropTypes.string.isRequired,
@@ -72,7 +75,7 @@ function ReportActionItemParentAction(props) {
displayAsGroup={false}
isMostRecentIOUReportAction={false}
shouldDisplayNewMarker={props.shouldDisplayNewMarker}
- index={0}
+ index={props.index}
/>
)}
diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js
index ba47e804de06..a9ae2b4c73b9 100644
--- a/src/pages/home/report/ReportActionsListItemRenderer.js
+++ b/src/pages/home/report/ReportActionsListItemRenderer.js
@@ -61,6 +61,7 @@ function ReportActionsListItemRenderer({
reportID={report.reportID}
parentReportID={`${report.parentReportID}`}
shouldDisplayNewMarker={shouldDisplayNewMarker}
+ index={index}
/>
) : (
;
-}
-
-MoneyRequestEditWaypointPage.displayName = 'MoneyRequestEditWaypointPage';
-MoneyRequestEditWaypointPage.propTypes = propTypes;
-MoneyRequestEditWaypointPage.defaultProps = defaultProps;
-export default MoneyRequestEditWaypointPage;
diff --git a/src/pages/iou/NewDistanceRequestWaypointEditorPage.js b/src/pages/iou/MoneyRequestWaypointPage.js
similarity index 77%
rename from src/pages/iou/NewDistanceRequestWaypointEditorPage.js
rename to src/pages/iou/MoneyRequestWaypointPage.js
index 269cde577040..2f8b8b9cc729 100644
--- a/src/pages/iou/NewDistanceRequestWaypointEditorPage.js
+++ b/src/pages/iou/MoneyRequestWaypointPage.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
-import WaypointEditor from './WaypointEditor';
+import IOURequestStepWaypoint from './request/step/IOURequestStepWaypoint';
const propTypes = {
/** The transactionID of this request */
@@ -32,9 +32,9 @@ const defaultProps = {
// This component is responsible for grabbing the transactionID from the IOU key
// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that WaypointEditor can subscribe to the transaction.
-function NewDistanceRequestWaypointEditorPage({transactionID, route}) {
+function MoneyRequestWaypointPage({transactionID, route}) {
return (
- iou && iou.transactionID},
-})(NewDistanceRequestWaypointEditorPage);
+})(MoneyRequestWaypointPage);
diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js
deleted file mode 100644
index ab8874091152..000000000000
--- a/src/pages/iou/WaypointEditor.js
+++ /dev/null
@@ -1,292 +0,0 @@
-import {useNavigation} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useMemo, useRef, useState} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import AddressSearch from '@components/AddressSearch';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import ConfirmModal from '@components/ConfirmModal';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import * as Expensicons from '@components/Icon/Expensicons';
-import ScreenWrapper from '@components/ScreenWrapper';
-import transactionPropTypes from '@components/transactionPropTypes';
-import useLocalize from '@hooks/useLocalize';
-import useLocationBias from '@hooks/useLocationBias';
-import useNetwork from '@hooks/useNetwork';
-import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
-import * as ErrorUtils from '@libs/ErrorUtils';
-import Navigation from '@libs/Navigation/Navigation';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import * as Transaction from '@userActions/Transaction';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-
-const propTypes = {
- /** Route params */
- route: PropTypes.shape({
- params: PropTypes.shape({
- /** IOU type */
- iouType: PropTypes.string,
-
- /** Thread reportID */
- threadReportID: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-
- /** ID of the transaction being edited */
- transactionID: PropTypes.string,
-
- /** Index of the waypoint being edited */
- waypointIndex: PropTypes.string,
- }),
- }),
-
- /* Current location coordinates of the user */
- userLocation: PropTypes.shape({
- /** Latitude of the location */
- latitude: PropTypes.number,
-
- /** Longitude of the location */
- longitude: PropTypes.number,
- }),
-
- recentWaypoints: PropTypes.arrayOf(
- PropTypes.shape({
- /** The name of the location */
- name: PropTypes.string,
-
- /** A description of the location (usually the address) */
- description: PropTypes.string,
-
- /** Data required by the google auto complete plugin to know where to put the markers on the map */
- geometry: PropTypes.shape({
- /** Data about the location */
- location: PropTypes.shape({
- /** Latitude of the location */
- lat: PropTypes.number,
-
- /** Longitude of the location */
- lng: PropTypes.number,
- }),
- }),
- }),
- ),
-
- /* Onyx props */
- /** The optimistic transaction for this request */
- transaction: transactionPropTypes,
-};
-
-const defaultProps = {
- route: {},
- recentWaypoints: [],
- transaction: {},
- userLocation: undefined,
-};
-
-function WaypointEditor({route: {params: {iouType = '', transactionID = '', waypointIndex = '', threadReportID = 0}} = {}, transaction, recentWaypoints, userLocation}) {
- const styles = useThemeStyles();
- const {windowWidth} = useWindowDimensions();
- const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false);
- const navigation = useNavigation();
- const isFocused = navigation.isFocused();
- const {translate} = useLocalize();
- const {isOffline} = useNetwork();
- const textInput = useRef(null);
- const parsedWaypointIndex = parseInt(waypointIndex, 10);
- const allWaypoints = lodashGet(transaction, 'comment.waypoints', {});
- const currentWaypoint = lodashGet(allWaypoints, `waypoint${waypointIndex}`, {});
-
- const waypointCount = _.size(allWaypoints);
- const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint)));
- const locationBias = useLocationBias(allWaypoints, userLocation);
- const waypointDescriptionKey = useMemo(() => {
- switch (parsedWaypointIndex) {
- case 0:
- return 'distance.waypointDescription.start';
- case waypointCount - 1:
- return 'distance.waypointDescription.finish';
- default:
- return 'distance.waypointDescription.stop';
- }
- }, [parsedWaypointIndex, waypointCount]);
-
- const waypointAddress = lodashGet(currentWaypoint, 'address', '');
- const isEditingWaypoint = Boolean(threadReportID);
- // Hide the menu when there is only start and finish waypoint
- const shouldShowThreeDotsButton = waypointCount > 2;
- const shouldDisableEditor =
- isFocused &&
- (Number.isNaN(parsedWaypointIndex) || parsedWaypointIndex < 0 || parsedWaypointIndex > waypointCount || (filledWaypointCount < 2 && parsedWaypointIndex >= waypointCount));
-
- const validate = (values) => {
- const errors = {};
- const waypointValue = values[`waypoint${waypointIndex}`] || '';
- if (isOffline && waypointValue !== '' && !ValidationUtils.isValidAddress(waypointValue)) {
- ErrorUtils.addErrorMessage(errors, `waypoint${waypointIndex}`, 'bankAccount.error.address');
- }
-
- // If the user is online, and they are trying to save a value without using the autocomplete, show an error message instructing them to use a selected address instead.
- // That enables us to save the address with coordinates when it is selected
- if (!isOffline && waypointValue !== '' && waypointAddress !== waypointValue) {
- ErrorUtils.addErrorMessage(errors, `waypoint${waypointIndex}`, 'distance.errors.selectSuggestedAddress');
- }
-
- return errors;
- };
-
- const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, waypointIndex, waypoint, isEditingWaypoint);
-
- const submit = (values) => {
- const waypointValue = values[`waypoint${waypointIndex}`] || '';
-
- // Allows letting you set a waypoint to an empty value
- if (waypointValue === '') {
- Transaction.removeWaypoint(transaction, waypointIndex);
- }
-
- // While the user is offline, the auto-complete address search will not work
- // Therefore, we're going to save the waypoint as just the address, and the lat/long will be filled in on the backend
- if (isOffline && waypointValue) {
- const waypoint = {
- lat: null,
- lng: null,
- address: waypointValue,
- name: null,
- };
- saveWaypoint(waypoint);
- }
-
- // Other flows will be handled by selecting a waypoint with selectWaypoint as this is mainly for the offline flow
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- };
-
- const deleteStopAndHideModal = () => {
- Transaction.removeWaypoint(transaction, waypointIndex);
- setIsDeleteStopModalOpen(false);
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- };
-
- const selectWaypoint = (values) => {
- const waypoint = {
- lat: values.lat,
- lng: values.lng,
- address: values.address,
- name: values.name || null,
- };
- saveWaypoint(waypoint);
-
- if (isEditingWaypoint) {
- Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(threadReportID));
- return;
- }
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- };
-
- return (
- textInput.current && textInput.current.focus()}
- shouldEnableMaxHeight
- testID={WaypointEditor.displayName}
- >
-
- {
- Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType));
- }}
- shouldShowThreeDotsButton={shouldShowThreeDotsButton}
- threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)}
- threeDotsMenuItems={[
- {
- icon: Expensicons.Trashcan,
- text: translate('distance.deleteWaypoint'),
- onSelected: () => setIsDeleteStopModalOpen(true),
- },
- ]}
- />
- setIsDeleteStopModalOpen(false)}
- prompt={translate('distance.deleteWaypointConfirmation')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- />
-
- (textInput.current = e)}
- hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''}
- containerStyles={[styles.mt3]}
- label={translate('distance.address')}
- defaultValue={waypointAddress}
- onPress={selectWaypoint}
- maxInputLength={CONST.FORM_CHARACTER_LIMIT}
- renamedInputKeys={{
- address: `waypoint${waypointIndex}`,
- city: null,
- country: null,
- street: null,
- street2: null,
- zipCode: null,
- lat: null,
- lng: null,
- state: null,
- }}
- predefinedPlaces={recentWaypoints}
- resultTypes=""
- />
-
-
-
- );
-}
-
-WaypointEditor.displayName = 'WaypointEditor';
-WaypointEditor.propTypes = propTypes;
-WaypointEditor.defaultProps = defaultProps;
-export default withOnyx({
- userLocation: {
- key: ONYXKEYS.USER_LOCATION,
- },
- transaction: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(route, 'params.transactionID')}`,
- },
- recentWaypoints: {
- key: ONYXKEYS.NVP_RECENT_WAYPOINTS,
-
- // Only grab the most recent 5 waypoints because that's all that is shown in the UI. This also puts them into the format of data
- // that the google autocomplete component expects for it's "predefined places" feature.
- selector: (waypoints) =>
- _.map(waypoints ? waypoints.slice(0, 5) : [], (waypoint) => ({
- name: waypoint.name,
- description: waypoint.address,
- geometry: {
- location: {
- lat: waypoint.lat,
- lng: waypoint.lng,
- },
- },
- })),
- },
-})(WaypointEditor);
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js
index ddf692fedd46..9549a93c8124 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.js
+++ b/src/pages/iou/request/step/IOURequestStepDistance.js
@@ -21,6 +21,7 @@ import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import * as MapboxToken from '@userActions/MapboxToken';
import * as Transaction from '@userActions/Transaction';
+import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
import StepScreenWrapper from './StepScreenWrapper';
@@ -102,7 +103,9 @@ function IOURequestStepDistance({
* @param {Number} index of the waypoint to edit
*/
const navigateToWaypointEditPage = (index) => {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(iouType, transactionID, reportID, index));
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_WAYPOINT.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.REQUEST, transactionID, report.reportID, index, Navigation.getActiveRouteWithoutParams()),
+ );
};
const navigateToNextStep = useCallback(() => {
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js
index 09617026576d..1087018eeed9 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.js
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js
@@ -81,7 +81,7 @@ const defaultProps = {
function IOURequestStepWaypoint({
recentWaypoints,
route: {
- params: {iouType, pageIndex, reportID, transactionID},
+ params: {action, backTo, iouType, pageIndex, reportID, transactionID},
},
transaction,
userLocation,
@@ -135,7 +135,7 @@ function IOURequestStepWaypoint({
return errors;
};
- const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, pageIndex, waypoint, false);
+ const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE);
const submit = (values) => {
const waypointValue = values[`waypoint${pageIndex}`] || '';
@@ -180,7 +180,11 @@ function IOURequestStepWaypoint({
address: values.address,
name: values.name || null,
};
- Transaction.saveWaypoint(transactionID, pageIndex, waypoint, false);
+ Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE);
+ if (backTo) {
+ Navigation.goBack(backTo);
+ return;
+ }
Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(iouType, transactionID, reportID));
};
diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.js b/src/pages/iou/request/step/withFullTransactionOrNotFound.js
index 001159f944e9..7cdbb3484999 100644
--- a/src/pages/iou/request/step/withFullTransactionOrNotFound.js
+++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.js
@@ -70,7 +70,8 @@ export default function (WrappedComponent) {
transaction: {
key: ({route}) => {
const transactionID = lodashGet(route, 'params.transactionID', 0);
- return `${transactionID === CONST.IOU.OPTIMISTIC_TRANSACTION_ID ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
+ const userAction = lodashGet(route, 'params.action', CONST.IOU.ACTION.CREATE);
+ return `${userAction === CONST.IOU.ACTION.CREATE ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
})(WithFullTransactionOrNotFoundWithRef);
diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js
index 81186af3fcd1..a460c95cdfe6 100644
--- a/src/pages/settings/AboutPage/AboutPage.js
+++ b/src/pages/settings/AboutPage/AboutPage.js
@@ -1,17 +1,16 @@
-import React, {useMemo, useRef} from 'react';
-import {ScrollView, View} from 'react-native';
+import React, {useCallback, useMemo, useRef} from 'react';
+import {View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import _ from 'underscore';
-import Logo from '@assets/images/new-expensify.svg';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
-import ImageSVG from '@components/ImageSVG';
+import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout';
+import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
-import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import compose from '@libs/compose';
@@ -22,6 +21,7 @@ import * as Link from '@userActions/Link';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import pkg from '../../../../package.json';
const propTypes = {
@@ -41,6 +41,7 @@ function getFlavor() {
}
function AboutPage(props) {
+ const theme = useTheme();
const styles = useThemeStyles();
const {translate} = props;
const popoverAnchor = useRef(null);
@@ -95,64 +96,61 @@ function AboutPage(props) {
}));
}, [translate, waitForNavigate]);
+ const overlayContent = useCallback(
+ () => (
+
+
+ v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`}
+
+
+ ),
+ // disabling this rule, as we want this to run only on the first render
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
return (
- Navigation.goBack(ROUTES.SETTINGS)}
+ illustration={LottieAnimations.Coin}
+ backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.ABOUT].backgroundColor}
+ overlayContent={overlayContent}
>
- {({safeAreaPaddingBottomStyle}) => (
- <>
- Navigation.goBack(ROUTES.SETTINGS)}
- />
-
-
-
-
-
-
- v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`}
-
- {props.translate('initialSettingsPage.aboutPage.description')}
-
-
-
-
-
-
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '}
-
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')}
- {' '}
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '}
-
- {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')}
-
- .
-
-
-
- >
- )}
-
+
+ {props.translate('footer.aboutExpensify')}
+ {props.translate('initialSettingsPage.aboutPage.description')}
+
+
+
+
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '}
+
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')}
+ {' '}
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '}
+
+ {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')}
+
+ .
+
+
+
);
}
diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js
index d2b91ed6b76b..6e310b9a62bd 100755
--- a/src/pages/settings/InitialSettingsPage.js
+++ b/src/pages/settings/InitialSettingsPage.js
@@ -267,11 +267,11 @@ function InitialSettingsPage(props) {
translationKey: 'initialSettingsPage.goToExpensifyClassic',
icon: Expensicons.NewExpensify,
action: () => {
- Link.openExternalLink(CONST.EXPENSIFY_INBOX_URL);
+ Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX);
},
shouldShowRightIcon: true,
iconRight: Expensicons.NewWindow,
- link: CONST.EXPENSIFY_INBOX_URL,
+ link: Link.buildOldDotURL(CONST.OLDDOT_URLS.INBOX),
},
{
translationKey: 'initialSettingsPage.signOut',
diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js
index 2586be9fb673..8280d9b5c604 100644
--- a/src/pages/settings/Profile/TimezoneSelectPage.js
+++ b/src/pages/settings/Profile/TimezoneSelectPage.js
@@ -96,6 +96,7 @@ function TimezoneSelectPage(props) {
sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]}
initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')}
showScrollIndicator
+ shouldShowTooltips={false}
/>
);
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js
index 3c44f806fdb8..856c0613cec7 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage.js
+++ b/src/pages/settings/Wallet/ExpensifyCardPage.js
@@ -208,7 +208,7 @@ function ExpensifyCardPage({
medium
style={[styles.mh5, styles.mb5]}
text={translate('cardPage.reviewTransaction')}
- onPress={() => Link.openOldDotLink('inbox')}
+ onPress={() => Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX)}
/>
>
) : null}
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js
index bf547bc4bd10..8382014a01e5 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.js
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js
@@ -556,6 +556,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod
}}
onItemSelected={(method) => addPaymentMethodTypePressed(method)}
anchorRef={addPaymentMethodAnchorRef}
+ shouldShowPersonalBankAccountOption
/>
>
);
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index c7a1da7b64ff..2150358a5134 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -57,6 +57,7 @@ const propTypes = {
}).isRequired,
isLoadingReportData: PropTypes.bool,
+ invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number),
...policyPropTypes,
};
@@ -64,6 +65,7 @@ const defaultProps = {
personalDetails: {},
betas: [],
isLoadingReportData: true,
+ invitedEmailsToAccountIDsDraft: {},
...policyDefaultProps,
};
@@ -81,7 +83,10 @@ function WorkspaceInvitePage(props) {
useEffect(() => {
setSearchTerm(SearchInputManager.searchInput);
- }, []);
+ return () => {
+ Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {});
+ };
+ }, [props.route.params.policyID]);
useEffect(() => {
Policy.clearErrors(props.route.params.policyID);
@@ -105,6 +110,12 @@ function WorkspaceInvitePage(props) {
_.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
const newSelectedOptions = [];
+ _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => {
+ if (!_.has(detailsMap, login)) {
+ return;
+ }
+ newSelectedOptions.push({...detailsMap[login], isSelected: true});
+ });
_.each(selectedOptions, (option) => {
newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
});
@@ -323,5 +334,8 @@ export default compose(
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
+ invitedEmailsToAccountIDsDraft: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`,
+ },
}),
)(WorkspaceInvitePage);
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 21c93b87806a..35fab36e5d41 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -77,6 +77,9 @@ const propTypes = {
/** accountID of current user */
accountID: PropTypes.number,
}),
+
+ /** policyID for main workspace */
+ activePolicyID: PropTypes.string,
};
const defaultProps = {
reports: {},
@@ -88,6 +91,7 @@ const defaultProps = {
session: {
accountID: 0,
},
+ activePolicyID: null,
};
function WorkspaceNewRoomPage(props) {
@@ -96,7 +100,7 @@ function WorkspaceNewRoomPage(props) {
const {isOffline} = useNetwork();
const {isSmallScreenWidth} = useWindowDimensions();
const [visibility, setVisibility] = useState(CONST.REPORT.VISIBILITY.RESTRICTED);
- const [policyID, setPolicyID] = useState(null);
+ const [policyID, setPolicyID] = useState(props.activePolicyID);
const [writeCapability, setWriteCapability] = useState(CONST.REPORT.WRITE_CAPABILITIES.ALL);
const wasLoading = usePrevious(props.formState.isLoading);
const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]);
@@ -138,6 +142,13 @@ function WorkspaceNewRoomPage(props) {
Report.clearNewRoomFormError();
}, []);
+ useEffect(() => {
+ if (policyID) {
+ return;
+ }
+ setPolicyID(props.activePolicyID);
+ }, [props.activePolicyID, policyID]);
+
useEffect(() => {
if (!(((wasLoading && !props.formState.isLoading) || (isOffline && props.formState.isLoading)) && _.isEmpty(props.formState.errorFields))) {
return;
@@ -296,6 +307,7 @@ function WorkspaceNewRoomPage(props) {
inputID="policyID"
label={translate('workspace.common.workspace')}
items={workspaceOptions}
+ value={policyID}
onValueChange={setPolicyID}
/>
@@ -320,6 +332,7 @@ function WorkspaceNewRoomPage(props) {
onValueChange={setVisibility}
value={visibility}
furtherDetails={visibilityDescription}
+ shouldShowTooltips={false}
/>
@@ -353,5 +366,10 @@ export default compose(
session: {
key: ONYXKEYS.SESSION,
},
+ activePolicyID: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (account) => (account && account.activePolicyID) || null,
+ initialValue: null,
+ },
}),
)(WorkspaceNewRoomPage);
diff --git a/src/styles/index.ts b/src/styles/index.ts
index df2db6b995df..54326ec575df 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -443,6 +443,10 @@ const styles = (theme: ThemeColors) =>
color: theme.link,
},
+ textIvoryLight: {
+ color: theme.iconColorfulBackground,
+ },
+
textNoWrap: {
...whiteSpace.noWrap,
},
diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts
index 8ac7b0a2359c..4d4234e167ef 100644
--- a/src/styles/theme/themes/dark.ts
+++ b/src/styles/theme/themes/dark.ts
@@ -126,6 +126,10 @@ const darkTheme = {
backgroundColor: colors.productDark200,
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
},
+ [SCREENS.SETTINGS.ABOUT]: {
+ backgroundColor: colors.yellow600,
+ statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
+ },
[SCREENS.RIGHT_MODAL.REFERRAL]: {
backgroundColor: colors.pink800,
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts
index 663b94aa0fc7..9cc5b03ac777 100644
--- a/src/styles/theme/themes/light.ts
+++ b/src/styles/theme/themes/light.ts
@@ -126,6 +126,10 @@ const lightTheme = {
backgroundColor: colors.productLight200,
statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT,
},
+ [SCREENS.SETTINGS.ABOUT]: {
+ backgroundColor: colors.yellow600,
+ statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
+ },
[SCREENS.RIGHT_MODAL.REFERRAL]: {
backgroundColor: colors.pink800,
statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT,
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index c4e30157bf6f..09be2d9e04dd 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -19,6 +19,7 @@ type OriginalMessageActionName =
| 'TASKCOMPLETED'
| 'TASKEDITED'
| 'TASKREOPENED'
+ | 'ACTIONABLEMENTIONWHISPER'
| ValueOf;
type OriginalMessageApproved = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.APPROVED;
@@ -36,6 +37,7 @@ type IOUMessage = {
/** The ID of the iou transaction */
IOUTransactionID?: string;
IOUReportID?: string;
+ expenseReportID?: string;
amount: number;
comment?: string;
currency: string;
@@ -43,10 +45,15 @@ type IOUMessage = {
participantAccountIDs?: number[];
type: ValueOf;
paymentType?: DeepValueOf;
+ cancellationReason?: string;
/** Only exists when we are sending money */
IOUDetails?: IOUDetails;
};
+type ReimbursementDeQueuedMessage = {
+ cancellationReason: string;
+};
+
type OriginalMessageIOU = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.IOU;
originalMessage: IOUMessage;
@@ -109,6 +116,18 @@ type OriginalMessageAddComment = {
};
};
+type OriginalMessageActionableMentionWhisper = {
+ actionName: typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLEMENTIONWHISPER;
+ originalMessage: {
+ inviteeAccountIDs: number[];
+ inviteeEmails: string;
+ lastModified: string;
+ reportID: number;
+ resolution?: ValueOf | null;
+ whisperedTo?: number[];
+ };
+};
+
type OriginalMessageSubmitted = {
actionName: typeof CONST.REPORT.ACTIONS.TYPE.SUBMITTED;
originalMessage: unknown;
@@ -239,6 +258,7 @@ type OriginalMessage =
| OriginalMessageApproved
| OriginalMessageIOU
| OriginalMessageAddComment
+ | OriginalMessageActionableMentionWhisper
| OriginalMessageSubmitted
| OriginalMessageClosed
| OriginalMessageCreated
@@ -260,6 +280,7 @@ export type {
Reaction,
ActionName,
IOUMessage,
+ ReimbursementDeQueuedMessage,
Closed,
OriginalMessageActionName,
ChangeLog,
diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts
index d81335b284ac..b2dc340af606 100644
--- a/src/types/onyx/ReportAction.ts
+++ b/src/types/onyx/ReportAction.ts
@@ -53,6 +53,15 @@ type Message = {
/** ID of a task report */
taskReportID?: string;
+
+ /** Reason of payment cancellation */
+ cancellationReason?: string;
+
+ /** ID of an expense report */
+ expenseReportID?: string;
+
+ /** resolution for actionable mention whisper */
+ resolution?: ValueOf | null;
};
type ImageMetadata = {
diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.js
index ebffc71e4e0e..4a363d1de36b 100644
--- a/tests/unit/MigrationTest.js
+++ b/tests/unit/MigrationTest.js
@@ -51,8 +51,8 @@ describe('Migrations', () => {
it('Should remove any individual reportActions that have no data in Onyx', () =>
Onyx.multiSet({
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {
- 1: null,
- 2: null,
+ 1: {},
+ 2: {},
},
})
.then(PersonalDetailsByAccountID)
diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js
index b8b6eb5e7673..19a89d1c892c 100644
--- a/tests/unit/ReportActionsUtilsTest.js
+++ b/tests/unit/ReportActionsUtilsTest.js
@@ -368,7 +368,7 @@ describe('ReportActionsUtils', () => {
callback: () => {
Onyx.disconnect(connectionID);
const res = ReportActionsUtils.getLastVisibleAction(report.reportID);
- expect(res).toBe(action2);
+ expect(res).toEqual(action2);
resolve();
},
});