diff --git a/android/app/build.gradle b/android/app/build.gradle index 5fb10a8173b6..919c08b99963 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047703 - versionName "1.4.77-3" + versionCode 1001047708 + versionName "1.4.77-8" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/docs/Hidden/Instructions b/docs/Hidden/Instructions new file mode 100644 index 000000000000..940c7ab60d10 --- /dev/null +++ b/docs/Hidden/Instructions @@ -0,0 +1 @@ +This folder is used to house articles that should not be live articles on the helpsite. diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9602b864b1ac..01d7c0869775 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.77.3 + 1.4.77.8 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c123880655b2..40249a9864f8 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.77.3 + 1.4.77.8 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 6cc928676c80..8e786dee39a1 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.77 CFBundleVersion - 1.4.77.3 + 1.4.77.8 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index a552305e84b4..d9cb3daafff5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.77-3", + "version": "1.4.77-8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.77-3", + "version": "1.4.77-8", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -82,7 +82,7 @@ "react-error-boundary": "^4.0.11", "react-fast-pdf": "1.0.13", "react-map-gl": "^7.1.3", - "react-native": "0.73.5", + "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.1", @@ -7730,19 +7730,19 @@ } }, "node_modules/@react-native-community/cli": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.6.tgz", - "integrity": "sha512-647OSi6xBb8FbwFqX9zsJxOzu685AWtrOUWHfOkbKD+5LOpGORw+GQo0F9rWZnB68rLQyfKUZWJeaD00pGv5fw==", - "dependencies": { - "@react-native-community/cli-clean": "12.3.6", - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-doctor": "12.3.6", - "@react-native-community/cli-hermes": "12.3.6", - "@react-native-community/cli-plugin-metro": "12.3.6", - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native-community/cli-types": "12.3.6", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.2.tgz", + "integrity": "sha512-WgoUWwLDcf/G1Su2COUUVs3RzAwnV/vUTdISSpAUGgSc57mPabaAoUctKTnfYEhCnE3j02k3VtaVPwCAFRO3TQ==", + "dependencies": { + "@react-native-community/cli-clean": "12.3.2", + "@react-native-community/cli-config": "12.3.2", + "@react-native-community/cli-debugger-ui": "12.3.2", + "@react-native-community/cli-doctor": "12.3.2", + "@react-native-community/cli-hermes": "12.3.2", + "@react-native-community/cli-plugin-metro": "12.3.2", + "@react-native-community/cli-server-api": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", + "@react-native-community/cli-types": "12.3.2", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", @@ -7761,11 +7761,11 @@ } }, "node_modules/@react-native-community/cli-clean": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.6.tgz", - "integrity": "sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.2.tgz", + "integrity": "sha512-90k2hCX0ddSFPT7EN7h5SZj0XZPXP0+y/++v262hssoey3nhurwF57NGWN0XAR0o9BSW7+mBfeInfabzDraO6A==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "execa": "^5.0.0" } @@ -7835,11 +7835,11 @@ } }, "node_modules/@react-native-community/cli-config": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-12.3.6.tgz", - "integrity": "sha512-JGWSYQ9EAK6m2v0abXwFLEfsqJ1zkhzZ4CV261QZF9MoUNB6h57a274h1MLQR9mG6Tsh38wBUuNfEPUvS1vYew==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-12.3.2.tgz", + "integrity": "sha512-UUCzDjQgvAVL/57rL7eOuFUhd+d+6qfM7V8uOegQFeFEmSmvUUDLYoXpBa5vAK9JgQtSqMBJ1Shmwao+/oElxQ==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "cosmiconfig": "^5.1.0", "deepmerge": "^4.3.0", @@ -7958,28 +7958,29 @@ } }, "node_modules/@react-native-community/cli-debugger-ui": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.6.tgz", - "integrity": "sha512-SjUKKsx5FmcK9G6Pb6UBFT0s9JexVStK5WInmANw75Hm7YokVvHEgtprQDz2Uvy5znX5g2ujzrkIU//T15KQzA==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.2.tgz", + "integrity": "sha512-nSWQUL+51J682DlfcC1bjkUbQbGvHCC25jpqTwHIjmmVjYCX1uHuhPSqQKgPNdvtfOkrkACxczd7kVMmetxY2Q==", "dependencies": { "serve-static": "^1.13.1" } }, "node_modules/@react-native-community/cli-doctor": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-12.3.6.tgz", - "integrity": "sha512-fvBDv2lTthfw4WOQKkdTop2PlE9GtfrlNnpjB818MhcdEnPjfQw5YaTUcnNEGsvGomdCs1MVRMgYXXwPSN6OvQ==", - "dependencies": { - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-12.3.2.tgz", + "integrity": "sha512-GrAabdY4qtBX49knHFvEAdLtCjkmndjTeqhYO6BhsbAeKOtspcLT/0WRgdLIaKODRa61ADNB3K5Zm4dU0QrZOg==", + "dependencies": { + "@react-native-community/cli-config": "12.3.2", + "@react-native-community/cli-platform-android": "12.3.2", + "@react-native-community/cli-platform-ios": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.10.0", "execa": "^5.0.0", "hermes-profile-transformer": "^0.0.6", + "ip": "^1.1.5", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", @@ -8041,6 +8042,11 @@ "node": ">=8" } }, + "node_modules/@react-native-community/cli-doctor/node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + }, "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -8064,14 +8070,15 @@ } }, "node_modules/@react-native-community/cli-hermes": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-12.3.6.tgz", - "integrity": "sha512-sNGwfOCl8OAIjWCkwuLpP8NZbuO0dhDI/2W7NeOGDzIBsf4/c4MptTrULWtGIH9okVPLSPX0NnRyGQ+mSwWyuQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-12.3.2.tgz", + "integrity": "sha512-SL6F9O8ghp4ESBFH2YAPLtIN39jdnvGBKnK4FGKpDCjtB3DnUmDsGFlH46S+GGt5M6VzfG2eeKEOKf3pZ6jUzA==", "dependencies": { - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-platform-android": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", - "hermes-profile-transformer": "^0.0.6" + "hermes-profile-transformer": "^0.0.6", + "ip": "^1.1.5" } }, "node_modules/@react-native-community/cli-hermes/node_modules/ansi-styles": { @@ -8127,6 +8134,11 @@ "node": ">=8" } }, + "node_modules/@react-native-community/cli-hermes/node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + }, "node_modules/@react-native-community/cli-hermes/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8139,11 +8151,11 @@ } }, "node_modules/@react-native-community/cli-platform-android": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.6.tgz", - "integrity": "sha512-DeDDAB8lHpuGIAPXeeD9Qu2+/wDTFPo99c8uSW49L0hkmZJixzvvvffbGQAYk32H0TmaI7rzvzH+qzu7z3891g==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.2.tgz", + "integrity": "sha512-MZ5nO8yi/N+Fj2i9BJcJ9C/ez+9/Ir7lQt49DWRo9YDmzye66mYLr/P2l/qxsixllbbDi7BXrlLpxaEhMrDopg==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.2.4", @@ -8216,11 +8228,11 @@ } }, "node_modules/@react-native-community/cli-platform-ios": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.6.tgz", - "integrity": "sha512-3eZ0jMCkKUO58wzPWlvAPRqezVKm9EPZyaPyHbRPWU8qw7JqkvnRlWIaYDGpjCJgVW4k2hKsEursLtYKb188tg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.2.tgz", + "integrity": "sha512-OcWEAbkev1IL6SUiQnM6DQdsvfsKZhRZtoBNSj9MfdmwotVZSOEZJ+IjZ1FR9ChvMWayO9ns/o8LgoQxr1ZXeg==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", @@ -8293,17 +8305,17 @@ } }, "node_modules/@react-native-community/cli-plugin-metro": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.6.tgz", - "integrity": "sha512-3jxSBQt4fkS+KtHCPSyB5auIT+KKIrPCv9Dk14FbvOaEh9erUWEm/5PZWmtboW1z7CYeNbFMeXm9fM2xwtVOpg==" + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.2.tgz", + "integrity": "sha512-FpFBwu+d2E7KRhYPTkKvQsWb2/JKsJv+t1tcqgQkn+oByhp+qGyXBobFB8/R3yYvRRDCSDhS+atWTJzk9TjM8g==" }, "node_modules/@react-native-community/cli-server-api": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-12.3.6.tgz", - "integrity": "sha512-80NIMzo8b2W+PL0Jd7NjiJW9mgaT8Y8wsIT/lh6mAvYH7mK0ecDJUYUTAAv79Tbo1iCGPAr3T295DlVtS8s4yQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-12.3.2.tgz", + "integrity": "sha512-iwa7EO9XFA/OjI5pPLLpI/6mFVqv8L73kNck3CNOJIUCCveGXBKK0VMyOkXaf/BYnihgQrXh+x5cxbDbggr7+Q==", "dependencies": { - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-debugger-ui": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", @@ -8448,9 +8460,9 @@ } }, "node_modules/@react-native-community/cli-tools": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-12.3.6.tgz", - "integrity": "sha512-FPEvZn19UTMMXUp/piwKZSh8cMEfO8G3KDtOwo53O347GTcwNrKjgZGtLSPELBX2gr+YlzEft3CoRv2Qmo83fQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-12.3.2.tgz", + "integrity": "sha512-nDH7vuEicHI2TI0jac/DjT3fr977iWXRdgVAqPZFFczlbs7A8GQvEdGnZ1G8dqRUmg+kptw0e4hwczAOG89JzQ==", "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", @@ -8548,9 +8560,9 @@ } }, "node_modules/@react-native-community/cli-types": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-12.3.6.tgz", - "integrity": "sha512-xPqTgcUtZowQ8WKOkI9TLGBwH2bGggOC4d2FFaIRST3gTcjrEeGRNeR5aXCzJFIgItIft8sd7p2oKEdy90+01Q==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-12.3.2.tgz", + "integrity": "sha512-9D0UEFqLW8JmS16mjHJxUJWX8E+zJddrHILSH8AJHZ0NNHv4u2DXKdb0wFLMobFxGNxPT+VSOjc60fGvXzWHog==", "dependencies": { "joi": "^17.2.1" } @@ -9002,13 +9014,13 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.73.17", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.17.tgz", - "integrity": "sha512-F3PXZkcHg+1ARIr6FRQCQiB7ZAA+MQXGmq051metRscoLvgYJwj7dgC8pvgy0kexzUkHu5BNKrZeySzUft3xuQ==", + "version": "0.73.16", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.16.tgz", + "integrity": "sha512-eNH3v3qJJF6f0n/Dck90qfC9gVOR4coAXMTdYECO33GfgjTi+73vf/SBqlXw9HICH/RNZYGPM3wca4FRF7TYeQ==", "dependencies": { - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native/dev-middleware": "0.73.8", + "@react-native-community/cli-server-api": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", + "@react-native/dev-middleware": "0.73.7", "@react-native/metro-babel-transformer": "0.73.15", "chalk": "^4.0.0", "execa": "^5.1.1", @@ -9094,9 +9106,8 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.73.8", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.73.8.tgz", - "integrity": "sha512-oph4NamCIxkMfUL/fYtSsE+JbGOnrlawfQ0kKtDQ5xbOjPKotKoXqrs1eGwozNKv7FfQ393stk1by9a6DyASSg==", + "version": "0.73.7", + "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.73.3", @@ -9107,8 +9118,7 @@ "node-fetch": "^2.2.0", "open": "^7.0.3", "serve-static": "^1.13.1", - "temp-dir": "^2.0.0", - "ws": "^6.2.2" + "temp-dir": "^2.0.0" }, "engines": { "node": ">=18" @@ -9139,14 +9149,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "node_modules/@react-native/gradle-plugin": { "version": "0.73.4", "license": "MIT", @@ -16797,8 +16799,7 @@ }, "node_modules/colorette": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -17866,9 +17867,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debounce": { "version": "1.2.1", @@ -20946,9 +20947,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", - "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz", + "integrity": "sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==", "funding": [ { "type": "github", @@ -26588,9 +26589,9 @@ } }, "node_modules/joi": { - "version": "17.13.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", - "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", + "version": "17.12.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", + "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -27355,6 +27356,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/logkitty/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, "node_modules/logkitty/node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -27471,6 +27480,18 @@ "node": ">=8" } }, + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/longest": { "version": "1.0.1", "license": "MIT", @@ -30590,6 +30611,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/qrcode/node_modules/camelcase": { + "version": "5.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", "license": "ISC", @@ -30696,6 +30724,17 @@ "node": ">=8" } }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.10.3", "license": "BSD-3-Clause", @@ -31115,17 +31154,17 @@ } }, "node_modules/react-native": { - "version": "0.73.5", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.5.tgz", - "integrity": "sha512-iHgDArmF4CrhL0qTj+Rn+CBN5pZWUL9lUGl8ub+V9Hwu/vnzQQh8rTMVSwVd2sV6N76KjpE5a4TfIAHkpIHhKg==", + "version": "0.73.4", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.4.tgz", + "integrity": "sha512-VtS+Yr6OOTIuJGDECIYWzNU8QpJjASQYvMtfa/Hvm/2/h5GdB6W9H9TOmh13x07Lj4AOhNMx3XSsz6TdrO4jIg==", "dependencies": { "@jest/create-cache-key-function": "^29.6.3", - "@react-native-community/cli": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", + "@react-native-community/cli": "12.3.2", + "@react-native-community/cli-platform-android": "12.3.2", + "@react-native-community/cli-platform-ios": "12.3.2", "@react-native/assets-registry": "0.73.1", "@react-native/codegen": "0.73.3", - "@react-native/community-cli-plugin": "0.73.17", + "@react-native/community-cli-plugin": "0.73.16", "@react-native/gradle-plugin": "0.73.4", "@react-native/js-polyfills": "0.73.1", "@react-native/normalize-colors": "0.73.2", @@ -37908,26 +37947,6 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, "node_modules/yargs/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5d1244a40dab..b3a31ab149a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.77-3", + "version": "1.4.77-8", "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.", @@ -134,7 +134,7 @@ "react-error-boundary": "^4.0.11", "react-fast-pdf": "1.0.13", "react-map-gl": "^7.1.3", - "react-native": "0.73.5", + "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.1", @@ -302,7 +302,7 @@ "yaml": "^2.2.1" }, "overrides": { - "react-native": "0.73.5", + "react-native": "0.73.4", "expo": "$expo", "react-native-svg": "$react-native-svg" }, diff --git a/src/CONST.ts b/src/CONST.ts index 8e889b82c498..d00cc5e04540 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -662,9 +662,9 @@ const CONST = { DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action DISMISSED_VIOLATION: 'DISMISSEDVIOLATION', DONATION: 'DONATION', // OldDot Action - EXPORTED_TO_CSV: 'EXPORTEDTOCSV', // OldDot Action - EXPORTED_TO_INTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action - EXPORTED_TO_QUICK_BOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action + EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action + EXPORTED_TO_INTEGRATION: 'EXPORTINTEGRATION', // OldDot Action + EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // OldDot Action FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', HOLD_COMMENT: 'HOLDCOMMENT', @@ -1188,6 +1188,10 @@ const CONST = { WEBP: 'image/webp', JPEG: 'image/jpeg', }, + ATTACHMENT_TYPE: { + REPORT: 'r', + NOTE: 'n', + }, IMAGE_OBJECT_POSITION: { TOP: 'top', @@ -1307,12 +1311,13 @@ const CONST = { SYNC: 'sync', ENABLE_NEW_CATEGORIES: 'enableNewCategories', EXPORT: 'export', + TENANT_ID: 'tenantID', IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', INVOICE_STATUS: { - AWAITING_PAYMENT: 'AWT_PAYMENT', DRAFT: 'DRAFT', AWAITING_APPROVAL: 'AWT_APPROVAL', + AWAITING_PAYMENT: 'AWT_PAYMENT', }, IMPORT_TRACKING_CATEGORIES: 'importTrackingCategories', MAPPINGS: 'mappings', @@ -1597,6 +1602,9 @@ const CONST = { ACCOUNTANT: 'accountant', }, }, + ACCESS_VARIANTS: { + CREATE: 'create', + }, }, GROWL: { @@ -1777,7 +1785,8 @@ const CONST = { XERO: 'xero', }, SYNC_STAGE_NAME: { - STARTING_IMPORT: 'startingImport', + STARTING_IMPORT_QBO: 'startingImportQBO', + STARTING_IMPORT_XERO: 'startingImportXero', QBO_IMPORT_MAIN: 'quickbooksOnlineImportMain', QBO_IMPORT_CUSTOMERS: 'quickbooksOnlineImportCustomers', QBO_IMPORT_EMPLOYEES: 'quickbooksOnlineImportEmployees', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ed6e1cd3ce38..a382c16c8136 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -196,7 +196,10 @@ const ROUTES = { SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', SETTINGS_TROUBLESHOOT: 'settings/troubleshoot', - SETTINGS_CONSOLE: 'settings/troubleshoot/console', + SETTINGS_CONSOLE: { + route: 'settings/troubleshoot/console', + getRoute: (backTo?: string) => getUrlWithBackToParam(`settings/troubleshoot/console`, backTo), + }, SETTINGS_SHARE_LOG: { route: 'settings/troubleshoot/console/share-log', getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const, @@ -248,9 +251,10 @@ const ROUTES = { route: 'r/:reportID/details/shareCode', getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const, }, - REPORT_ATTACHMENTS: { - route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const, + ATTACHMENTS: { + route: 'attachment', + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number) => + `attachment?source=${encodeURIComponent(url)}&type=${type}${reportID ? `&reportID=${reportID}` : ''}${accountID ? `&accountID=${accountID}` : ''}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -686,12 +690,12 @@ const ROUTES = { getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const, }, WORKSPACE_TAG_EDIT: { - route: 'settings/workspace/:policyID/tag/:tagName/edit', - getRoute: (policyID: string, tagName: string) => `settings/workspace/${policyID}/tag/${encodeURIComponent(tagName)}/edit` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/edit', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/edit` as const, }, WORKSPACE_TAG_SETTINGS: { - route: 'settings/workspaces/:policyID/tag/:tagName', - getRoute: (policyID: string, tagName: string) => `settings/workspaces/${policyID}/tag/${encodeURIComponent(tagName)}` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', @@ -809,17 +813,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories` as const, }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/cost-centers', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/cost-centers` as const, - }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/region` as const, + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP: { + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/mapping/:categoryId/:categoryName', + getRoute: (policyID: string, categoryId: string, categoryName: string) => + `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/mapping/${categoryId}/${encodeURIComponent(categoryName)}` as const, }, POLICY_ACCOUNTING_XERO_CUSTOMER: { - route: '/settings/workspaces/:policyID/accounting/xero/import/customers', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/accounting/xero/import/customers` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/customers', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/customers` as const, }, POLICY_ACCOUNTING_XERO_TAXES: { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', @@ -830,8 +831,8 @@ const ROUTES = { getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, }, POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT: { - route: '/settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, + route: 'settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, }, POLICY_ACCOUNTING_XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: { route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-date-select', @@ -845,6 +846,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/advanced', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, }, + POLICY_ACCOUNTING_XERO_BILL_STATUS_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-status-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export/purchase-bill-status-selector` as const, + }, POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR: { route: 'settings/workspaces/:policyID/accounting/xero/advanced/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/invoice-account-selector` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4e7243d0eb2c..b4965196ab3b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -7,7 +7,7 @@ import type DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', CONCIERGE: 'Concierge', - REPORT_ATTACHMENTS: 'ReportAttachments', + ATTACHMENTS: 'Attachments', } as const; const SCREENS = { @@ -248,11 +248,11 @@ const SCREENS = { XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', XERO_TRACKING_CATEGORIES: 'Policy_Accounting_Xero_Tracking_Categories', - XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', - XERO_MAP_REGION: 'Policy_Accounting_Xero_Map_Region', + XERO_MAP_TRACKING_CATEGORY: 'Policy_Accounting_Xero_Map_Tracking_Category', XERO_EXPORT: 'Policy_Accounting_Xero_Export', XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: 'Policy_Accounting_Xero_Export_Purchase_Bill_Date_Select', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', + XERO_BILL_STATUS_SELECTOR: 'Policy_Accounting_Xero_Export_Bill_Status_Selector', XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector', XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', diff --git a/src/components/AttachmentContext.ts b/src/components/AttachmentContext.ts new file mode 100644 index 000000000000..4ed6bdc9084f --- /dev/null +++ b/src/components/AttachmentContext.ts @@ -0,0 +1,22 @@ +import {createContext} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type AttachmentContextProps = { + type?: ValueOf; + reportID?: string; + accountID?: number; +}; + +const AttachmentContext = createContext({ + type: undefined, + reportID: undefined, + accountID: undefined, +}); + +AttachmentContext.displayName = 'AttachmentContext'; + +export { + // eslint-disable-next-line import/prefer-default-export + AttachmentContext, +}; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index af7f482198bb..19a9b845093d 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -5,6 +5,7 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {useSharedValue} from 'react-native-reanimated'; +import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -101,6 +102,12 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { /** The report that has this attachment */ report?: OnyxEntry | EmptyObject; + /** The type of the attachment */ + type?: ValueOf; + + /** If the attachment originates from a note, the accountID will represent the author of that note. */ + accountID?: number; + /** Optional callback to fire when we want to do something after modal show. */ onModalShow?: () => void; @@ -156,6 +163,8 @@ function AttachmentModal({ onModalClose = () => {}, isLoading = false, shouldShowNotFoundPage = false, + type = undefined, + accountID = undefined, }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -519,6 +528,8 @@ function AttachmentModal({ )} {!isEmptyObject(report) && !isReceiptAttachment ? ( , reportActions?: OnyxEntry) { - const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; +function extractAttachments( + type: ValueOf, + {reportID, accountID, parentReportAction, reportActions}: {reportID?: string; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry}, +) { + const report = getReport(reportID); + const privateNotes = report?.privateNotes; + const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate @@ -71,6 +78,14 @@ function extractAttachmentsFromReport(parentReportAction?: OnyxEntry { if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) { return; @@ -86,4 +101,4 @@ function extractAttachmentsFromReport(parentReportAction?: OnyxEntry(null); @@ -30,16 +31,21 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, useEffect(() => { const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions); + let targetAttachments: Attachment[] = []; + if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {reportID: report.reportID, accountID}); + } else { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions}); + } - const initialPage = attachmentsFromReport.findIndex(compareImage); + const initialPage = targetAttachments.findIndex(compareImage); // Dismiss the modal when deleting an attachment during its display in preview. if (initialPage === -1 && attachments.find(compareImage)) { Navigation.dismissModal(); } else { setPage(initialPage); - setAttachments(attachmentsFromReport); + setAttachments(targetAttachments); // Update the download button visibility in the parent modal if (setDownloadButtonVisibility) { @@ -47,8 +53,8 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } // Update the parent modal's state with the source and name from the mapped attachments - if (attachmentsFromReport[initialPage] !== undefined && onNavigate) { - onNavigate(attachmentsFromReport[initialPage]); + if (targetAttachments[initialPage] !== undefined && onNavigate) { + onNavigate(targetAttachments[initialPage]); } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 23b285faf10e..947569538d32 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -21,7 +21,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import CarouselActions from './CarouselActions'; import CarouselButtons from './CarouselButtons'; import CarouselItem from './CarouselItem'; -import extractAttachmentsFromReport from './extractAttachmentsFromReport'; +import extractAttachments from './extractAttachments'; import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types'; import useCarouselArrows from './useCarouselArrows'; @@ -33,7 +33,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -57,9 +57,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, useEffect(() => { const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions ?? undefined); + let targetAttachments: Attachment[] = []; + if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {reportID: report.reportID, accountID}); + } else { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined}); + } - if (isEqual(attachments, attachmentsFromReport)) { + if (isEqual(attachments, targetAttachments)) { if (attachments.length === 0) { setPage(-1); setDownloadButtonVisibility?.(false); @@ -67,14 +72,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, return; } - const initialPage = attachmentsFromReport.findIndex(compareImage); + const initialPage = targetAttachments.findIndex(compareImage); // Dismiss the modal when deleting an attachment during its display in preview. if (initialPage === -1 && attachments.find(compareImage)) { Navigation.dismissModal(); } else { setPage(initialPage); - setAttachments(attachmentsFromReport); + setAttachments(targetAttachments); // Update the download button visibility in the parent modal if (setDownloadButtonVisibility) { @@ -82,11 +87,11 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } // Update the parent modal's state with the source and name from the mapped attachments - if (attachmentsFromReport[initialPage] !== undefined && onNavigate) { - onNavigate(attachmentsFromReport[initialPage]); + if (targetAttachments[initialPage] !== undefined && onNavigate) { + onNavigate(targetAttachments[initialPage]); } } - }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate]); + }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate, accountID, report.reportID, type]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts index 8ba3489a5fcf..d31ebbd328cd 100644 --- a/src/components/Attachments/AttachmentCarousel/types.ts +++ b/src/components/Attachments/AttachmentCarousel/types.ts @@ -1,6 +1,8 @@ import type {ViewToken} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; +import type CONST from '@src/CONST'; import type {Report, ReportActions} from '@src/types/onyx'; type UpdatePageProps = { @@ -28,6 +30,12 @@ type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & { /** The report currently being looked at */ report: Report; + /** The type of the attachment */ + type?: ValueOf; + + /** If the attachment originates from a note, the accountID will represent the author of that note. */ + accountID?: number; + /** A callback that is called when swipe-down-to-close gesture happens */ onClose: () => void; }; diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index 7e1d81cc4071..1776a0401403 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -12,7 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; -import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; +import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session} from '@src/types/onyx'; diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.tsx index 37fea2b957a2..10c358ad79c0 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.tsx @@ -6,8 +6,9 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; -import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; +import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline'; import * as Link from '@userActions/Link'; +import * as PolicyAction from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import type {ConnectToQuickbooksOnlineButtonProps} from './types'; @@ -27,6 +28,8 @@ function ConnectToQuickbooksOnlineButton({policyID, shouldDisconnectIntegrationB setIsDisconnectModalOpen(true); return; } + // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO + PolicyAction.enablePolicyTaxes(policyID, false); Link.openLink(getQuickBooksOnlineSetupLink(policyID), environmentURL); }} isDisabled={isOffline} diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index 8abe0e5759fc..bcb2a0833086 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -1,13 +1,15 @@ +import {isEmpty} from 'lodash'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {TranslationPaths} from '@src/languages/types'; -import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import type {ConnectionName, PolicyFeatureName} from '@src/types/onyx/Policy'; import HeaderWithBackButton from './HeaderWithBackButton'; import ScreenWrapper from './ScreenWrapper'; import ScrollView from './ScrollView'; @@ -17,8 +19,8 @@ type ConnectionLayoutProps = { /** Used to set the testID for tests */ displayName: string; - /** Header title for the connection */ - headerTitle: TranslationPaths; + /** Header title to be translated for the connection component */ + headerTitle?: TranslationPaths; /** The subtitle to show in the header */ headerSubtitle?: string; @@ -26,14 +28,14 @@ type ConnectionLayoutProps = { /** React nodes that will be shown */ children?: React.ReactNode; - /** Title of the connection component */ + /** Title to be translated for the connection component */ title?: TranslationPaths; /** The current policyID */ policyID: string; /** Defines which types of access should be verified */ - accessVariants?: PolicyAccessVariant[]; + accessVariants?: AccessVariant[]; /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; @@ -44,18 +46,30 @@ type ConnectionLayoutProps = { /** Style of the title text */ titleStyle?: StyleProp | undefined; + /** Whether to include safe area padding bottom or not */ + shouldIncludeSafeAreaPaddingBottom?: boolean; + /** Whether to use ScrollView or not */ shouldUseScrollView?: boolean; + + /** Used for dynamic header title translation with parameters */ + headerTitleAlreadyTranslated?: string; + + /** Used for dynamic title translation with parameters */ + titleAlreadyTranslated?: string; + + /** Name of the current connection */ + connectionName: ConnectionName; }; -type ConnectionLayoutContentProps = Pick; +type ConnectionLayoutContentProps = Pick; -function ConnectionLayoutContent({title, titleStyle, children}: ConnectionLayoutContentProps) { +function ConnectionLayoutContent({title, titleStyle, children, titleAlreadyTranslated}: ConnectionLayoutContentProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); return ( <> - {title && {translate(title)}} + {title && {titleAlreadyTranslated ?? translate(title)}} {children} ); @@ -72,20 +86,28 @@ function ConnectionLayout({ featureName, contentContainerStyle, titleStyle, + shouldIncludeSafeAreaPaddingBottom, + connectionName, shouldUseScrollView = true, + headerTitleAlreadyTranslated, + titleAlreadyTranslated, }: ConnectionLayoutProps) { const {translate} = useLocalize(); + const policy = PolicyUtils.getPolicy(policyID ?? ''); + const isConnectionEmpty = isEmpty(policy.connections?.[connectionName]); + const renderSelectionContent = useMemo( () => ( {children} ), - [title, titleStyle, children], + [title, titleStyle, children, titleAlreadyTranslated], ); return ( @@ -93,14 +115,15 @@ function ConnectionLayout({ policyID={policyID} accessVariants={accessVariants} featureName={featureName} + shouldBeBlocked={isConnectionEmpty} > Navigation.goBack()} /> diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index 79eaa30ee922..221731bbeef6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -2,6 +2,7 @@ import React, {memo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import {AttachmentContext} from '@components/AttachmentContext'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; @@ -78,19 +79,29 @@ function ImageRenderer({tnode}: ImageRendererProps) { ) : ( {({anchor, report, action, checkIfContextMenuActive}) => ( - { - const route = ROUTES.REPORT_ATTACHMENTS.getRoute(report?.reportID ?? '', source); - Navigation.navigate(route); - }} - onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} - shouldUseHapticsOnLongPress - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={translate('accessibilityHints.viewAttachment')} - > - {thumbnailImageComponent} - + + {({reportID, accountID, type}) => ( + { + if (!source || !type) { + return; + } + + if (reportID) { + const route = ROUTES.ATTACHMENTS?.getRoute(reportID, type, source, accountID); + Navigation.navigate(route); + } + }} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + shouldUseHapticsOnLongPress + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityLabel={translate('accessibilityHints.viewAttachment')} + > + {thumbnailImageComponent} + + )} + )} ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index 095dcb294857..52d14df46471 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -35,7 +35,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { videoDimensions={{width, height}} videoDuration={duration} onShowModalPress={() => { - const route = ROUTES.REPORT_ATTACHMENTS.getRoute(report?.reportID ?? '', sourceURL); + const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID ?? '', CONST.ATTACHMENT_TYPE.REPORT, sourceURL); Navigation.navigate(route); }} /> diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx index 710f045ede4e..a3df93844ca9 100644 --- a/src/components/InitialURLContextProvider.tsx +++ b/src/components/InitialURLContextProvider.tsx @@ -1,5 +1,6 @@ -import React, {createContext} from 'react'; +import React, {createContext, useEffect, useState} from 'react'; import type {ReactNode} from 'react'; +import {Linking} from 'react-native'; import type {Route} from '@src/ROUTES'; /** Initial url that will be opened when NewDot is embedded into Hybrid App. */ @@ -14,7 +15,16 @@ type InitialURLContextProviderProps = { }; function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) { - return {children}; + const [initialURL, setInitialURL] = useState(url); + useEffect(() => { + if (initialURL) { + return; + } + Linking.getInitialURL().then((initURL) => { + setInitialURL(initURL as Route); + }); + }, [initialURL]); + return {children}; } InitialURLContextProvider.displayName = 'InitialURLContextProvider'; diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 5174db6fd32f..2e304bb0214b 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -1,7 +1,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'; import type {MapState} from '@rnmapbox/maps'; import Mapbox, {MarkerView, setAccessToken} from '@rnmapbox/maps'; -import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -30,7 +30,8 @@ const MapView = forwardRef( const cameraRef = useRef(null); const [isIdle, setIsIdle] = useState(false); - const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); + const initialLocation = useMemo(() => initialState && {longitude: initialState.location[0], latitude: initialState.location[1]}, [initialState]); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation ?? initialLocation); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const shouldInitializeCurrentPosition = useRef(true); @@ -42,13 +43,13 @@ const MapView = forwardRef( const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback( (error) => { - if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialState) { + if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) { return; } UserLocation.clearUserLocation(); - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + setCurrentPosition(initialLocation); }, - [initialState], + [initialLocation], ); useFocusEffect( diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index e75d47305d90..f59463442ac8 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -5,7 +5,7 @@ import {useFocusEffect} from '@react-navigation/native'; import mapboxgl from 'mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {MapRef} from 'react-map-gl'; import Map, {Marker} from 'react-map-gl'; import {View} from 'react-native'; @@ -52,7 +52,8 @@ const MapView = forwardRef( const StyleUtils = useStyleUtils(); const [mapRef, setMapRef] = useState(null); - const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); + const initialLocation = useMemo(() => ({longitude: initialState.location[0], latitude: initialState.location[1]}), [initialState]); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation ?? initialLocation); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const [shouldResetBoundaries, setShouldResetBoundaries] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); @@ -66,13 +67,13 @@ const MapView = forwardRef( const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback( (error) => { - if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialState) { + if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) { return; } UserLocation.clearUserLocation(); - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + setCurrentPosition(initialLocation); }, - [initialState], + [initialLocation], ); useFocusEffect( diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 6a92f483b396..7f9a729c161a 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -156,9 +156,15 @@ type MenuItemBaseProps = { /** Error to display at the bottom of the component */ errorText?: MaybePhraseKey; + /** Any additional styles to pass to error text. */ + errorTextStyle?: StyleProp; + /** Hint to display at the bottom of the component */ hintText?: MaybePhraseKey; + /** Should the error text red dot indicator be shown */ + shouldShowRedDotIndicator?: boolean; + /** A boolean flag that gives the icon a green fill if true */ success?: boolean; @@ -318,6 +324,8 @@ function MenuItem( helperText, helperTextStyle, errorText, + errorTextStyle, + shouldShowRedDotIndicator, hintText, success = false, focused = false, @@ -707,9 +715,9 @@ function MenuItem( {!!errorText && ( )} {!!hintText && ( diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index bcf80c058af4..f701f11c25b4 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -426,8 +426,7 @@ function BaseSelectionList( shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} // We're already handling the Enter key press in the useKeyboardShortcut hook, so we don't want the list item to submit the form shouldPreventEnterKeySubmit - // Change this because of lint - rightHandSideComponent={rightHandSideComponent && (typeof rightHandSideComponent === 'function' ? rightHandSideComponent({} as TItem) : rightHandSideComponent)} + rightHandSideComponent={rightHandSideComponent} keyForList={item.keyForList ?? ''} isMultilineSupported={isRowMultilineSupported} onFocus={() => { diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index a2ab477accef..c8c290b562b6 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -1,9 +1,11 @@ +import {isEmpty} from 'lodash'; import React from 'react'; import useLocalize from '@hooks/useLocalize'; -import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {TranslationPaths} from '@src/languages/types'; -import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import type {ConnectionName, PolicyFeatureName} from '@src/types/onyx/Policy'; import HeaderWithBackButton from './HeaderWithBackButton'; import ScreenWrapper from './ScreenWrapper'; import SelectionList from './SelectionList'; @@ -45,13 +47,16 @@ type SelectionScreenProps = { policyID: string; /** Defines which types of access should be verified */ - accessVariants?: PolicyAccessVariant[]; + accessVariants?: AccessVariant[]; /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; /** Whether or not to block user from accessing the page */ shouldBeBlocked?: boolean; + + /** Name of the current connection */ + connectionName: ConnectionName; }; function SelectionScreen({ @@ -67,14 +72,19 @@ function SelectionScreen({ accessVariants, featureName, shouldBeBlocked, + connectionName, }: SelectionScreenProps) { const {translate} = useLocalize(); + + const policy = PolicyUtils.getPolicy(policyID ?? ''); + const isConnectionEmpty = isEmpty(policy.connections?.[connectionName]); + return ( - {disabled && ( + {(Boolean(disabled) || Boolean(showLockIcon)) && ( ; + + /** Whether or not logs should be stored */ + shouldStoreLogs: OnyxEntry; }; type TestToolsModalProps = TestToolsModalOnyxProps; -function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) { +function TestToolsModal({isTestToolsModalOpen = false, shouldStoreLogs = false}: TestToolsModalProps) { const {isDevelopment} = useEnvironment(); const {windowWidth} = useWindowDimensions(); const StyleUtils = useStyleUtils(); @@ -46,6 +53,18 @@ function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) { + {!!shouldStoreLogs && ( + + - )} - {/** + + {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} + + + )} + {/** 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 && ( - - )} - - ) : ( - - )} + {actionableItemButtons.length > 0 && ( + + )} + + ) : ( + + )} + ); } diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 82b49d1e260c..3ff472baee40 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -9,10 +9,12 @@ import * as ReportUtils from '@libs/ReportUtils'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type ReportAttachmentsProps = StackScreenProps; +type ReportAttachmentsProps = StackScreenProps; function ReportAttachments({route}: ReportAttachmentsProps) { const reportID = route.params.reportID; + const type = route.params.type; + const accountID = route.params.accountID; const report = ReportUtils.getReport(reportID); // In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource @@ -20,14 +22,16 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, String(attachment.source)); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID)); Navigation.navigate(routeToNavigate); }, - [reportID], + [reportID, accountID, type], ); return ( { Navigation.closeRHPFlow(); }; @@ -126,15 +121,21 @@ function IOURequestStartPage({ } return ( - - {({safeAreaPaddingBottomStyle}) => ( - + + {({safeAreaPaddingBottomStyle}) => ( - - )} - + )} + + ); } diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index 132388365ada..aee11c89f22c 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -1,3 +1,5 @@ +import type {RouteProp} from '@react-navigation/native'; +import {useRoute} from '@react-navigation/native'; import {format} from 'date-fns'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -21,9 +23,11 @@ import type {Log} from '@libs/Console'; import localFileCreate from '@libs/localFileCreate'; import localFileDownload from '@libs/localFileDownload'; import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type {CapturedLogs} from '@src/types/onyx'; type ConsolePageOnyxProps = { @@ -44,6 +48,8 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const route = useRoute>(); + const logsList = useMemo( () => Object.entries(logs ?? {}) @@ -114,7 +120,7 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { Navigation.goBack(ROUTES.SETTINGS_TROUBLESHOOT)} + onBackButtonPress={() => Navigation.goBack(route.params?.backTo)} /> Navigation.goBack(ROUTES.SETTINGS_CONSOLE)} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONSOLE.getRoute())} /> Navigation.navigate(ROUTES.SETTINGS_CONSOLE)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CONSOLE.getRoute(ROUTES.SETTINGS_TROUBLESHOOT))), }; const baseMenuItems: BaseMenuItem[] = [ diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index cbc94ad37f03..a9d971d4c0f1 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -1,14 +1,17 @@ /* eslint-disable rulesdir/no-negated-variables */ import React, {useEffect} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Policy from '@userActions/Policy/Policy'; +import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -17,13 +20,22 @@ import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const POLICY_ACCESS_VARIANTS = { +const ACCESS_VARIANTS = { [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy) && !!policy?.isPolicyExpenseChatEnabled, [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry) => PolicyUtils.isPolicyAdmin(policy), -} as const satisfies Record boolean>; - -type PolicyAccessVariant = keyof typeof POLICY_ACCESS_VARIANTS; + [CONST.IOU.ACCESS_VARIANTS.CREATE]: (policy: OnyxEntry, report: OnyxEntry, allPolicies: OnyxCollection, iouType?: IOUType) => + !!iouType && + IOUUtils.isValidMoneyRequestType(iouType) && + // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the expense + (isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType)) && + (iouType !== CONST.IOU.TYPE.INVOICE || PolicyUtils.canSendInvoice(allPolicies)), +} as const satisfies Record, iouType?: IOUType) => boolean>; + +type AccessVariant = keyof typeof ACCESS_VARIANTS; type AccessOrNotFoundWrapperOnyxProps = { + /** The report that holds the transaction */ + report: OnyxEntry; + /** The report currently being looked at */ policy: OnyxEntry; @@ -35,11 +47,14 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** The children to render */ children: ((props: AccessOrNotFoundWrapperOnyxProps) => React.ReactNode) | React.ReactNode; + /** The id of the report that holds the transaction */ + reportID?: string; + /** The report currently being looked at */ - policyID: string; + policyID?: string; /** Defines which types of access should be verified */ - accessVariants?: PolicyAccessVariant[]; + accessVariants?: AccessVariant[]; /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; @@ -49,6 +64,12 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** Whether or not to block user from accessing the page */ shouldBeBlocked?: boolean; + + /** The type of the transaction */ + iouType?: IOUType; + + /** The list of all policies */ + allPolicies?: OnyxCollection; } & Pick; type PageNotFoundFallbackProps = Pick & {shouldShowFullScreenFallback: boolean}; @@ -64,7 +85,7 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN /> ) : ( Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(policyID ? ROUTES.WORKSPACE_PROFILE.getRoute(policyID) : ROUTES.HOME)} // eslint-disable-next-line react/jsx-props-no-spreading {...fullPageNotFoundViewProps} /> @@ -72,9 +93,11 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN } function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, shouldBeBlocked, ...props}: AccessOrNotFoundWrapperProps) { - const {policy, policyID, featureName, isLoadingReportData} = props; + const {policy, policyID, report, iouType, allPolicies, featureName, isLoadingReportData} = props; const isPolicyIDInRoute = !!policyID?.length; + const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType); + const isFromGlobalCreate = isEmptyObject(report?.reportID); useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(policy)) { @@ -86,17 +109,17 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPolicyIDInRoute, policyID]); - const shouldShowFullScreenLoadingIndicator = isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); + const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true; const isPageAccessible = accessVariants.reduce((acc, variant) => { - const accessFunction = POLICY_ACCESS_VARIANTS[variant]; - return acc && accessFunction(policy); + const accessFunction = ACCESS_VARIANTS[variant]; + return acc && accessFunction(policy, report, allPolicies ?? null, iouType); }, true); - const shouldShowNotFoundPage = - isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked; + const isPolicyNotAccessible = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id; + const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked; if (shouldShowFullScreenLoadingIndicator) { return ; @@ -115,11 +138,14 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps return callOrReturn(props.children, props); } -export type {PolicyAccessVariant}; +export type {AccessVariant}; export default withOnyx({ + report: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + }, policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index a3f7a65d179e..699ba9a14564 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -1,7 +1,8 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,12 +14,14 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as Category from '@userActions/Policy/Category'; import * as Policy from '@userActions/Policy/Policy'; import * as Tag from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -28,6 +31,12 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow'; +type ItemType = 'organize' | 'integrate'; +type ConnectionWarningModalState = { + isOpen: boolean; + itemType?: ItemType; +}; + type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; type Item = { @@ -55,6 +64,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const {canUseAccountingIntegrations} = usePermissions(); const hasAccountingConnection = !!policy?.areConnectionsEnabled && !isEmptyObject(policy?.connections); const isSyncTaxEnabled = !!policy?.connections?.quickbooksOnline?.config.syncTax || !!policy?.connections?.xero?.config.importTaxRates; + const policyID = policy?.id ?? ''; + + const [connectionWarningModalState, setConnectionWarningModalState] = useState({isOpen: false}); const spendItems: Item[] = [ { @@ -88,6 +100,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areCategoriesEnabled, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'organize', + }); + return; + } Category.enablePolicyCategories(policy?.id ?? '', isEnabled); }, }, @@ -99,6 +118,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.areTagsEnabled, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'organize', + }); + return; + } Tag.enablePolicyTags(policy?.id ?? '', isEnabled); }, }, @@ -107,9 +133,16 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro titleTranslationKey: 'workspace.moreFeatures.taxes.title', subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle', isActive: (policy?.tax?.trackingEnabled ?? false) || isSyncTaxEnabled, - disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline?.data?.country === CONST.COUNTRY.US, + disabled: hasAccountingConnection, pendingAction: policy?.pendingFields?.tax, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'organize', + }); + return; + } Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled); }, }, @@ -123,6 +156,13 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive: !!policy?.areConnectionsEnabled, pendingAction: policy?.pendingFields?.areConnectionsEnabled, action: (isEnabled: boolean) => { + if (hasAccountingConnection) { + setConnectionWarningModalState({ + isOpen: true, + itemType: 'integrate', + }); + return; + } Policy.enablePolicyConnections(policy?.id ?? '', isEnabled); }, disabled: hasAccountingConnection, @@ -166,7 +206,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro isActive={item.isActive} pendingAction={item.pendingAction} onToggle={item.action} - disabled={item.disabled} + showLockIcon={item.disabled} errors={item.errors} onCloseError={item.onCloseError} /> @@ -207,6 +247,17 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }, [fetchFeatures]), ); + const getConnectionWarningPrompt = useCallback(() => { + switch (connectionWarningModalState.itemType) { + case 'organize': + return translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText'); + case 'integrate': + return translate('workspace.moreFeatures.connectionsWarningModal.disconnectText'); + default: + return undefined; + } + }, [connectionWarningModalState.itemType, translate]); + return ( {sections.map(renderSection)} + + { + setConnectionWarningModalState({ + isOpen: false, + itemType: undefined, + }); + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + }} + onCancel={() => + setConnectionWarningModalState({ + isOpen: false, + itemType: undefined, + }) + } + isVisible={connectionWarningModalState.isOpen} + prompt={getConnectionWarningPrompt()} + confirmText={translate('workspace.moreFeatures.connectionsWarningModal.manageSettings')} + cancelText={translate('common.cancel')} + /> ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 5716812ced16..382c1dde6d47 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -351,6 +351,7 @@ export default withOnyx; }; @@ -101,7 +104,7 @@ function accountingIntegrationData( } } -function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataFetchNeeded}: PolicyAccountingPageProps) { +function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccountingPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -129,7 +132,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF { icon: Expensicons.Sync, text: translate('workspace.accounting.syncNow'), - onSelected: () => syncConnection(policyID), + onSelected: () => syncConnection(policyID, connectedIntegration), disabled: isOffline, }, { @@ -138,10 +141,10 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF onSelected: () => setIsDisconnectModalOpen(true), }, ], - [translate, policyID, isOffline], + [translate, policyID, isOffline, connectedIntegration], ); - const connectionsMenuItems: MenuItemProps[] = useMemo(() => { + const connectionsMenuItems: MenuItemData[] = useMemo(() => { if (isEmptyObject(policy?.connections) && !isSyncInProgress) { return accountingIntegrations.map((integration) => { const integrationData = accountingIntegrationData(integration, policyID, translate); @@ -160,16 +163,19 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF if (!connectedIntegration) { return []; } + const shouldShowSynchronizationError = hasSynchronizationError(policy, connectedIntegration); const integrationData = accountingIntegrationData(connectedIntegration, policyID, translate); const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; return [ { ...iconProps, interactive: false, - wrapperStyle: [styles.sectionMenuItemTopDescription], + wrapperStyle: [styles.sectionMenuItemTopDescription, shouldShowSynchronizationError && styles.pb0], shouldShowRightComponent: true, title: integrationData?.title, - + errorText: shouldShowSynchronizationError ? translate('workspace.accounting.syncError', connectedIntegration) : undefined, + errorTextStyle: [styles.mt5], + shouldShowRedDotIndicator: true, description: isSyncInProgress ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) : translate('workspace.accounting.lastSync'), @@ -196,7 +202,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF ), }, - ...(policyConnectedToXero + ...(policyConnectedToXero && !shouldShowSynchronizationError ? [ { description: translate('workspace.xero.organization'), @@ -212,10 +218,12 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF } Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id ?? '')); }, + pendingAction: policy?.connections?.xero?.config?.pendingFields?.tenantID, + brickRoadIndicator: policy?.connections?.xero?.config?.errorFields?.tenantID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, ] : []), - ...(isEmptyObject(policy?.connections) + ...(isEmptyObject(policy?.connections) || shouldShowSynchronizationError ? [] : [ { @@ -245,21 +253,25 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF ]), ]; }, [ - connectedIntegration, - connectionSyncProgress?.stageInProgress, - currentXeroOrganization, - currentXeroOrganizationName, - tenants, + policy, isSyncInProgress, - overflowMenu, - policy?.connections, - policyConnectedToXero, + connectedIntegration, policyID, - styles, + translate, + styles.sectionMenuItemTopDescription, + styles.pb0, + styles.mt5, + styles.popoverMenuIcon, + styles.fontWeightNormal, + connectionSyncProgress?.stageInProgress, theme.spinner, + overflowMenu, threeDotsMenuPosition, - translate, + policyConnectedToXero, + currentXeroOrganizationName, + tenants.length, accountingIntegrations, + currentXeroOrganization?.id, ]); const otherIntegrationsItems = useMemo(() => { @@ -292,21 +304,6 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF accountingIntegrations, ]); - const headerThreeDotsMenuItems: ThreeDotsMenuProps['menuItems'] = [ - { - icon: Expensicons.Key, - shouldShowRightIcon: true, - iconRight: Expensicons.NewWindow, - text: translate('workspace.accounting.enterCredentials'), - onSelected: () => {}, - }, - { - icon: Expensicons.Trashcan, - text: translate('workspace.accounting.disconnect'), - onSelected: () => setIsDisconnectModalOpen(true), - }, - ]; - return ( @@ -336,30 +331,28 @@ function PolicyAccountingPage({policy, connectionSyncProgress, isConnectionDataF titleStyles={styles.accountSettingsSectionTitle} childrenStyles={styles.pt5} > - {isConnectionDataFetchNeeded ? ( - - - - ) : ( - <> + {connectionsMenuItems.map((menuItem) => ( + + + + ))} + {otherIntegrationsItems && ( + - {otherIntegrationsItems && ( - - - - )} - + )} diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx index d433977e50d5..a24e6a8c2f65 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx @@ -28,6 +28,7 @@ function QuickbooksChartOfAccountsPage({policy}: WithPolicyProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={[styles.pb2, styles.ph5]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.QBO} > diff --git a/src/pages/workspace/accounting/xero/XeroImportPage.tsx b/src/pages/workspace/accounting/xero/XeroImportPage.tsx index 0d9b96c46003..3450fbe8deb3 100644 --- a/src/pages/workspace/accounting/xero/XeroImportPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroImportPage.tsx @@ -1,15 +1,12 @@ import React, {useMemo} from 'react'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {getCurrentXeroOrganizationName} from '@libs/PolicyUtils'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import CONST from '@src/CONST'; @@ -37,7 +34,7 @@ function XeroImportPage({policy}: WithPolicyProps) { description: translate('workspace.xero.trackingCategories'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)), hasError: !!errorFields?.importTrackingCategories, - title: importTrackingCategories ? translate('workspace.accounting.importTypes.TAG') : translate('workspace.xero.notImported'), + title: importTrackingCategories ? translate('workspace.accounting.imported') : translate('workspace.xero.notImported'), pendingAction: pendingFields?.importTrackingCategories, }, { @@ -75,39 +72,34 @@ function XeroImportPage({policy}: WithPolicyProps) { ); return ( - - - - - {translate('workspace.xero.importDescription')} - {sections.map((section) => ( - - - - ))} - - - + {translate('workspace.xero.importDescription')} + + {sections.map((section) => ( + + + + ))} + ); } diff --git a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx deleted file mode 100644 index e5ec85b05c3a..000000000000 --- a/src/pages/workspace/accounting/xero/XeroMapRegionsToConfigurationPage.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, {useCallback, useMemo} from 'react'; -import ConnectionLayout from '@components/ConnectionLayout'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as Connections from '@libs/actions/connections'; -import {getTrackingCategory} from '@libs/actions/connections/ConnectToXero'; -import Navigation from '@libs/Navigation/Navigation'; -import type {WithPolicyProps} from '@pages/workspace/withPolicy'; -import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ROUTES from '@src/ROUTES'; - -function XeroMapRegionsToConfigurationPage({policy}: WithPolicyProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - - const policyID = policy?.id ?? ''; - const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION); - - const optionsList = useMemo( - () => - Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).map((option) => ({ - value: option, - text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), - keyForList: option, - isSelected: option.toLowerCase() === category?.value?.toLowerCase(), - })), - [translate, category], - ); - - const updateMapping = useCallback( - (option: {value: string}) => { - if (option.value !== category?.value) { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { - ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), - }); - } - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); - }, - [category, policyID, policy?.connections?.xero?.config?.mappings], - ); - - return ( - - - - ); -} - -XeroMapRegionsToConfigurationPage.displayName = 'XeroMapRegionsToConfigurationPage'; -export default withPolicyConnections(XeroMapRegionsToConfigurationPage); diff --git a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroMapTrackingCategoryConfigurationPage.tsx similarity index 53% rename from src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx rename to src/pages/workspace/accounting/xero/XeroMapTrackingCategoryConfigurationPage.tsx index eec922522468..d81bd7d92865 100644 --- a/src/pages/workspace/accounting/xero/XeroMapCostCentersToConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroMapTrackingCategoryConfigurationPage.tsx @@ -1,3 +1,4 @@ +import {useRoute} from '@react-navigation/native'; import React, {useCallback, useMemo} from 'react'; import ConnectionLayout from '@components/ConnectionLayout'; import SelectionList from '@components/SelectionList'; @@ -5,7 +6,6 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import {getTrackingCategory} from '@libs/actions/connections/ConnectToXero'; import Navigation from '@libs/Navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; @@ -13,13 +13,26 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; -function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { +type RouteParams = { + categoryId?: string; + categoryName?: string; +}; + +const TRACKING_CATEGORIES_KEY = 'trackingCategory_'; + +function XeroMapTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); + const route = useRoute(); + const params = route.params as RouteParams; const styles = useThemeStyles(); - + const categoryId = params?.categoryId ?? ''; + const categoryName = decodeURIComponent(params?.categoryName ?? ''); const policyID = policy?.id ?? ''; + const {trackingCategories} = policy?.connections?.xero?.data ?? {}; + const {mappings} = policy?.connections?.xero?.config ?? {}; - const category = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS); + const currentTrackingCategory = trackingCategories?.find((category) => category.id === categoryId); + const currentTrackingCategoryValue = currentTrackingCategory ? mappings?.[`${TRACKING_CATEGORIES_KEY}${currentTrackingCategory.id}`] ?? '' : ''; const optionsList = useMemo( () => @@ -27,45 +40,47 @@ function XeroMapCostCentersToConfigurationPage({policy}: WithPolicyProps) { value: option, text: translate(`workspace.xero.trackingCategoriesOptions.${option.toLowerCase()}` as TranslationPaths), keyForList: option, - isSelected: option.toLowerCase() === category?.value?.toLowerCase(), + isSelected: option === currentTrackingCategoryValue, })), - [translate, category], + [translate, currentTrackingCategoryValue], ); const updateMapping = useCallback( (option: {value: string}) => { - if (option.value !== category?.value) { + if (option.value !== categoryName) { Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.MAPPINGS, { ...(policy?.connections?.xero?.config?.mappings ?? {}), - ...(category?.id ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`]: option.value} : {}), + ...(categoryId ? {[`${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${categoryId}`]: option.value} : {}), }); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES.getRoute(policyID)); }, - [category, policyID, policy?.connections?.xero?.config?.mappings], + [categoryId, categoryName, policyID, policy?.connections?.xero?.config?.mappings], ); return ( option.isSelected)?.keyForList} shouldDebounceRowSelect /> ); } -XeroMapCostCentersToConfigurationPage.displayName = 'XeroMapCostCentersToConfigurationPage'; -export default withPolicyConnections(XeroMapCostCentersToConfigurationPage); +XeroMapTrackingCategoryConfigurationPage.displayName = 'XeroMapTrackingCategoryConfigurationPage'; +export default withPolicyConnections(XeroMapTrackingCategoryConfigurationPage); diff --git a/src/pages/workspace/accounting/xero/XeroOrganizationConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroOrganizationConfigurationPage.tsx index 85b0f20071ab..ae882d690bd0 100644 --- a/src/pages/workspace/accounting/xero/XeroOrganizationConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroOrganizationConfigurationPage.tsx @@ -1,8 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useMemo} from 'react'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; +import ConnectionLayout from '@components/ConnectionLayout'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; @@ -10,12 +9,13 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {updatePolicyConnectionConfig} from '@libs/actions/connections'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {findCurrentXeroOrganization, getXeroTenants} from '@libs/PolicyUtils'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -30,7 +30,8 @@ function XeroOrganizationConfigurationPage({ const {translate} = useLocalize(); const styles = useThemeStyles(); const tenants = useMemo(() => getXeroTenants(policy ?? undefined), [policy]); - const currentXeroOrganization = findCurrentXeroOrganization(tenants, policy?.connections?.xero?.config?.tenantID); + const xeroConfig = policy?.connections?.xero?.config; + const currentXeroOrganization = findCurrentXeroOrganization(tenants, xeroConfig?.tenantID); const policyID = policy?.id ?? ''; @@ -46,34 +47,36 @@ function XeroOrganizationConfigurationPage({ return; } - updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, 'tenantID', keyForList); + updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.TENANT_ID, keyForList); Navigation.goBack(ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID)); }; return ( - - Policy.clearXeroErrorField(policyID, CONST.XERO_CONFIG.TENANT_ID)} > - - - {translate('workspace.xero.organizationDescription')} - - - - + {translate('workspace.xero.organizationDescription')} + + + ); } diff --git a/src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx index e07941338c69..716ba6b17ccb 100644 --- a/src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx @@ -26,6 +26,7 @@ function XeroTaxesConfigurationPage({policy}: WithPolicyProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={[styles.pb2, styles.ph5]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} > { - const availableCategories = []; - - const costCenterCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.COST_CENTERS)?.value ?? ''; - const regionCategoryValue = getTrackingCategory(policy, CONST.XERO_CONFIG.TRACKING_CATEGORY_FIELDS.REGION)?.value ?? ''; - if (costCenterCategoryValue) { - const isValidOption = Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).findIndex((option) => option.toLowerCase() === costCenterCategoryValue.toLowerCase()) > -1; - availableCategories.push({ - description: translate('workspace.xero.mapXeroCostCentersTo'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS.getRoute(policyID)), - title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${costCenterCategoryValue.toLowerCase()}` as TranslationPaths) : '', - }); - } - - if (regionCategoryValue) { - const isValidOption = Object.values(CONST.XERO_CONFIG.TRACKING_CATEGORY_OPTIONS).findIndex((option) => option.toLowerCase() === regionCategoryValue.toLowerCase()) > -1; - availableCategories.push({ - description: translate('workspace.xero.mapXeroRegionsTo'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION.getRoute(policyID)), - title: isValidOption ? translate(`workspace.xero.trackingCategoriesOptions.${regionCategoryValue.toLowerCase()}` as TranslationPaths) : '', - }); - } - return availableCategories; + const trackingCategories = getTrackingCategories(policy); + return trackingCategories.map((category: XeroTrackingCategory & {value: string}) => ({ + description: translate('workspace.xero.mapTrackingCategoryTo', {categoryName: category.name}) as TranslationPaths, + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP.getRoute(policyID, category.id, category.name)), + title: translate(`workspace.xero.trackingCategoriesOptions.${category.value.toLowerCase()}` as TranslationPaths), + })); }, [translate, policy, policyID]); return ( @@ -58,11 +43,13 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={[styles.pb2, styles.ph5]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} > Connections.updatePolicyConnectionConfig( policyID, diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index ad44a517d6b9..d337309473a8 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -28,7 +28,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { const getSelectedAccountName = useMemo( () => (accountID: string) => { const selectedAccount = (bankAccounts ?? []).find((bank) => bank.id === accountID); - return selectedAccount?.name ?? ''; + return selectedAccount?.name ?? bankAccounts?.[0]?.name ?? ''; }, [bankAccounts], ); @@ -47,6 +47,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={[styles.pb2, styles.ph5]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} > ( - () => - (bankAccounts ?? []).map(({id, name}) => ({ - value: id, - text: name, - keyForList: id, - isSelected: reimbursementAccountID === id, - })), - [reimbursementAccountID, bankAccounts], - ); + const xeroSelectorOptions = useMemo(() => getXeroBankAccountsWithDefaultSelect(policy ?? undefined, reimbursementAccountID), [reimbursementAccountID, policy]); const listHeaderComponent = useMemo( () => ( @@ -62,6 +52,7 @@ function XeroBillPaymentAccountSelectorPage({policy}: WithPolicyConnectionsProps displayName={XeroBillPaymentAccountSelectorPage.displayName} sections={[{data: xeroSelectorOptions}]} listItem={RadioListItem} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} shouldBeBlocked={!syncReimbursedReports} onSelectRow={updateAccount} initiallyFocusedOptionKey={initiallyFocusedOptionKey} diff --git a/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx index ba7749fef4f2..c80e604b5c81 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; +import {getXeroBankAccountsWithDefaultSelect} from '@libs/PolicyUtils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; @@ -18,20 +19,9 @@ function XeroInvoiceAccountSelectorPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? ''; - const {bankAccounts} = policy?.connections?.xero?.data ?? {}; const {invoiceCollectionsAccountID, syncReimbursedReports} = policy?.connections?.xero?.config.sync ?? {}; - - const xeroSelectorOptions = useMemo( - () => - (bankAccounts ?? []).map(({id, name}) => ({ - value: id, - text: name, - keyForList: id, - isSelected: invoiceCollectionsAccountID === id, - })), - [invoiceCollectionsAccountID, bankAccounts], - ); + const xeroSelectorOptions = useMemo(() => getXeroBankAccountsWithDefaultSelect(policy ?? undefined, invoiceCollectionsAccountID), [invoiceCollectionsAccountID, policy]); const listHeaderComponent = useMemo( () => ( @@ -62,6 +52,7 @@ function XeroInvoiceAccountSelectorPage({policy}: WithPolicyConnectionsProps) { displayName={XeroInvoiceAccountSelectorPage.displayName} sections={[{data: xeroSelectorOptions}]} listItem={RadioListItem} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} shouldBeBlocked={!syncReimbursedReports} onSelectRow={updateAccount} initiallyFocusedOptionKey={initiallyFocusedOptionKey} diff --git a/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx b/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx index 897ed0b37d78..508af9a89fa6 100644 --- a/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroBankAccountSelectPage.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import Navigation from '@libs/Navigation/Navigation'; +import {getXeroBankAccountsWithDefaultSelect} from '@libs/PolicyUtils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; @@ -18,20 +19,9 @@ function XeroBankAccountSelectPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? ''; - const {bankAccounts} = policy?.connections?.xero?.data ?? {}; const {nonReimbursableAccount: nonReimbursableAccountID} = policy?.connections?.xero?.config.export ?? {}; - - const xeroSelectorOptions = useMemo( - () => - (bankAccounts ?? []).map(({id, name}) => ({ - value: id, - text: name, - keyForList: id, - isSelected: nonReimbursableAccountID === id, - })), - [nonReimbursableAccountID, bankAccounts], - ); + const xeroSelectorOptions = useMemo(() => getXeroBankAccountsWithDefaultSelect(policy ?? undefined, nonReimbursableAccountID), [nonReimbursableAccountID, policy]); const listHeaderComponent = useMemo( () => ( @@ -69,6 +59,7 @@ function XeroBankAccountSelectPage({policy}: WithPolicyConnectionsProps) { headerContent={listHeaderComponent} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID))} title="workspace.xero.xeroBankAccount" + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} /> ); } diff --git a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx index fd0213ed0f89..4de19a4689b0 100644 --- a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx @@ -26,7 +26,7 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const {bankAccounts} = policy?.connections?.xero?.data ?? {}; const selectedBankAccountName = useMemo(() => { const selectedAccount = (bankAccounts ?? []).find((bank) => bank.id === exportConfiguration?.nonReimbursableAccount); - return selectedAccount?.name ?? ''; + return selectedAccount?.name ?? bankAccounts?.[0]?.name ?? ''; }, [bankAccounts, exportConfiguration?.nonReimbursableAccount]); const currentXeroOrganizationName = useMemo(() => getCurrentXeroOrganizationName(policy ?? undefined), [policy]); @@ -59,7 +59,7 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { }, { description: translate('workspace.xero.advancedConfig.purchaseBillStatusTitle'), - onPress: () => {}, + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_BILL_STATUS_SELECTOR.getRoute(policyID)), title: exportConfiguration?.billStatus?.purchase ? translate(`workspace.xero.invoiceStatus.values.${exportConfiguration.billStatus.purchase}`) : undefined, pendingAction: pendingFields?.export, errorText: errorFields?.purchase ? translate('common.genericErrorMessage') : undefined, @@ -99,6 +99,7 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={styles.pb2} titleStyle={styles.ph5} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} > {menuItems.map((menuItem) => ( { @@ -38,18 +40,26 @@ function XeroPreferredExporterSelectPage({policy}: WithPolicyConnectionsProps) { }, ]; } - return exporters?.reduce((vendors, vendor) => { - if (vendor.email) { - vendors.push({ - value: vendor.email, - text: vendor.email, - keyForList: vendor.email, - isSelected: exportConfiguration?.exporter === vendor.email, - }); + + return exporters?.reduce((options, exporter) => { + if (!exporter.email) { + return options; + } + + // Don't show guides if the current user is not a guide themselves or an Expensify employee + if (isExpensifyTeam(exporter.email) && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) { + return options; } - return vendors; + + options.push({ + value: exporter.email, + text: exporter.email, + keyForList: exporter.email, + isSelected: exportConfiguration?.exporter === exporter.email, + }); + return options; }, []); - }, [exportConfiguration, exporters, policyOwner]); + }, [exportConfiguration, exporters, policyOwner, currentUserLogin]); const selectExporter = useCallback( (row: CardListItem) => { @@ -84,6 +94,7 @@ function XeroPreferredExporterSelectPage({policy}: WithPolicyConnectionsProps) { initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID))} title="workspace.xero.preferredExporter" + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} /> ); } diff --git a/src/pages/workspace/accounting/xero/export/XeroPurchaseBillDateSelectPage.tsx b/src/pages/workspace/accounting/xero/export/XeroPurchaseBillDateSelectPage.tsx index 2060e48fe987..51ae82d79e3a 100644 --- a/src/pages/workspace/accounting/xero/export/XeroPurchaseBillDateSelectPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroPurchaseBillDateSelectPage.tsx @@ -64,6 +64,7 @@ function XeroPurchaseBillDateSelectPage({policy}: WithPolicyConnectionsProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} /> ); } diff --git a/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx b/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx new file mode 100644 index 000000000000..0fbf1ff5c285 --- /dev/null +++ b/src/pages/workspace/accounting/xero/export/XeroPurchaseBillStatusSelectorPage.tsx @@ -0,0 +1,79 @@ +import {isEmpty} from 'lodash'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function XeroPurchaseBillStatusSelectorPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? ''; + const styles = useThemeStyles(); + const {billStatus} = policy?.connections?.xero?.config?.export ?? {}; + const invoiceStatus = billStatus?.purchase; + + const data: MenuListItem[] = Object.values(CONST.XERO_CONFIG.INVOICE_STATUS).map((status) => ({ + value: status, + text: translate(`workspace.xero.invoiceStatus.values.${status}`), + keyForList: status, + isSelected: invoiceStatus === status, + })); + + const headerContent = useMemo( + () => ( + + {translate('workspace.xero.invoiceStatus.description')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + const selectPurchaseBillStatus = useCallback( + (row: MenuListItem) => { + if (isEmpty(billStatus)) { + return; + } + if (row.value !== invoiceStatus) { + Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.XERO_CONFIG.EXPORT, {billStatus: {...billStatus, purchase: row.value}}); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_BILL_STATUS_SELECTOR.getRoute(policyID)); + }, + [billStatus, invoiceStatus, policyID], + ); + + return ( + selectPurchaseBillStatus(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} + /> + ); +} + +XeroPurchaseBillStatusSelectorPage.displayName = 'XeroPurchaseBillStatusSelectorPage'; + +export default withPolicyConnections(XeroPurchaseBillStatusSelectorPage); diff --git a/src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx b/src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx index c3df12960150..92915364932a 100644 --- a/src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx +++ b/src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx @@ -30,6 +30,7 @@ function XeroChartOfAccountsPage({policy}: WithPolicyProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={[styles.pb2, styles.ph5]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} > diff --git a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx index 19ca447ed4a6..dacdb9c288a1 100644 --- a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx @@ -27,6 +27,7 @@ function XeroCustomerConfigurationPage({policy}: WithPolicyProps) { policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} contentContainerStyle={[[styles.pb2, styles.ph5]]} + connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} > 0; + const isConnectedToQbo = Boolean(policy?.connections?.quickbooksOnline); const fetchCategories = useCallback(() => { Category.openPolicyCategoriesPage(policyId); @@ -245,15 +247,16 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const getHeaderText = () => ( - {Object.keys(policy?.connections ?? {}).length > 0 ? ( + {isConnectedToAccounting ? ( {`${translate('workspace.categories.importedFromAccountingSoftware')} `} - {`${translate('workspace.accounting.qbo')} ${translate('workspace.accounting.settings')}`} + {`${translate(isConnectedToQbo ? 'workspace.accounting.qbo' : 'workspace.accounting.xero')} ${translate('workspace.accounting.settings')}`} + . ) : ( {translate('workspace.categories.subtitle')} @@ -292,7 +295,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { danger /> {isSmallScreenWidth && {getHeaderButtons()}} - {!isSmallScreenWidth && getHeaderText()} + {(!isSmallScreenWidth || shouldShowEmptyState || isLoading) && getHeaderText()} {isLoading && ( ) => { const errors: FormInputErrors = {}; const tagName = values.tagName.trim(); - const {tags} = PolicyUtils.getTagList(policyTags, 0); + const {tags} = PolicyUtils.getTagList(policyTags, route.params.orderWeight); if (!ValidationUtils.isRequiredFulfilled(tagName)) { errors.tagName = 'workspace.tags.tagRequiredError'; } else if (tags?.[tagName] && currentTagName !== tagName) { @@ -50,7 +50,7 @@ function EditTagPage({route, policyTags}: EditTagPageProps) { return errors; }, - [currentTagName, policyTags], + [route.params.orderWeight, currentTagName, policyTags], ); const editTag = useCallback( diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 646b0d20e9fa..a4f6940814dd 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -38,7 +38,7 @@ type TagSettingsPageProps = TagSettingsPageOnyxProps & StackScreenProps PolicyUtils.getTagList(policyTags, 0), [policyTags]); + const policyTag = useMemo(() => PolicyUtils.getTagList(policyTags, route.params.orderWeight), [policyTags, route.params.orderWeight]); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`); const {windowWidth} = useWindowDimensions(); @@ -66,11 +66,11 @@ function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps) }; const updateWorkspaceTagEnabled = (value: boolean) => { - setWorkspaceTagEnabled(route.params.policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}); + setWorkspaceTagEnabled(route.params.policyID, {[currentPolicyTag.name]: {name: currentPolicyTag.name, enabled: value}}, policyTag.orderWeight); }; const navigateToEditTag = () => { - Navigation.navigate(ROUTES.WORKSPACE_TAG_EDIT.getRoute(route.params.policyID, currentPolicyTag.name)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_EDIT.getRoute(route.params.policyID, route.params.orderWeight, currentPolicyTag.name)); }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 84640f63b063..c31dc223494a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -52,6 +52,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const {environmentURL} = useEnvironment(); const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; + const isConnectedToQbo = Boolean(policy?.connections?.quickbooksOnline); const [policyTagLists, isMultiLevelTags] = useMemo(() => [PolicyUtils.getTagLists(policyTags), PolicyUtils.isMultiLevelTags(policyTags)], [policyTags]); const canSelectMultiple = !isMultiLevelTags; @@ -155,11 +156,11 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const navigateToTagSettings = (tag: TagListItem) => { - if (tag.orderWeight != null) { + if (tag.orderWeight !== undefined) { Navigation.navigate(ROUTES.WORKSPACE_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight)); return; } - Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, tag.value)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, 0, tag.value)); }; const selectedTagsArray = Object.keys(selectedTags).filter((key) => selectedTags[key]); @@ -239,7 +240,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DISABLE, onSelected: () => { setSelectedTags({}); - Tag.setWorkspaceTagEnabled(policyID, tagsToDisable); + Tag.setWorkspaceTagEnabled(policyID, tagsToDisable, 0); }, }); } @@ -251,7 +252,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags({}); - Tag.setWorkspaceTagEnabled(policyID, tagsToEnable); + Tag.setWorkspaceTagEnabled(policyID, tagsToEnable, 0); }, }); } @@ -279,8 +280,9 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { style={[styles.textNormal, styles.link]} href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`} > - {`${translate('workspace.accounting.qbo')} ${translate('workspace.accounting.settings')}`} + {`${translate(isConnectedToQbo ? 'workspace.accounting.qbo' : 'workspace.accounting.xero')} ${translate('workspace.accounting.settings')}`} + . ) : ( {translate('workspace.tags.subtitle')} @@ -319,7 +321,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { cancelText={translate('common.cancel')} danger /> - {!isSmallScreenWidth && getHeaderText()} + {(!isSmallScreenWidth || tagList.length === 0 || isLoading) && getHeaderText()} {isLoading && ( { - Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, tag.value)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, route.params.orderWeight, tag.value)); }; const selectedTagsArray = Object.keys(selectedTags).filter((key) => selectedTags[key]); @@ -175,7 +175,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DISABLE, onSelected: () => { setSelectedTags({}); - Tag.setWorkspaceTagEnabled(policyID, tagsToDisable); + Tag.setWorkspaceTagEnabled(policyID, tagsToDisable, route.params.orderWeight); }, }); } @@ -187,7 +187,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags({}); - Tag.setWorkspaceTagEnabled(policyID, tagsToEnable); + Tag.setWorkspaceTagEnabled(policyID, tagsToEnable, route.params.orderWeight); }, }); } diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 8ccff7b4e126..c7f3c50489e7 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -86,7 +86,7 @@ function WorkspaceEditTaxPage({ ([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const defaultExternalID = policy?.taxRates?.defaultExternalID; const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; const isFocused = useIsFocused(); + const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; + const isConnectedToQbo = Boolean(policy?.connections?.quickbooksOnline); + const fetchTaxes = useCallback(() => { openPolicyTaxesPage(policyID); }, [policyID]); @@ -171,14 +177,15 @@ function WorkspaceTaxesPage({ const dropdownMenuOptions = useMemo(() => { const isMultiple = selectedTaxesIDs.length > 1; - const options: Array> = [ - { + const options: Array> = []; + if (!PolicyUtils.hasAccountingConnections(policy)) { + options.push({ icon: Expensicons.Trashcan, text: isMultiple ? translate('workspace.taxes.actions.deleteMultiple') : translate('workspace.taxes.actions.delete'), value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DELETE, onSelected: () => setIsDeleteModalVisible(true), - }, - ]; + }); + } // `Disable rates` when at least one enabled rate is selected. if (selectedTaxesIDs.some((taxID) => !policy?.taxRates?.taxes[taxID]?.isDisabled)) { @@ -200,18 +207,20 @@ function WorkspaceTaxesPage({ }); } return options; - }, [policy?.taxRates?.taxes, selectedTaxesIDs, toggleTaxes, translate]); + }, [policy, selectedTaxesIDs, toggleTaxes, translate]); const headerButtons = !selectedTaxesIDs.length ? ( - -