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) => ( + )} + {/** + 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(); }, });