diff --git a/.eslintrc.js b/.eslintrc.js index d24ea9766e19..9f839e45ce75 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,11 @@ const restrictedImportPaths = [ name: 'date-fns/locale', message: "Do not import 'date-fns/locale' directly. Please use the submodule import instead, like 'date-fns/locale/en-GB'.", }, + { + name: 'expensify-common', + importNames: ['Device'], + message: "Do not import Device directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.", + }, ]; const restrictedImportPatterns = [ diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 5ceb760a452b..20bf0d257a9b 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -97,7 +97,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: android-sourcemap - path: android/app/build/generated/sourcemaps/react/release/*.map + path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map - name: Upload Android version to GitHub artifacts if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 0a0a0ee04e90..9e0fcd2e0bc2 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 1001048301 - versionName "1.4.83-1" + versionCode 1001048402 + versionName "1.4.84-2" // 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/android/build.gradle b/android/build.gradle index 52c998998ba0..9fc585ab9f05 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,7 +29,7 @@ buildscript { classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.firebase:perf-plugin:1.4.1") // Fullstory integration - classpath ("com.fullstory:gradle-plugin-local:1.47.0") + classpath ("com.fullstory:gradle-plugin-local:1.49.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/babel.config.js b/babel.config.js index 060bc0313950..8eb0d102db01 100644 --- a/babel.config.js +++ b/babel.config.js @@ -10,8 +10,13 @@ const defaultPlugins = [ '@babel/transform-runtime', '@babel/plugin-proposal-class-properties', - // This will serve to map the classes correctly in FullStory - '@fullstory/babel-plugin-annotate-react', + [ + '@fullstory/babel-plugin-annotate-react', + { + 'react-native-web': true, + native: true, + }, + ], // We use `transform-class-properties` for transforming ReactNative libraries and do not use it for our own // source code transformation as we do not use class property assignment. @@ -45,7 +50,6 @@ const metro = { '@fullstory/babel-plugin-annotate-react', { native: true, - setFSTagName: true, }, ], diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index f27e421057ef..6af3a82c2ff6 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -48,7 +48,6 @@ - [Forwarding refs](#forwarding-refs) - [Hooks and HOCs](#hooks-and-hocs) - [Stateless components vs Pure Components vs Class based components vs Render Props](#stateless-components-vs-pure-components-vs-class-based-components-vs-render-props---when-to-use-what) - - [Composition](#composition) - [Use Refs Appropriately](#use-refs-appropriately) - [Are we allowed to use [insert brand new React feature]?](#are-we-allowed-to-use-insert-brand-new-react-feature-why-or-why-not) - [React Hooks: Frequently Asked Questions](#react-hooks-frequently-asked-questions) @@ -1094,51 +1093,6 @@ Class components are DEPRECATED. Use function components and React hooks. [https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) -### Composition - -Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. - -> Why? `compose` function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use [hooks instead of HOCs](#hooks-instead-of-hocs) whenever possible to minimize or even remove the need of HOCs in the component. - -From React's documentation - ->Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. ->If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it. - - ```ts - // BAD - export default compose( - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), - )(Component); - - // GOOD - export default withCurrentUserPersonalDetails( - withReportOrNotFound()( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component), - ), - ); - - // GOOD - alternative to HOC nesting - const ComponentWithOnyx = withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component); - const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); - export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); - ``` - -**Note:** If you find that none of these approaches work for you, please ask an Expensify engineer for guidance via Slack or GitHub. - ### Use Refs Appropriately React's documentation explains refs in [detail](https://reactjs.org/docs/refs-and-the-dom.html). It's important to understand when to use them and how to use them to avoid bugs and hard to maintain code. diff --git a/ios/.xcode.env b/ios/.xcode.env index 3d5782c71568..0ef3f59d606a 100644 --- a/ios/.xcode.env +++ b/ios/.xcode.env @@ -9,3 +9,8 @@ # For example, to use nvm with brew, add the following line # . "$(brew --prefix nvm)/nvm.sh" --no-use export NODE_BINARY=$(command -v node) + +# Provide a sourcemap path so that in release builds a source map file will be +# created at the specified location. +# (RN's default behaviour on iOS is to skip creating a sourcemap file): +export SOURCEMAP_FILE="$(pwd)/../main.jsbundle.map"; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ed5fdc1b6a77..69ef5f90dd5c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.83 + 1.4.84 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.83.1 + 1.4.84.2 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c52d60fe34ed..534bd3625870 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.83 + 1.4.84 CFBundleSignature ???? CFBundleVersion - 1.4.83.1 + 1.4.84.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index ca21f2331ad3..5f083aea5ba8 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.83 + 1.4.84 CFBundleVersion - 1.4.83.1 + 1.4.84.2 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index d72086d4c07b..6330bb3d8d52 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -118,4 +118,4 @@ target 'NotificationServiceExtension' do pod 'AirshipServiceExtension' end -pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz' \ No newline at end of file +pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz' \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aca46d6b18ed..4857cab9bb24 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,7 +138,7 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (6.2.1) - - FullStory (1.48.0) + - FullStory (1.49.0) - fullstory_react-native (1.4.2): - FullStory (~> 1.14) - glog @@ -1852,7 +1852,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.83): + - RNLiveMarkdown (0.1.85): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1870,9 +1870,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.83) + - RNLiveMarkdown/common (= 0.1.85) - Yoga - - RNLiveMarkdown/common (0.1.83): + - RNLiveMarkdown/common (0.1.85): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2098,7 +2098,7 @@ DEPENDENCIES: - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz\"}`)" + - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz\"}`)" - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) @@ -2263,7 +2263,7 @@ EXTERNAL SOURCES: FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" FullStory: - :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz fullstory_react-native: :path: "../node_modules/@fullstory/react-native" glog: @@ -2458,7 +2458,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: FullStory: - :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz SPEC CHECKSUMS: Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 @@ -2485,7 +2485,7 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - FullStory: 097347c823c21c655ca25fd8d5e6355a9326ec54 + FullStory: c95f74445f871bc344cdc4a4e4ece61b5554e55d fullstory_react-native: 6cba8a2c054374a24a44dc4310407d9435459cae glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 @@ -2589,7 +2589,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 88030b7d9a31f5f6e67743df48ad952d64513b4a + RNLiveMarkdown: fff70dc755ed8199a449f61e76cbadec7cd20440 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 @@ -2606,8 +2606,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 -PODFILE CHECKSUM: 66a5c97ae1059e4da1993a4ad95abe5d819f555b +PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14 COCOAPODS: 1.13.0 diff --git a/package-lock.json b/package-lock.json index 7ba03f0417df..e1c8cee95d34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,26 @@ { "name": "new.expensify", - "version": "1.4.83-1", + "version": "1.4.84-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.83-1", + "version": "1.4.84-2", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.83", + "@expensify/react-native-live-markdown": "0.1.85", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-annotate-react": "github:fullstorydev/fullstory-babel-plugin-annotate-react#ryanwang/react-native-web-demo", "@fullstory/babel-plugin-react-native": "^1.2.1", "@fullstory/browser": "^2.0.3", "@fullstory/react-native": "^1.4.2", @@ -59,7 +60,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "^2.0.10", + "expensify-common": "^2.0.12", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -103,7 +104,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.48", + "react-native-onyx": "2.0.49", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -3560,9 +3561,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.83", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.83.tgz", - "integrity": "sha512-xGn1P9FbFVueEF8BNKJJ4dQb0wPtsAvrrxND9pwVQT35ZL5cu1KZ4o6nzCqtesISPRB8Dw9Zx0ftIZy2uCQyzA==", + "version": "0.1.85", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.85.tgz", + "integrity": "sha512-jeP4JBzN34pGSpjHKM7Zj3d0cqcKbID3//WrqC+SI7SK/1iJT4SdhZptVCxUg+Dcxq5XwzYIhdnhTNimeya0Fg==", "workspaces": [ "parser", "example", @@ -5587,8 +5588,7 @@ }, "node_modules/@fullstory/babel-plugin-annotate-react": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-annotate-react/-/babel-plugin-annotate-react-2.3.0.tgz", - "integrity": "sha512-gYLUL6Tu0exbvTIhK9nSCaztmqBlQAm07Fvtl/nKTc+lxwFkcX9vR8RrdTbyjJZKbPaA5EMlExQ6GeLCXkfm5g==" + "resolved": "git+ssh://git@github.com/fullstorydev/fullstory-babel-plugin-annotate-react.git#25c26dadb644d5355e381a4ea4ca1cd05af4a8f6" }, "node_modules/@fullstory/babel-plugin-react-native": { "version": "1.2.1", @@ -20769,9 +20769,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.10.tgz", - "integrity": "sha512-+8LCtnR+VxmCjKKkfeR6XGAhVxvwZtQAw3386c1EDGNK1C0bvz3I1kLVMFbulSeibZv6/G33aO6SiW/kwum6Nw==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.12.tgz", + "integrity": "sha512-idIm9mAGDX1qyfA2Ky/1ZJZVMbGydtpIdwl6zl1Yc7FO11IGvAYLh2cH9VsQk98AapRTiJu7QUaRWLLGDaHIcQ==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -31939,9 +31939,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.48.tgz", - "integrity": "sha512-qJQTWMzhLD7zy5/9vBZJSlb3//fYVx3obTdsw1tXZDVOZXUcBmd6evA2tzGe5KT8H2sIbvFR1UyvwE03oOqYYg==", + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.49.tgz", + "integrity": "sha512-cmFc7OZcVRuegb86c0tOCa8GGAXIraOfnLgtSxnNOA7DV/PMrbSetyFry2tzEDnGwORsgVWaunV78Jw1Em5rwA==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 16b77e6e754c..dd594ddbc8c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.83-1", + "version": "1.4.84-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -65,13 +65,14 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.83", + "@expensify/react-native-live-markdown": "0.1.85", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-annotate-react": "github:fullstorydev/fullstory-babel-plugin-annotate-react#ryanwang/react-native-web-demo", "@fullstory/babel-plugin-react-native": "^1.2.1", "@fullstory/browser": "^2.0.3", "@fullstory/react-native": "^1.4.2", @@ -111,7 +112,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "^2.0.10", + "expensify-common": "^2.0.12", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -155,7 +156,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.48", + "react-native-onyx": "2.0.49", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", diff --git a/src/CONST.ts b/src/CONST.ts index 6a936bc97087..9311816c38a2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -523,6 +523,16 @@ const CONST = { shortcutKey: 'Tab', modifiers: [], }, + DEBUG: { + descriptionKey: 'openDebug', + shortcutKey: 'D', + modifiers: ['CTRL'], + trigger: { + DEFAULT: {input: 'd', modifierFlags: keyModifierControl}, + [PLATFORM_OS_MACOS]: {input: 'd', modifierFlags: keyModifierCommand}, + [PLATFORM_IOS]: {input: 'd', modifierFlags: keyModifierCommand}, + }, + }, }, KEYBOARD_SHORTCUTS_TYPES: { NAVIGATION_SHORTCUT: KEYBOARD_SHORTCUT_NAVIGATION_TYPE, @@ -3309,6 +3319,11 @@ const CONST = { CONCIERGE_TRAVEL_URL: 'https://community.expensify.com/discussion/7066/introducing-concierge-travel', BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', + TRAVEL_DOT_URL: 'https://travel.expensify.com', + STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', + TRIP_ID_PATH: (tripID: string) => `trips/${tripID}`, + SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', + STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { ALL: 'all', ACTIVE: 'active', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index eb3b439ea1ff..c1fdd68951fa 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -102,7 +102,10 @@ const ROUTES = { SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_SUBSCRIPTION: 'settings/subscription', - SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size', + SETTINGS_SUBSCRIPTION_SIZE: { + route: 'settings/subscription/subscription-size', + getRoute: (canChangeSize: 0 | 1) => `settings/subscription/subscription-size?canChangeSize=${canChangeSize}` as const, + }, SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 61e9a2d1860a..055026573864 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -138,35 +138,35 @@ function PaymentCardForm({ const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); const [currency, setCurrency] = useState(CONST.CURRENCY.USD); - const validate = (formValues: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS); + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS); - if (formValues.nameOnCard && !ValidationUtils.isValidLegalName(formValues.nameOnCard)) { - errors.nameOnCard = label.error.nameOnCard; + if (values.nameOnCard && !ValidationUtils.isValidLegalName(values.nameOnCard)) { + errors.nameOnCard = translate('addDebitCardPage.error.invalidName'); } - if (formValues.cardNumber && !ValidationUtils.isValidDebitCard(formValues.cardNumber.replace(/ /g, ''))) { - errors.cardNumber = label.error.cardNumber; + if (values.cardNumber && !ValidationUtils.isValidDebitCard(values.cardNumber.replace(/ /g, ''))) { + errors.cardNumber = translate('addDebitCardPage.error.debitCardNumber'); } - if (formValues.expirationDate && !ValidationUtils.isValidExpirationDate(formValues.expirationDate)) { - errors.expirationDate = label.error.expirationDate; + if (values.expirationDate && !ValidationUtils.isValidExpirationDate(values.expirationDate)) { + errors.expirationDate = translate('addDebitCardPage.error.expirationDate'); } - if (formValues.securityCode && !ValidationUtils.isValidSecurityCode(formValues.securityCode)) { - errors.securityCode = label.error.securityCode; + if (values.securityCode && !ValidationUtils.isValidSecurityCode(values.securityCode)) { + errors.securityCode = translate('addDebitCardPage.error.securityCode'); } - if (formValues.addressStreet && !ValidationUtils.isValidAddress(formValues.addressStreet)) { - errors.addressStreet = label.error.addressStreet; + if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { + errors.addressStreet = translate('addDebitCardPage.error.addressStreet'); } - if (formValues.addressZipCode && !ValidationUtils.isValidZipCode(formValues.addressZipCode)) { - errors.addressZipCode = label.error.addressZipCode; + if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { + errors.addressZipCode = translate('addDebitCardPage.error.addressZipCode'); } - if (!formValues.acceptTerms) { - errors.acceptTerms = 'common.error.acceptTerms'; + if (!values.acceptTerms) { + errors.acceptTerms = translate('common.error.acceptTerms'); } return errors; diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx index a1430615e37b..a112b36705c3 100644 --- a/src/components/AddPlaidBankAccount.tsx +++ b/src/components/AddPlaidBankAccount.tsx @@ -172,6 +172,7 @@ function AddPlaidBankAccount({ })); const {icon, iconSize, iconStyles} = getBankIcon({styles}); const plaidErrors = plaidData?.errors; + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const plaidDataErrorMessage = !isEmptyObject(plaidErrors) ? (Object.values(plaidErrors)[0] as string) : ''; const bankName = plaidData?.bankName; diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 296ecce7d092..27822fb390a6 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -3,7 +3,6 @@ import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import type {MaybePhraseKey} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -76,7 +75,7 @@ function AddressForm({ const zipSampleFormat = (country && (CONST.COUNTRY_ZIP_REGEX_DATA[country] as CountryZipRegex)?.samples) ?? ''; - const zipFormat: MaybePhraseKey = ['common.zipCodeExampleFormat', {zipSampleFormat}]; + const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); const isUSAForm = country === CONST.COUNTRY.US; @@ -87,50 +86,53 @@ function AddressForm({ * @returns - An object containing the errors for each inputID */ - const validator = useCallback((values: FormOnyxValues): Errors => { - const errors: Errors & { - zipPostCode?: string | string[]; - } = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const; - - // Check "State" dropdown is a valid state if selected Country is USA - if (values.country === CONST.COUNTRY.US && !values.state) { - errors.state = 'common.error.fieldRequired'; - } - - // Add "Field required" errors if any required field is empty - requiredFields.forEach((fieldKey) => { - const fieldValue = values[fieldKey] ?? ''; - if (ValidationUtils.isRequiredFulfilled(fieldValue)) { - return; + const validator = useCallback( + (values: FormOnyxValues): Errors => { + const errors: Errors & { + zipPostCode?: string | string[]; + } = {}; + const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const; + + // Check "State" dropdown is a valid state if selected Country is USA + if (values.country === CONST.COUNTRY.US && !values.state) { + errors.state = translate('common.error.fieldRequired'); } - errors[fieldKey] = 'common.error.fieldRequired'; - }); + // Add "Field required" errors if any required field is empty + requiredFields.forEach((fieldKey) => { + const fieldValue = values[fieldKey] ?? ''; + if (ValidationUtils.isRequiredFulfilled(fieldValue)) { + return; + } + + errors[fieldKey] = translate('common.error.fieldRequired'); + }); - // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; + // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object + const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex; - // The postal code system might not exist for a country, so no regex either for them. - const countrySpecificZipRegex = countryRegexDetails?.regex; - const countryZipFormat = countryRegexDetails?.samples ?? ''; + // The postal code system might not exist for a country, so no regex either for them. + const countrySpecificZipRegex = countryRegexDetails?.regex; + const countryZipFormat = countryRegexDetails?.samples ?? ''; - ErrorUtils.addErrorMessage(errors, 'firstName', 'bankAccount.error.firstName'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('bankAccount.error.firstName')); - if (countrySpecificZipRegex) { - if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { - if (ValidationUtils.isRequiredFulfilled(values.zipPostCode?.trim())) { - errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', countryZipFormat]; - } else { - errors.zipPostCode = 'common.error.fieldRequired'; + if (countrySpecificZipRegex) { + if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) { + if (ValidationUtils.isRequiredFulfilled(values.zipPostCode?.trim())) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat); + } else { + errors.zipPostCode = translate('common.error.fieldRequired'); + } } + } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { + errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat'); } - } else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) { - errors.zipPostCode = 'privatePersonalDetails.error.incorrectZipFormat'; - } - return errors; - }, []); + return errors; + }, + [translate], + ); return ( void; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Hint text to display */ hint?: string; diff --git a/src/components/AmountPicker/types.ts b/src/components/AmountPicker/types.ts index f7025685d840..5069893f8186 100644 --- a/src/components/AmountPicker/types.ts +++ b/src/components/AmountPicker/types.ts @@ -1,6 +1,5 @@ import type {AmountFormProps} from '@components/AmountForm'; import type {MenuItemBaseProps} from '@components/MenuItem'; -import type {MaybePhraseKey} from '@libs/Localize'; type AmountSelectorModalProps = { /** Whether the modal is visible */ @@ -24,7 +23,7 @@ type AmountPickerProps = { title?: string | ((value?: string) => string); /** Form Error description */ - errorText?: MaybePhraseKey; + errorText?: string; /** Callback to call when the input changes */ onInputChange?: (value: string | undefined) => void; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index c0d89f4acf1e..2fbe69a120a0 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -45,12 +45,12 @@ type AvatarProps = { /** Used to locate fallback icon in end-to-end tests. */ fallbackIconTestID?: string; - /** Denotes whether it is an avatar or a workspace avatar */ - type?: AvatarType; - /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; + /** Denotes whether it is an avatar or a workspace avatar */ + type: AvatarType; + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ avatarID?: number | string; }; @@ -64,7 +64,7 @@ function Avatar({ fill, fallbackIcon = Expensicons.FallbackAvatar, fallbackIconTestID = '', - type = CONST.ICON_TYPE_AVATAR, + type, name = '', avatarID, }: AvatarProps) { @@ -80,9 +80,9 @@ function Avatar({ }, [originalSource]); const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; + const userAccountID = isWorkspace ? undefined : (avatarID as number); - // If it's user avatar then accountID will be a number - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, avatarID as number); + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, userAccountID); const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index f024a1239f4e..bf8fe93b8b21 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -41,6 +41,7 @@ function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon source={UserUtils.getSmallSizeAvatar(source, accountID)} fallbackIcon={fallbackIcon} avatarID={accountID} + type={CONST.ICON_TYPE_AVATAR} /> diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index dd169576186e..db62aa9e1441 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -3,7 +3,6 @@ import React, {useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; @@ -41,7 +40,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { style?: StyleProp; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; diff --git a/src/components/ClientSideLoggingToolMenu/index.android.tsx b/src/components/ClientSideLoggingToolMenu/index.android.tsx index aa1bc215b719..298299e37fb9 100644 --- a/src/components/ClientSideLoggingToolMenu/index.android.tsx +++ b/src/components/ClientSideLoggingToolMenu/index.android.tsx @@ -1,47 +1,10 @@ -import React, {useState} from 'react'; -import RNFetchBlob from 'react-native-blob-util'; -import Share from 'react-native-share'; -import type {Log} from '@libs/Console'; -import localFileCreate from '@libs/localFileCreate'; -import CONST from '@src/CONST'; -import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu'; - +/** + * Since client-side logging is currently supported on web and desktop natively right now, + * this menu will be hidden in iOS and Android. + * See comment here: https://github.com/Expensify/App/issues/43256#issuecomment-2154610196 + */ function ClientSideLoggingToolMenu() { - const [file, setFile] = useState<{path: string; newFileName: string; size: number}>(); - - const createAndSaveFile = (logs: Log[]) => { - localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => { - RNFetchBlob.MediaCollection.copyToMediaStore( - { - name: localFile.newFileName, - parentFolder: '', - mimeType: 'text/plain', - }, - 'Download', - localFile.path, - ); - setFile(localFile); - }); - }; - - const shareLogs = () => { - if (!file) { - return; - } - Share.open({ - url: `file://${file.path}`, - }); - }; - - return ( - setFile(undefined)} - onDisableLogging={createAndSaveFile} - onShareLogs={shareLogs} - displayPath={`${CONST.DOWNLOADS_PATH}/${file?.newFileName ?? ''}`} - /> - ); + return null; } ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu'; diff --git a/src/components/ClientSideLoggingToolMenu/index.ios.tsx b/src/components/ClientSideLoggingToolMenu/index.ios.tsx index 78ffccf612a2..298299e37fb9 100644 --- a/src/components/ClientSideLoggingToolMenu/index.ios.tsx +++ b/src/components/ClientSideLoggingToolMenu/index.ios.tsx @@ -1,40 +1,10 @@ -import React, {useState} from 'react'; -import Share from 'react-native-share'; -import useEnvironment from '@hooks/useEnvironment'; -import type {Log} from '@libs/Console'; -import getDownloadFolderPathSuffixForIOS from '@libs/getDownloadFolderPathSuffixForIOS'; -import localFileCreate from '@libs/localFileCreate'; -import CONST from '@src/CONST'; -import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu'; - +/** + * Since client-side logging is currently supported on web and desktop natively right now, + * this menu will be hidden in iOS and Android. + * See comment here: https://github.com/Expensify/App/issues/43256#issuecomment-2154610196 + */ function ClientSideLoggingToolMenu() { - const [file, setFile] = useState<{path: string; newFileName: string; size: number}>(); - const {environment} = useEnvironment(); - - const createFile = (logs: Log[]) => { - localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => { - setFile(localFile); - }); - }; - - const shareLogs = () => { - if (!file) { - return; - } - Share.open({ - url: `file://${file.path}`, - }); - }; - - return ( - setFile(undefined)} - onDisableLogging={createFile} - onShareLogs={shareLogs} - displayPath={`${CONST.NEW_EXPENSIFY_PATH}${getDownloadFolderPathSuffixForIOS(environment)}/${file?.newFileName ?? ''}`} - /> - ); + return null; } ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu'; diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 002c0c6d4b0a..62fdc85687e1 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -4,7 +4,6 @@ import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -13,7 +12,7 @@ import MenuItemWithTopDescription from './MenuItemWithTopDescription'; type CountrySelectorProps = { /** Form error text. e.g when no country is selected */ - errorText?: MaybePhraseKey; + errorText?: string; /** Callback called when the country changes. */ onInputChange?: (value?: string) => void; diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 524c8a3903e0..356fbd3726a3 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -114,11 +114,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack [prevIsRootStatusBarEnabled, isRootStatusBarEnabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], ); - useEffect(() => { - updateStatusBarAppearance({backgroundColor: theme.appBG}); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want this to run on first render - }, []); - useEffect(() => { didForceUpdateStatusBarRef.current = false; }, [isRootStatusBarEnabled]); diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index 3f72bbf429aa..564d2eeb8c75 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -7,7 +7,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isReceiptError} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; -import type {MaybePhraseKey} from '@libs/Localize'; import * as Localize from '@libs/Localize'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import Icon from './Icon'; @@ -23,7 +22,7 @@ type DotIndicatorMessageProps = { * timestamp: 'message', * } */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; @@ -45,12 +44,12 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica } // Fetch the keys, sort them, and map through each key to get the corresponding message - const sortedMessages: Array = Object.keys(messages) + const sortedMessages: Array = Object.keys(messages) .sort() - .map((key) => messages[key]); - + .map((key) => messages[key]) + .filter((message): message is string | ReceiptError => message !== null); // Removing duplicates using Set and transforming the result into an array - const uniqueMessages: Array = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); + const uniqueMessages: Array = [...new Set(sortedMessages)].map((message) => message); const isErrorMessage = type === 'error'; diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx index 161e3f1b7f84..b06044404ee2 100644 --- a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx @@ -10,7 +10,7 @@ function FocusTrapForModal({children, active}: FocusTrapForModalProps) { focusTrapOptions={{ trapStack: sharedTrapStack, allowOutsideClick: true, - fallbackFocus: document.body, + initialFocus: false, }} > {children} diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index 2d0c51edbba9..5f10a7293457 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -13,6 +13,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.SETTINGS.WALLET.ROOT, SCREENS.SETTINGS.ABOUT, SCREENS.SETTINGS.WORKSPACES, + SCREENS.SETTINGS.SUBSCRIPTION.ROOT, SCREENS.WORKSPACE.INITIAL, SCREENS.WORKSPACE.PROFILE, SCREENS.WORKSPACE.CARD, diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 3d20f910dca0..16ecc3805d2d 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -253,10 +253,10 @@ function FormProvider( const errorFields = formState?.errorFields?.[inputID] ?? {}; const fieldErrorMessage = - (Object.keys(errorFields) + Object.keys(errorFields) .sort() .map((key) => errorFields[key]) - .at(-1) as string) ?? ''; + .at(-1) ?? ''; const inputRef = inputProps.ref; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 331f1c943b30..9aa8bc921164 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -18,7 +18,6 @@ import type StateSelector from '@components/StateSelector'; import type TextInput from '@components/TextInput'; import type TextPicker from '@components/TextPicker'; import type ValuePicker from '@components/ValuePicker'; -import type {MaybePhraseKey} from '@libs/Localize'; import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker'; import type {Country} from '@src/CONST'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; @@ -139,7 +138,7 @@ type FormRef = { type InputRefs = Record>; -type FormInputErrors = Partial, MaybePhraseKey>>; +type FormInputErrors = Partial, string | undefined>>; export type { FormProps, diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 137012478549..cd177a1d77a3 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -2,13 +2,12 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; type FormAlertWithSubmitButtonProps = { /** Error message to display above button */ - message?: MaybePhraseKey; + message?: string; /** Whether the button is disabled */ isDisabled?: boolean; diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index d8b379208a29..525182070095 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -4,7 +4,6 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import type Network from '@src/types/onyx/Network'; import FormHelpMessage from './FormHelpMessage'; import {withNetwork} from './OnyxProvider'; @@ -29,7 +28,7 @@ type FormAlertWrapperProps = { isMessageHtml?: boolean; /** Error message to display above button */ - message?: MaybePhraseKey; + message?: string; /** Props to detect online status */ network: Network; diff --git a/src/components/FormHelpMessage.tsx b/src/components/FormHelpMessage.tsx index 4f1d784788bf..01a5a1eaf3a8 100644 --- a/src/components/FormHelpMessage.tsx +++ b/src/components/FormHelpMessage.tsx @@ -4,14 +4,13 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Localize from '@libs/Localize'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; type FormHelpMessageProps = { /** Error or hint text. Ignored when children is not empty */ - message?: Localize.MaybePhraseKey; + message?: string; /** Children to render next to dot indicator */ children?: React.ReactNode; @@ -33,8 +32,6 @@ function FormHelpMessage({message = '', children, isError = true, style, shouldS return null; } - const translatedMessage = Localize.translateIfPhraseKey(message); - return ( {isError && shouldShowRedDotIndicator && ( @@ -44,7 +41,7 @@ function FormHelpMessage({message = '', children, isError = true, style, shouldS /> )} - {children ?? {translatedMessage}} + {children ?? {message}} ); diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index b787a28a3c17..60d5bf7034cc 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -10,7 +10,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; -import Text from '@components/Text'; +import TextBlock from '@components/TextBlock'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; @@ -61,36 +61,52 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const emptyLHNSubtitle = useMemo( () => ( - - - {translate('common.emptyLHN.subtitleText1')} - - {translate('common.emptyLHN.subtitleText2')} - - {translate('common.emptyLHN.subtitleText3')} - + + + + + + ), - [theme, styles.alignItemsCenter, styles.textAlignCenter, translate], + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.mh1, + theme.icon, + theme.textSupporting, + styles.textNormal, + translate, + ], ); /** diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index fde4fe8a4dac..49c5ee0568db 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,4 @@ import {useFocusEffect} from '@react-navigation/native'; -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; @@ -20,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; +import {parseHtmlToText} from '@libs/OnyxAwareParser'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -29,8 +29,6 @@ import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OptionRowLHNProps} from './types'; -const parser = new ExpensiMark(); - function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style, onLayout = () => {}, hasDraftComment}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -184,17 +182,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti > - {(optionItem.icons?.length ?? 0) > 0 && + {!!optionItem.icons?.length && (optionItem.shouldShowSubscript ? ( ) : ( {}, opti numberOfLines={1} accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')} > - {parser.htmlToText(optionItem.alternateText)} + {parseHtmlToText(optionItem.alternateText)} ) : null} diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index eb7d9324d2ab..e0e30d14d2a2 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -2,7 +2,6 @@ import React, {createContext, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import * as LocaleDigitUtils from '@libs/LocaleDigitUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -125,18 +124,17 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {} return {children}; } -const Provider = compose( +const Provider = withCurrentUserPersonalDetails( withOnyx({ preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, selector: (preferredLocale) => preferredLocale, }, - }), - withCurrentUserPersonalDetails, -)(LocaleContextProvider); + })(LocaleContextProvider), +); Provider.displayName = 'withOnyx(LocaleContextProvider)'; -export {Provider as LocaleContextProvider, LocaleContext}; +export {LocaleContext, Provider as LocaleContextProvider}; -export type {LocaleContextProps, Locale}; +export type {Locale, LocaleContextProps}; diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index deff56a534ee..6239243cb5ab 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -7,7 +7,6 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; -import type {MaybePhraseKey} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; @@ -33,7 +32,7 @@ type MagicCodeInputProps = { shouldDelayFocus?: boolean; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Specifies autocomplete hints for the system, so it can provide autofill */ autoComplete: AutoCompleteVariant; diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 8168cef99cd3..06128c0d06b7 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -10,7 +10,6 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as UserLocation from '@libs/actions/UserLocation'; -import compose from '@libs/compose'; import getCurrentPosition from '@libs/getCurrentPosition'; import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; @@ -265,11 +264,8 @@ const MapView = forwardRef( }, ); -export default compose( - withOnyx({ - userLocation: { - key: ONYXKEYS.USER_LOCATION, - }, - }), - memo, -)(MapView); +export default withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, +})(memo(MapView)); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 6a0ca0c9f5e3..06edd921440c 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -14,7 +14,6 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; -import type {MaybePhraseKey} from '@libs/Localize'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -154,13 +153,13 @@ type MenuItemBaseProps = { shouldShowDescriptionOnTop?: boolean; /** Error to display at the bottom of the component */ - errorText?: MaybePhraseKey; + errorText?: string; /** Any additional styles to pass to error text. */ errorTextStyle?: StyleProp; /** Hint to display at the bottom of the component */ - hintText?: MaybePhraseKey; + hintText?: string; /** Should the error text red dot indicator be shown */ shouldShowRedDotIndicator?: boolean; @@ -565,6 +564,7 @@ function MenuItem( avatarID={avatarID} fallbackIcon={fallbackIcon} size={avatarSize} + type={CONST.ICON_TYPE_AVATAR} /> )} diff --git a/src/components/MessagesRow.tsx b/src/components/MessagesRow.tsx index 7c764ec94fcd..6a5be5db07bc 100644 --- a/src/components/MessagesRow.tsx +++ b/src/components/MessagesRow.tsx @@ -4,7 +4,6 @@ import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -16,7 +15,7 @@ import Tooltip from './Tooltip'; type MessagesRowProps = { /** The messages to display */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 5a66e2be5238..90952157f179 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -64,6 +65,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const {windowWidth} = useWindowDimensions(); const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); @@ -234,7 +236,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldDisableApproveButton={shouldDisableApproveButton} style={[styles.pv2]} formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''} - isDisabled={!canAllowSettlement} + isDisabled={isOffline && !canAllowSettlement} + isLoading={!isOffline && !canAllowSettlement} /> )} @@ -305,7 +308,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldShowApproveButton={shouldShowApproveButton} formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''} shouldDisableApproveButton={shouldDisableApproveButton} - isDisabled={!canAllowSettlement} + isDisabled={isOffline && !canAllowSettlement} + isLoading={!isOffline && !canAllowSettlement} /> )} diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e08b35dd3866..87fc207dd1fd 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -35,6 +35,7 @@ import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import * as IOU from '@userActions/IOU'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -320,7 +321,7 @@ function MoneyRequestConfirmationList({ const previousTransactionCurrency = usePrevious(transaction?.currency); const isFocused = useIsFocused(); - const [formError, debouncedFormError, setFormError] = useDebouncedState(''); + const [formError, debouncedFormError, setFormError] = useDebouncedState(''); const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); @@ -348,14 +349,14 @@ function MoneyRequestConfirmationList({ const isCategoryRequired = !!policy?.requiresCategory; useEffect(() => { - if (shouldDisplayFieldError && hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } if (shouldDisplayFieldError && didConfirmSplit) { setFormError('iou.error.genericSmartscanFailureMessage'); return; } + if (shouldDisplayFieldError && hasSmartScanFailed) { + setFormError('iou.receiptScanningFailed'); + return; + } // reset the form error whenever the screen gains or loses focus setFormError(''); @@ -718,20 +719,7 @@ function MoneyRequestConfirmationList({ return; } - if (formError) { - return; - } - - if (iouType === CONST.IOU.TYPE.PAY) { - if (!paymentMethod) { - return; - } - - setDidConfirm(true); - - Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney?.(paymentMethod); - } else { + if (iouType !== CONST.IOU.TYPE.PAY) { // validate the amount for distance expenses const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { @@ -745,27 +733,43 @@ function MoneyRequestConfirmationList({ return; } + if (formError) { + return; + } + playSound(SOUNDS.DONE); setDidConfirm(true); onConfirm?.(selectedParticipants); + } else { + if (!paymentMethod) { + return; + } + if (formError) { + return; + } + + setDidConfirm(true); + + Log.info(`[IOU] Sending money via: ${paymentMethod}`); + onSendMoney?.(paymentMethod); } }, [ selectedParticipants, + isEditingSplitBill, isMerchantRequired, isMerchantEmpty, shouldDisplayFieldError, transaction, + iouCategory.length, + formError, iouType, + setFormError, onSendMoney, iouCurrencyCode, isDistanceRequest, - iouCategory, isDistanceRequestWithPendingRoute, iouAmount, - isEditingSplitBill, - formError, - setFormError, onConfirm, ], ); @@ -817,7 +821,7 @@ function MoneyRequestConfirmationList({ )} @@ -834,10 +838,11 @@ function MoneyRequestConfirmationList({ policyID, splitOrRequestOptions, formError, - debouncedFormError, - shouldShowReadOnlySplits, styles.ph1, styles.mb2, + shouldShowReadOnlySplits, + debouncedFormError, + translate, ]); // An intermediate structure that helps us classify the fields as "primary" and "supplementary". diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 1ff3ee2ed737..70354c4a4676 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -5,8 +5,6 @@ import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import type {MaybePhraseKey} from '@libs/Localize'; import mapChildrenFlat from '@libs/mapChildrenFlat'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import type {AllStyles} from '@styles/utils/types'; @@ -63,10 +61,6 @@ type OfflineWithFeedbackProps = ChildrenProps & { type StrikethroughProps = Partial & {style: Array}; -function isMaybePhraseKeyType(message: unknown): message is MaybePhraseKey { - return typeof message === 'string' || Array.isArray(message); -} - function OfflineWithFeedback({ pendingAction, canDismissError = true, @@ -90,8 +84,8 @@ function OfflineWithFeedback({ // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. const errorEntries = Object.entries(errors ?? {}); - const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, MaybePhraseKey | ReceiptError] => errorEntry[1] !== null); - const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => (isMaybePhraseKeyType(error) ? ErrorUtils.getErrorMessageWithTranslationData(error) : error)); + const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null); + const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error); const hasErrorMessages = !isEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; diff --git a/src/components/PDFView/PDFPasswordForm.tsx b/src/components/PDFView/PDFPasswordForm.tsx index e1ef83c9b60d..4b6491addbef 100644 --- a/src/components/PDFView/PDFPasswordForm.tsx +++ b/src/components/PDFView/PDFPasswordForm.tsx @@ -47,13 +47,13 @@ function PDFPasswordForm({isFocused, isPasswordInvalid = false, shouldShowLoadin const errorText = useMemo(() => { if (isPasswordInvalid) { - return 'attachmentView.passwordIncorrect'; + return translate('attachmentView.passwordIncorrect'); } if (validationErrorText) { return validationErrorText; } return ''; - }, [isPasswordInvalid, validationErrorText]); + }, [isPasswordInvalid, validationErrorText, translate]); useEffect(() => { if (!isFocused) { @@ -90,7 +90,7 @@ function PDFPasswordForm({isFocused, isPasswordInvalid = false, shouldShowLoadin return true; } if (!password) { - setValidationErrorText('attachmentView.passwordRequired'); + setValidationErrorText(translate('attachmentView.passwordRequired')); } return false; }; diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index d935ebe8fdc5..a6a942f41f26 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -1,6 +1,5 @@ import type {ChangeEvent, Component, ReactElement} from 'react'; import type {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; -import type {MaybePhraseKey} from '@libs/Localize'; type MeasureLayoutOnFailCallback = () => void; @@ -59,7 +58,7 @@ type BasePickerProps = { placeholder?: PickerPlaceholder; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Customize the BasePicker container */ containerStyles?: StyleProp; diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx index 50cca4abcf03..43f44065f5d9 100644 --- a/src/components/RadioButtonWithLabel.tsx +++ b/src/components/RadioButtonWithLabel.tsx @@ -3,7 +3,6 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import FormHelpMessage from './FormHelpMessage'; import * as Pressables from './Pressable'; import RadioButton from './RadioButton'; @@ -29,7 +28,7 @@ type RadioButtonWithLabelProps = { hasError?: boolean; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; }; const PressableWithFeedback = Pressables.PressableWithFeedback; diff --git a/src/components/RadioButtons.tsx b/src/components/RadioButtons.tsx index 2030ce8f0bfd..07e8fe38f772 100644 --- a/src/components/RadioButtons.tsx +++ b/src/components/RadioButtons.tsx @@ -3,7 +3,6 @@ import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import FormHelpMessage from './FormHelpMessage'; import RadioButtonWithLabel from './RadioButtonWithLabel'; @@ -24,7 +23,7 @@ type RadioButtonsProps = { onPress: (value: string) => void; /** Potential error text provided by a form InputWrapper */ - errorText?: MaybePhraseKey; + errorText?: string; /** Style for radio button */ radioButtonStyle?: StyleProp; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index cb588ca911f7..83fb8cd25292 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -28,6 +28,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {TransactionDetails} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; @@ -83,7 +84,13 @@ function MoneyRequestPreviewContent({ // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(transaction) ?? {}; + const { + amount: requestAmount, + currency: requestCurrency, + comment: requestComment, + merchant, + } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); + const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(transaction); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e233e60d7c0d..58d56b005f0b 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -1,8 +1,9 @@ import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {useSession} from '@components/OnyxProvider'; @@ -28,11 +29,13 @@ import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {TransactionDetails} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import * as IOU from '@userActions/IOU'; +import * as Link from '@userActions/Link'; import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -98,6 +101,8 @@ function MoneyRequestView({ const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const parentReportAction = parentReportActions?.[report.parentReportActionID ?? '-1'] ?? null; const isTrackExpense = ReportUtils.isTrackExpenseReport(report); const {canUseViolations, canUseP2PDistanceRequests} = usePermissions(isTrackExpense ? CONST.IOU.TYPE.TRACK : undefined); @@ -115,7 +120,7 @@ function MoneyRequestView({ originalAmount: transactionOriginalAmount, originalCurrency: transactionOriginalCurrency, cardID: transactionCardID, - } = ReportUtils.getTransactionDetails(transaction) ?? {}; + } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; @@ -168,6 +173,8 @@ function MoneyRequestView({ const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); + const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport); + const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID; const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback( @@ -544,6 +551,15 @@ function MoneyRequestView({ /> )} + {shouldShowViewTripDetails && ( + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID))} + /> + )} {shouldShowBillable && ( diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 6ef01b3eb6f5..daa7e24709c2 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -15,6 +15,7 @@ import SettlementButton from '@components/SettlementButton'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -109,6 +110,7 @@ function ReportPreview({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); + const {isOffline} = useNetwork(); const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions} = useMemo( () => ({ @@ -421,7 +423,8 @@ function ReportPreview({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} - isDisabled={!canAllowSettlement} + isDisabled={isOffline && !canAllowSettlement} + isLoading={!isOffline && !canAllowSettlement} /> )} {shouldShowSubmitButton && ( diff --git a/src/components/RoomNameInput/types.ts b/src/components/RoomNameInput/types.ts index 80f08a01e472..763d0f3f2668 100644 --- a/src/components/RoomNameInput/types.ts +++ b/src/components/RoomNameInput/types.ts @@ -1,10 +1,9 @@ import type {NativeSyntheticEvent, ReturnKeyTypeOptions, TextInputFocusEventData, TextInputSubmitEditingEventData} from 'react-native'; -import type {MaybePhraseKey} from '@libs/Localize'; type RoomNameInputProps = { value?: string; disabled?: boolean; - errorText?: MaybePhraseKey; + errorText?: string; onChangeText?: (value: string) => void; onSubmitEditing?: (event: NativeSyntheticEvent) => void; onInputChange?: (value: string) => void; diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index c597b8c741db..874e61a37713 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -139,7 +139,7 @@ function ScreenWrapper( const navigationFallback = useNavigation>(); const navigation = navigationProp ?? navigationFallback; const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight); - const {isSmallScreenWidth} = useResponsiveLayout(); + const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {initialHeight} = useInitialDimensions(); const styles = useThemeStyles(); const keyboardState = useKeyboardState(); @@ -279,7 +279,7 @@ function ScreenWrapper( : children } {isSmallScreenWidth && shouldShowOfflineIndicator && } - {!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && ( + {!shouldUseNarrowLayout && shouldShowOfflineIndicatorInWideScreen && ( customListHeader={ @@ -131,6 +133,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { sortOrder={sortOrder} isSortingAllowed={isSortingAllowed} sortBy={sortBy} + shouldShowYear={shouldShowYear} /> } // To enhance the smoothness of scrolling and minimize the risk of encountering blank spaces during scrolling, diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index d17d923a54e1..c0fff452d1e5 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -13,6 +13,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DateUtils from '@libs/DateUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import variables from '@styles/variables'; @@ -69,7 +70,14 @@ function ReceiptCell({transactionItem}: TransactionCellProps) { const StyleUtils = useStyleUtils(); return ( - + - + void; + shouldShowYear: boolean; }; -function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPress}: SearchTableHeaderProps) { +function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPress, shouldShowYear}: SearchTableHeaderProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); @@ -123,7 +124,7 @@ function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPre textStyle={textStyle} sortOrder={sortOrder ?? CONST.SORT_ORDER.ASC} isActive={isActive} - containerStyle={[StyleUtils.getSearchTableColumnStyles(columnName)]} + containerStyle={[StyleUtils.getSearchTableColumnStyles(columnName, shouldShowYear)]} isSortable={isSortable} onPress={(order: SortOrder) => onSortPress(columnName, order)} /> diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 5d5e7fc3891b..3ea9c3a32646 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,6 +1,5 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; -import type {MaybePhraseKey} from '@libs/Localize'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import type CONST from '@src/CONST'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -168,6 +167,11 @@ type TransactionListItemType = ListItem & /** Whether we should show the tax column */ shouldShowTax: boolean; + + /** Whether we should show the transaction year. + * This is true if at least one transaction in the dataset was created in past years + */ + shouldShowYear: boolean; }; type ReportListItemType = ListItem & @@ -288,7 +292,7 @@ type BaseSelectionListProps = Partial & { textInputPlaceholder?: string; /** Hint for the text input */ - textInputHint?: MaybePhraseKey; + textInputHint?: string; /** Value for the text input */ textInputValue?: string; diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx index 3ff844dd80e9..c2dc72438e43 100644 --- a/src/components/SingleChoiceQuestion.tsx +++ b/src/components/SingleChoiceQuestion.tsx @@ -3,14 +3,13 @@ import React, {forwardRef} from 'react'; // eslint-disable-next-line no-restricted-imports import type {Text as RNText} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import type {Choice} from './RadioButtons'; import RadioButtons from './RadioButtons'; import Text from './Text'; type SingleChoiceQuestionProps = { prompt: string; - errorText?: MaybePhraseKey; + errorText?: string; possibleAnswers: Choice[]; currentQuestionIndex: number; onInputChange: (value: string) => void; diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx index 67ba80c13ef8..2481c29d8123 100644 --- a/src/components/StateSelector.tsx +++ b/src/components/StateSelector.tsx @@ -6,7 +6,6 @@ import type {View} from 'react-native'; import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {MaybePhraseKey} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -17,7 +16,7 @@ type State = keyof typeof COMMON_CONST.STATES; type StateSelectorProps = { /** Form error text. e.g when no state is selected */ - errorText?: MaybePhraseKey; + errorText?: string; /** Current selected state */ value?: State | ''; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 46a88c54219c..789b4b4957c3 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -27,8 +27,8 @@ type SubIcon = { }; type SubscriptAvatarProps = { - /** Avatar URL or icon */ - mainAvatar?: IconType; + /** Avatar icon */ + mainAvatar: IconType; /** Subscript avatar URL or icon */ secondaryAvatar?: IconType; diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index fc29da30941d..c62b84911835 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; @@ -54,9 +54,11 @@ type TaxPickerProps = TaxPickerOnyxProps & { /** The type of IOU */ iouType?: ValueOf; + + onDismiss: () => void; }; -function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit, action, splitDraftTransaction, iouType}: TaxPickerProps) { +function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit, action, splitDraftTransaction, iouType, onDismiss}: TaxPickerProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); @@ -94,6 +96,17 @@ function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit, const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]); + const handleSelectRow = useCallback( + (newSelectedOption: OptionsListUtils.TaxRatesOption) => { + if (selectedOptionKey === newSelectedOption.keyForList) { + onDismiss(); + return; + } + onSubmit(newSelectedOption); + }, + [onSubmit, onDismiss, selectedOptionKey], + ); + return ( ({ user: { key: ONYXKEYS.USER, }, - }), - withNetwork(), -)(TestToolMenu); + })(TestToolMenu), +); diff --git a/src/components/TextBlock.tsx b/src/components/TextBlock.tsx new file mode 100644 index 000000000000..8b036f42f4cc --- /dev/null +++ b/src/components/TextBlock.tsx @@ -0,0 +1,39 @@ +/** + * TextBlock component splits a given text into individual words and displays + * each word within a Text component. + */ +import React, {memo, useMemo} from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; +import Text from './Text'; + +type TextBlockProps = { + /** The color of the text */ + color?: string; + + /** Styles to apply to each text word */ + textStyles?: StyleProp; + + /** The full text to be split into words */ + text: string; +}; + +function TextBlock({color, textStyles, text}: TextBlockProps) { + const words = useMemo(() => text.match(/(\S+\s*)/g) ?? [], [text]); + + return ( + <> + {words.map((word) => ( + + {word} + + ))} + + ); +} + +TextBlock.displayName = 'TextBlock'; + +export default memo(TextBlock); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e8e2d5ab352d..7a46cca693e3 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,5 @@ import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import type {MaybePhraseKey} from '@libs/Localize'; import type IconAsset from '@src/types/utils/IconAsset'; type CustomBaseTextInputProps = { @@ -20,7 +19,7 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText?: MaybePhraseKey; + errorText?: string; /** Icon to display in right side of text input */ icon?: IconAsset | null; @@ -68,7 +67,7 @@ type CustomBaseTextInputProps = { hideFocusedState?: boolean; /** Hint text to display below the TextInput */ - hint?: MaybePhraseKey; + hint?: string; /** Prefix character */ prefixCharacter?: string; diff --git a/src/components/TextPicker/types.ts b/src/components/TextPicker/types.ts index 179d16a07262..cbdc1ed21efe 100644 --- a/src/components/TextPicker/types.ts +++ b/src/components/TextPicker/types.ts @@ -1,6 +1,5 @@ import type {MenuItemBaseProps} from '@components/MenuItem'; import type {BaseTextInputProps} from '@components/TextInput/BaseTextInput/types'; -import type {MaybePhraseKey} from '@libs/Localize'; type TextProps = Exclude; @@ -30,7 +29,7 @@ type TextPickerProps = { placeholder?: string; /** Form Error description */ - errorText?: MaybePhraseKey; + errorText?: string; /** Callback to call when the input changes */ onInputChange?: (value: string | undefined) => void; diff --git a/src/components/TimePicker/TimePicker.tsx b/src/components/TimePicker/TimePicker.tsx index aecaf74dc4a3..8905abd370fe 100644 --- a/src/components/TimePicker/TimePicker.tsx +++ b/src/components/TimePicker/TimePicker.tsx @@ -135,15 +135,15 @@ function TimePicker({defaultValue = '', onSubmit, onInputChange = () => {}}: Tim const hour = parseInt(hourStr, 10); if (hour === 0) { setError(true); - setErrorMessage('common.error.invalidTimeRange'); + setErrorMessage(translate('common.error.invalidTimeRange')); return false; } const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({timeString, dateTimeString: defaultValue}); setError(!isValid); - setErrorMessage('common.error.invalidTimeShouldBeFuture'); + setErrorMessage(translate('common.error.invalidTimeShouldBeFuture')); return isValid; }, - [hours, minutes, amPmValue, defaultValue], + [hours, minutes, amPmValue, defaultValue, translate], ); const resetHours = () => { diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx index f04b92644d89..c55edd9e6b15 100644 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx @@ -31,7 +31,7 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA // We replace the actor's email, name, and avatar with the Copilot manually for now. This will be improved upon when // the Copilot feature is implemented. - if (delegateAccountID) { + if (delegateAccountID && delegateAccountID > 0) { const delegateUserDetails = personalDetails?.[delegateAccountID]; const delegateUserDisplayName = ReportUtils.getUserDetailTooltipText(delegateAccountID); userDisplayName = `${delegateUserDisplayName} (${translate('reportAction.asCopilot')} ${userDisplayName})`; diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts index b9c2c89948d9..b57c9d32061a 100644 --- a/src/components/ValuePicker/types.ts +++ b/src/components/ValuePicker/types.ts @@ -1,5 +1,4 @@ import type {ListItem} from '@components/SelectionList/types'; -import type {MaybePhraseKey} from '@libs/Localize'; type ValuePickerListItem = ListItem & { value?: string; @@ -51,7 +50,7 @@ type ValuePickerProps = { placeholder?: string; /** Form Error description */ - errorText?: MaybePhraseKey; + errorText?: string; /** Callback to call when the input changes */ onInputChange?: (value: string | undefined) => void; diff --git a/src/components/WorkspaceEmptyStateSection.tsx b/src/components/WorkspaceEmptyStateSection.tsx index e1ee7a1b66c3..6a00aa4bf5eb 100644 --- a/src/components/WorkspaceEmptyStateSection.tsx +++ b/src/components/WorkspaceEmptyStateSection.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -15,14 +16,29 @@ type WorkspaceEmptyStateSectionProps = { /** The icon to display along with the title */ icon: IconAsset; + + /** Additional style for container */ + containerStyle?: StyleProp; + + /** Whether to apply card style to container */ + shouldStyleAsCard?: boolean; }; -function WorkspaceEmptyStateSection({icon, subtitle, title}: WorkspaceEmptyStateSectionProps) { +function WorkspaceEmptyStateSection({icon, subtitle, title, containerStyle, shouldStyleAsCard = true}: WorkspaceEmptyStateSectionProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); return ( - + { @@ -18,8 +18,7 @@ const insertAtCaret = (target: HTMLElement, text: string) => { // Move caret to the end of the newly inserted text node. range.setStart(node, node.length); range.setEnd(node, node.length); - selection.removeAllRanges(); - selection.addRange(range); + selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset); // Dispatch paste event to simulate real browser behavior target.dispatchEvent(new Event('paste', {bubbles: true})); @@ -46,9 +45,19 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi insertByCommand(text); } + if (!textInputRef.current?.isFocused()) { + textInputRef.current?.focus(); + return; + } + // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. - textInputRef.current?.blur(); - textInputRef.current?.focus(); + // To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler + // We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered + textInputHTMLElement.dispatchEvent( + new FocusEvent('focusin', { + bubbles: true, + }), + ); // eslint-disable-next-line no-empty } catch (e) {} // We only need to set the callback once. @@ -62,8 +71,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi */ const handlePastedHTML = useCallback( (html: string) => { - const parser = new ExpensiMark(); - paste(parser.htmlToMarkdown(html)); + paste(parseHtmlToMarkdown(html)); }, [paste], ); diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index fa97e2047bca..d5089d9f7990 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -76,22 +76,24 @@ const chatReportSelector = (report: OnyxEntry): ChatReportSele const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => (reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = reportAction.message?.[0]?.moderationDecision?.decision; - - return { - reportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ] as Message[], - originalMessage, - }; - })) as ReportActionsSelector; + Object.values(reportActions) + .filter(Boolean) + .map((reportAction) => { + const {reportActionID, actionName, errors = [], originalMessage} = reportAction; + const decision = reportAction.message?.[0]?.moderationDecision?.decision; + + return { + reportActionID, + actionName, + errors, + message: [ + { + moderationDecision: {decision}, + }, + ] as Message[], + originalMessage, + }; + })) as ReportActionsSelector; const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { diff --git a/src/languages/en.ts b/src/languages/en.ts index 1d3f495369d9..bf3803c7606d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1927,6 +1927,7 @@ export default { hotel: 'Hotel', car: 'Car', viewTrip: 'View trip', + viewTripDetails: 'View trip details', trip: 'Trip', tripSummary: 'Trip summary', departs: 'Departs', @@ -1979,32 +1980,32 @@ export default { subscription: 'Subscription', }, qbo: { - importDescription: 'Choose which coding configurations are imported from QuickBooks Online to Expensify.', + importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.', classes: 'Classes', locations: 'Locations', - customers: 'Customers/Projects', - accountsDescription: 'When connected to Quickbooks Online, chart of accounts are always imported to Expensify as categories.', - accountsSwitchTitle: 'Below you can choose to have any new account imported as an enabled or disabled category by default.', - accountsSwitchDescription: 'Enabled categories are available for members to select when creating their expenses.', - classesDescription: 'Choose whether to import classes, and see where classes are displayed.', - customersDescription: 'Choose whether to import customers/projects and see where customers/projects are displayed.', - locationsDescription: 'Choose whether to import locations, and see where locations are displayed.', - taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', - locationsAdditionalDescription: `QuickBooks Online does not support adding a location to vendor bills or checks. Update your export preference to journal entry if you'd like to import locations as tags.`, + customers: 'Customers/projects', + accountsDescription: 'Your Quickbooks Online chart of accounts will import into Expensify as categories.', + accountsSwitchTitle: 'Choose to import new accounts as enabled or disabled categories.', + accountsSwitchDescription: 'Enabled categories will be available for members to select when creating their expenses.', + classesDescription: 'Choose how to handle QuickBooks Online classes in Expensify.', + customersDescription: 'Choose how to handle QuickBooks Online customers/projects in Expensify.', + locationsDescription: 'Choose how to handle QuickBooks Online locations in Expensify.', + taxesDescription: 'Choose how to handle QuickBooks Online taxes in Expensify.', + locationsAdditionalDescription: + 'QuickBooks Online doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.', outOfPocketLocationEnabledDescription: - 'Note: QuickBooks Online does not support a field for locations on vendor bill or check exports. As you have locations enabled on your workspace, this export option is unavailable.', - taxesJournalEntrySwitchNote: - 'Note: QuickBooks Online does not support a field for tax on Journal Entry exports. Change your export preference to Vendor Bill or Check to import taxes.', + 'QuickBooks Online doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.', + taxesJournalEntrySwitchNote: "QuickBooks Online doesn't support taxes on journal entries. Please change your export option to vendor bill or check.", export: 'Export', exportAs: 'Export as', - exportDescription: 'Configure how data in Expensify gets exported to QuickBooks Online.', + exportDescription: 'Configure how Expensify data exports to QuickBooks Online.', preferredExporter: 'Preferred exporter', date: 'Date', exportExpenses: 'Export out-of-pocket expenses as', exportInvoices: 'Export invoices to', exportCompany: 'Export company cards as', exportExpensifyCard: 'Export Expensify Card transactions as', - deepDiveExpensifyCard: 'Expensify Card transactions automatically export to a "Expensify Card Liability Account" created with', + deepDiveExpensifyCard: 'Expensify Card transactions will automatically export to an "Expensify Card Liability Account" created with', deepDiveExpensifyCardIntegration: 'our integration.', exportDate: { label: 'Export date', @@ -2012,62 +2013,60 @@ export default { values: { [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { label: 'Date of last expense', - description: 'The date of the most recent expense on the report', + description: 'Date of the most recent expense on the report.', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_EXPORTED]: { label: 'Export date', - description: 'The date the report was exported to QuickBooks Online', + description: 'Date the report was exported to QuickBooks Online.', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_SUBMITTED]: { label: 'Submitted date', - description: 'The date the report was submitted for approval', + description: 'Date the report was submitted for approval.', }, }, }, receivable: 'Accounts receivable', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. archive: 'Accounts receivable archive', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. - exportInvoicesDescription: 'Invoices will be exported to this account in QuickBooks Online.', + exportInvoicesDescription: 'Invoices will export to this account in QuickBooks Online.', exportCompanyCardsDescription: 'Set how company card purchases export to QuickBooks Online.', vendor: 'Vendor', defaultVendor: 'Default vendor', defaultVendorDescription: 'Set a default vendor that will apply to all credit card transactions upon export.', - exportPreferredExporterNote: 'This can be any workspace admin, but must be a Domain Admin if you set different export accounts for individual company cards in Domain Settings.', + exportPreferredExporterNote: + 'The preferred exporter can be any workspace admin, but must also be a Domain Admin if you set different export accounts for individual company cards in Domain Settings.', exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Online.', - exportCheckDescription: "We'll create a single itemized check for each Expensify report. You can write the check from your bank account of choice (below).", - exportJournalEntryDescription: "We'll create a single itemized journal entry for each Expensify report. You can post the offset entry to your account of choice (below).", + exportCheckDescription: "We'll create an itemized check for each Expensify report and send it from the bank account below.", + exportJournalEntryDescription: "We'll create an itemized journal entry for each Expensify report and post it to the account below.", exportVendorBillDescription: - "We'll create a single itemized vendor bill for each Expensify report. If the period of the bill is closed, we'll post to the 1st of the next open period. You can add the vendor bill to your A/P account of choice (below).", + "We'll create an itemized vendor bill for each Expensify report and add it to the account below. If this period is closed, we'll post to the 1st of the next open period.", account: 'Account', - accountDescription: 'This is your chosen account to post the journal entry offset for each report.', + accountDescription: 'Choose where to post journal entry offsets.', accountsPayable: 'Accounts payable', - accountsPayableDescription: 'This is your chosen A/P account, against which vendor bills for each report are created.', + accountsPayableDescription: 'Choose where to create vendor bills.', bankAccount: 'Bank account', - bankAccountDescription: 'This is your chosen bank account to write checks from.', + bankAccountDescription: 'Choose where to send checks from.', optionBelow: 'Choose an option below:', companyCardsLocationEnabledDescription: - 'Note: QuickBooks Online does not support a field for locations on vendor bill exports. As you have locations enabled on your workspace, this export option is unavailable.', + "QuickBooks Online doesn't support locations on vendor bill exports. As you have locations enabled on your workspace, this export option is unavailable.", outOfPocketTaxEnabledDescription: - "Note: QuickBooks Online doesn't support a field for tax on Journal Entry exports. Because you have tax tracking enabled on your workspace, this export option is unavailable.", - outOfPocketTaxEnabledError: 'Journal entry is not available when taxes enabled. please select a different export option.', - outOfPocketLocationEnabledError: 'Vendor Bills are not available when locations are enabled. Please select a different export option.', + "QuickBooks Online doesn't support taxes on journal entry exports. As you have taxes enabled on your workspace, this export option is unavailable.", + outOfPocketTaxEnabledError: 'Journal entries are unavailable when taxes are enabled. Please choose a different export option.', + outOfPocketLocationEnabledError: 'Vendor bills are unavailable when locations are enabled. Please choose a different export option.', advancedConfig: { advanced: 'Advanced', autoSync: 'Auto-sync', - autoSyncDescription: 'Changes made in Quickbooks will automatically be reflected in Expensify.', + autoSyncDescription: 'Sync QuickBooks Online and Expensify automatically, every day.', inviteEmployees: 'Invite employees', - inviteEmployeesDescription: 'Import Quickbooks Online employee records and invite them to this workspace.', - createEntities: 'Automatically create entities', - createEntitiesDescription: - 'Expensify will automatically create a vendor in Quickbooks, if one does not exist. Expensify will also automatically create a customer when exporting invoices.', + inviteEmployeesDescription: 'Import Quickbooks Online employee records and invite employees to this workspace.', + createEntities: 'Auto-create entities', + createEntitiesDescription: "Expensify will automatically create vendors in QuickBooks Online if they don't exist already, and auto-create customers when exporting invoices.", reimbursedReports: 'Sync reimbursed reports', - reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Quickbooks account below.', + reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Quickbooks Online account below.', qboBillPaymentAccount: 'QuickBooks bill payment account', qboInvoiceCollectionAccount: 'QuickBooks invoice collections account', - accountSelectDescription: - "As you've enabled sync reimbursed reports, you will need select the bank account your reimbursements are coming out of, and we'll create the payment in QuickBooks.", - invoiceAccountSelectorDescription: - 'If you are exporting invoices from Expensify to Quickbooks Online, this is the account the invoice will appear against once marked as paid.', + accountSelectDescription: "Choose a bank account for reimbursements and we'll create the payment in QuickBooks Online.", + invoiceAccountSelectorDescription: 'Once an invoice is marked as paid in Expensify and exported to QuickBooks Online, it’ll appear against the account below.', }, accounts: { [CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD]: 'Debit card', @@ -2081,66 +2080,64 @@ export default { [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]: "We'll automatically match the merchant name on the credit card transaction to any corresponding vendors in QuickBooks. If no vendors exist, we'll create a 'Credit Card Misc.' vendor for association.", [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]: - "We'll create a single itemized vendor bill for each Expensify report, carrying the date of the last expense on the report. If this period is closed, we'll post to the 1st of the next open period. You can add the vendor bill to your A/P account of choice (below).", + "We'll create an itemized vendor bill for each Expensify report with the date of the last expense, and add it to the account below. If this period is closed, we'll post to the 1st of the next open period.", [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD}AccountDescription`]: 'Debit card transactions will export to the bank account below.”', [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}AccountDescription`]: 'Credit card transactions will export to the bank account below.', - [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]: 'Select the vendor applied to all credit card transactions.', + [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]: 'Choose a vendor to apply to all credit card transactions.', - [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]: 'Vendor Bills are not available when locations are enabled. Please select a different export option.', - [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK}Error`]: 'Check is not available when locations are enabled. Please select a different export option.', - [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY}Error`]: 'Journal entry is not available when taxes enabled. please select a different export option.', + [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]: 'Vendor bills are unavailable when locations are enabled. Please choose a different export option.', + [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK}Error`]: 'Checks are unavailable when locations are enabled. Please choose a different export option.', + [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY}Error`]: 'Journal entries are unavailable when taxes are enabled. Please choose a different export option.', }, noAccountsFound: 'No accounts found', - noAccountsFoundDescription: 'Add the account in Quickbooks Online and sync the connection again', + noAccountsFoundDescription: 'Add the account in Quickbooks Online and sync the connection again.', }, xero: { organization: 'Xero organization', - organizationDescription: 'Select the organization in Xero you are importing data from.', - importDescription: 'Choose which coding configurations are imported from Xero to Expensify.', - accountsDescription: 'When connected to Xero, chart of accounts are always imported to Expensify as categories.', - accountsSwitchTitle: 'Below you can choose to have any new account imported as an enabled or disabled category by default.', - accountsSwitchDescription: 'Enabled categories are available for members to select when creating their expenses.', + organizationDescription: "Choose the Xero organization that you'd like to import data from.", + importDescription: 'Choose which coding configurations to import from Xero to Expensify.', + accountsDescription: 'Your Xero chart of accounts will import into Expensify as categories.', + accountsSwitchTitle: 'Choose to import new accounts as enabled or disabled categories.', + accountsSwitchDescription: 'Enabled categories will be available for members to select when creating their expenses.', trackingCategories: 'Tracking categories', - trackingCategoriesDescription: 'Choose whether to import tracking categories and see where they are displayed.', + trackingCategoriesDescription: 'Choose how to handle Xero tracking categories in Expensify.', mapTrackingCategoryTo: ({categoryName}) => `Map Xero ${categoryName} to`, - mapTrackingCategoryToDescription: ({categoryName}) => `Choose where to map ${categoryName} to when exporting to Xero.`, + mapTrackingCategoryToDescription: ({categoryName}) => `Choose where to map ${categoryName} when exporting to Xero.`, customers: 'Re-bill customers', - customersDescription: 'Import customer contacts. Billable expenses need tags for export. Expenses will carry the customer information to Xero for sales invoices.', - taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', + customersDescription: 'Choose whether to re-bill customers in Expensify. Your Xero customer contacts can be tagged to expenses, and will export to Xero as a sales invoice.', + taxesDescription: 'Choose how to handle Xero taxes in Expensify.', notImported: 'Not imported', trackingCategoriesOptions: { default: 'Xero contact default', tag: 'Tags', }, export: 'Export', - exportDescription: 'Configure how data in Expensify gets exported to Xero.', + exportDescription: 'Configure how Expensify data exports to Xero.', exportCompanyCard: 'Export company card expenses as', purchaseBill: 'Purchase bill', - exportDeepDiveCompanyCard: - 'Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement.', + exportDeepDiveCompanyCard: 'Each exported expense posts as a bank transaction to the Xero bank account below, and transaction dates will match the dates on your bank statement.', bankTransactions: 'Bank transactions', xeroBankAccount: 'Xero bank account', - xeroBankAccountDescription: 'Select the bank account expenses will be posted to as bank transactions.', + xeroBankAccountDescription: 'Choose where expenses will post as bank transactions.', preferredExporter: 'Preferred exporter', exportExpenses: 'Export out-of-pocket expenses as', - exportExpensesDescription: 'Reports will export as a purchase bill, using the date and status you select below.', + exportExpensesDescription: 'Reports will export as a purchase bill with the date and status selected below.', purchaseBillDate: 'Purchase bill date', exportInvoices: 'Export invoices as', salesInvoice: 'Sales invoice', exportInvoicesDescription: 'Sales invoices always display the date on which the invoice was sent.', advancedConfig: { advanced: 'Advanced', - autoSync: 'Auto-Sync', - autoSyncDescription: 'Sync Xero and Expensify automatically every day.', + autoSync: 'Auto-sync', + autoSyncDescription: 'Sync Xero and Expensify automatically, every day.', purchaseBillStatusTitle: 'Purchase bill status', reimbursedReports: 'Sync reimbursed reports', reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Xero account below.', - xeroBillPaymentAccount: 'Xero Bill Payment Account', - xeroInvoiceCollectionAccount: 'Xero Invoice Collections Account', - invoiceAccountSelectorDescription: "As you've enabled exporting invoices from Expensify to Xero, this is the account the invoice will appear against once marked as paid.", - xeroBillPaymentAccountDescription: - "As you've enabled sync reimbursed reports, you will need to select the bank account your reimbursements are coming out of, and we'll create the payment in Xero.", + xeroBillPaymentAccount: 'Xero bill payment account', + xeroInvoiceCollectionAccount: 'Xero invoice collections account', + invoiceAccountSelectorDescription: 'Once an invoice is marked as paid in Expensify and exported to Xero, it’ll appear against the account below.', + xeroBillPaymentAccountDescription: "Choose a bank account for reimbursements and we'll create the payment in Xero.", }, exportDate: { label: 'Export date', @@ -2148,31 +2145,32 @@ export default { values: { [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { label: 'Date of last expense', - description: 'The date of the most recent expense on the report', + description: 'Date of the most recent expense on the report.', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_EXPORTED]: { label: 'Export date', - description: 'The date the report was exported to Xero', + description: 'Date the report was exported to Xero.', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_SUBMITTED]: { label: 'Submitted date', - description: 'The date the report was submitted for approval', + description: 'Date the report was submitted for approval.', }, }, }, invoiceStatus: { label: 'Purchase bill status', - description: 'When exported to Xero what state should purchase bills have.', + description: 'Choose a status for purchase bills exported to Xero.', values: { [CONST.XERO_CONFIG.INVOICE_STATUS.DRAFT]: 'Draft', [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_APPROVAL]: 'Awaiting approval', [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_PAYMENT]: 'Awaiting payment', }, }, - exportPreferredExporterNote: 'This can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', + exportPreferredExporterNote: + 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', noAccountsFound: 'No accounts found', - noAccountsFoundDescription: 'Add the account in Xero and sync the connection again', + noAccountsFoundDescription: 'Add the account in Xero and sync the connection again.', }, type: { free: 'Free', @@ -2191,7 +2189,7 @@ export default { deleteFailureMessage: 'An error occurred while deleting the category, please try again.', categoryName: 'Category name', requiresCategory: 'Members must categorize all expenses', - needCategoryForExportToIntegration: 'A category is required on every expense in order to export to', + needCategoryForExportToIntegration: 'Require a category on every expense in order to export to', subtitle: 'Get a better overview of where money is being spent. Use our default categories or add your own.', emptyCategories: { title: "You haven't created any categories", @@ -2416,11 +2414,11 @@ export default { syncError: (integration?: ConnectionName): string => { switch (integration) { case CONST.POLICY.CONNECTIONS.NAME.QBO: - return "Couldn't connect to QuickBooks Online."; + return "Can't connect to QuickBooks Online."; case CONST.POLICY.CONNECTIONS.NAME.XERO: - return "Couldn't connect to Xero."; + return "Can't connect to Xero."; default: { - return "Couldn't connect to integration."; + return "Can't connect to integration."; } } }, @@ -2473,25 +2471,25 @@ export default { case 'quickbooksOnlineImportProcessing': return 'Processing imported data'; case 'quickbooksOnlineSyncBillPayments': - return 'Synchronizing reimbursed reports and bill Payments'; + return 'Syncing reimbursed reports and bill payments'; case 'quickbooksOnlineSyncTaxCodes': return 'Importing tax codes'; case 'quickbooksOnlineCheckConnection': return 'Checking QuickBooks Online connection'; case 'quickbooksOnlineImportMain': - return 'Importing your QuickBooks Online data'; + return 'Importing QuickBooks Online data'; case 'startingImportXero': - return 'Importing your Xero data'; + return 'Importing Xero data'; case 'startingImportQBO': - return 'Importing your QuickBooks Online data'; + return 'Importing QuickBooks Online data'; case 'quickbooksOnlineSyncTitle': - return 'Synchronizing QuickBooks Online data'; + return 'Syncing QuickBooks Online data'; case 'quickbooksOnlineSyncLoadData': return 'Loading data'; case 'quickbooksOnlineSyncApplyCategories': return 'Updating categories'; case 'quickbooksOnlineSyncApplyCustomers': - return 'Updating Customers/Projects'; + return 'Updating customers/projects'; case 'quickbooksOnlineSyncApplyEmployees': return 'Updating people list'; case 'quickbooksOnlineSyncApplyClassesLocations': @@ -2515,7 +2513,7 @@ export default { case 'xeroCheckConnection': return 'Checking Xero connection'; case 'xeroSyncTitle': - return 'Synchronizing Xero data'; + return 'Syncing Xero data'; case 'xeroSyncStep': return 'Loading data'; default: { @@ -2789,6 +2787,7 @@ export default { search: 'Open search dialog', newChat: 'New chat screen', copy: 'Copy comment', + openDebug: 'Open testing preferences dialog', }, }, guides: { @@ -3271,11 +3270,12 @@ export default { subscriptionSize: 'Subscription size', activeMembers: ({size}) => `${size} active members/month`, subscriptionRenews: 'Subscription renews', - youCantDowngrade: 'You can’t downgrade during your annual subscription', + youCantDowngrade: 'You can’t downgrade during your annual subscription.', youAlreadyCommitted: ({size, date}) => `You already committed to an annual subscription size of ${size} active members per month until ${date}. You can switch to a pay-per-use subscription on ${date} by disabling auto-renew.`, error: { size: 'Please enter a valid subscription size.', + sameSize: 'Please enter a number different than your current subscription size.', }, }, paymentCard: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 6556735ff167..4c900e23acc5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1952,6 +1952,7 @@ export default { hotel: 'Hotel', car: 'Auto', viewTrip: 'Ver viaje', + viewTripDetails: 'Ver detalles del viaje', trip: 'Viaje', tripSummary: 'Resumen del viaje', departs: 'Sale', @@ -2007,18 +2008,17 @@ export default { importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.', classes: 'Clases', locations: 'Lugares', - customers: 'Clientes/Proyectos', - accountsDescription: 'Cuando estás conectado a Quickbooks Online, los planes de cuentas siempre se importan a Expensify como categorías.', - accountsSwitchTitle: 'Elige abajo si las categorías importadas serán activadas o desactivadas por defecto.', + customers: 'Clientes/proyectos', + accountsDescription: 'Tu plan de cuentas de Quickbooks Online se importará a Expensify como categorías.', + accountsSwitchTitle: 'Elige importar cuentas nuevas como categorías activadas o desactivadas.', accountsSwitchDescription: 'Las categorías activas estarán disponibles para ser escogidas cuando se crea un gasto.', - classesDescription: 'Elige si quieres importar las clases y donde las clases son mostradas.', - customersDescription: 'Elige si queres importar clientes/proyectos y donde los clientes/proyectos son mostrados.', - locationsDescription: 'Elige si quieres importar lugares y donde los lugares son mostrados.', - taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', - taxesJournalEntrySwitchNote: - 'Nota: QuickBooks Online no admite un campo para impuestos al exportar entradas en el asiento contable. Cambia tu preferencia de exportación a Factura de Proveedor o Cheque para importar impuestos.', + classesDescription: 'Elige cómo gestionar las clases de QuickBooks Online en Expensify.', + customersDescription: 'Elige cómo gestionar los clientes/proyectos de QuickBooks Online en Expensify.', + locationsDescription: 'Elige cómo gestionar los lugares de QuickBooks Online en Expensify.', + taxesDescription: 'Elige cómo gestionar los impuestos de QuickBooks Online en Expensify.', + taxesJournalEntrySwitchNote: 'QuickBooks Online no permite impuestos en los asientos contables. Por favor, cambia la opción de exportación a factura de proveedor o cheque.', locationsAdditionalDescription: - 'QuickBooks Online no permite añadir una ubicación a las facturas de proveedores o a los cheques. Actualice su preferencia de exportación a asiento contable si desea importar ubicaciones como etiquetas.', + 'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.', export: 'Exportar', exportAs: 'Exportar cómo', exportExpenses: 'Exportar gastos de bolsillo como', @@ -2036,15 +2036,15 @@ export default { values: { [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { label: 'Fecha del último gasto', - description: 'Fecha del gasto mas reciente en el informe', + description: 'Fecha del gasto mas reciente en el informe.', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_EXPORTED]: { label: 'Fecha de exportación', - description: 'Fecha de exportación del informe a QuickBooks Online', + description: 'Fecha de exportación del informe a QuickBooks Online.', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_SUBMITTED]: { label: 'Fecha de envío', - description: 'Fecha en la que el informe se envió para tu aprobación', + description: 'Fecha en la que el informe se envió para tu aprobación.', }, }, }, @@ -2053,51 +2053,48 @@ export default { exportInvoicesDescription: 'Las facturas se exportarán a esta cuenta en QuickBooks Online.', exportCompanyCardsDescription: 'Establece cómo se exportan las compras con tarjeta de empresa a QuickBooks Online.', account: 'Cuenta', - accountDescription: 'Esta es la cuenta elegida para contabilizar la compensación del asiento contable de cada informe.', + accountDescription: 'Elige dónde contabilizar las compensaciones de entradas a los asientos contables.', vendor: 'Proveedor', defaultVendor: 'Proveedor predeterminado', defaultVendorDescription: 'Establece un proveedor predeterminado que se aplicará a todas las transacciones con tarjeta de crédito al momento de exportarlas.', accountsPayable: 'Cuentas por pagar', - accountsPayableDescription: 'Esta es la cuenta de cuentas por pagar elegida, contra la cual se crean las facturas de proveedores para cada informe.', + accountsPayableDescription: 'Elige dónde crear las facturas de proveedores.', bankAccount: 'Cuenta bancaria', - bankAccountDescription: 'Esta es la cuenta bancaria elegida para emitir cheques.', + bankAccountDescription: 'Elige desde dónde enviar los cheques.', optionBelow: 'Elija una opción a continuación:', companyCardsLocationEnabledDescription: - 'Nota: QuickBooks Online no admite un campo para Ubicaciones como etiquetas en las exportaciones de facturas de proveedores. A medida que importa ubicaciones, esta opción de exportación no está disponible.', + 'QuickBooks Online no permite lugares en las exportaciones de facturas de proveedores. Como tienes activadas los lugares en tu espacio de trabajo, esta opción de exportación no está disponible.', exportPreferredExporterNote: 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en tu cuenta.', exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Online.', - exportCheckDescription: 'Crearemos un único cheque desglosado para cada informe de Expensify. Puedes emitir el cheque desde la cuenta bancaria que elijas (más abajo).', - exportJournalEntryDescription: - 'Crearemos un único asiento contable desglosado para cada informe de Expensify. Puedes enviar la compensación de la entrada de diario a la cuenta que elijas (más abajo).', + exportCheckDescription: 'Crearemos un cheque desglosado para cada informe de Expensify y lo enviaremos desde la cuenta bancaria a continuación.', + exportJournalEntryDescription: 'Crearemos una entrada contable desglosada para cada informe de Expensify y lo contabilizaremos en la cuenta a continuación.', exportVendorBillDescription: - 'Crearemos una única factura de proveedor detallada para cada informe de Expensify. Si el período de la factura está cerrado, lo publicaremos en el día 1 del siguiente período abierto. Puede agregar la factura del proveedor a la cuenta A/P de tu elección (a continuación).', + 'Crearemos una factura de proveedor desglosada para cada informe de Expensify y la añadiremos a la cuenta a continuación. Si este periodo está cerrado, lo contabilizaremos en el día 1 del siguiente periodo abierto.', outOfPocketTaxEnabledDescription: - 'Nota: QuickBooks Online no admite un campo para impuestos al exportar asientos contables. Debido a que tienes habilitado el seguimiento de impuestos en tu área de trabajo, esta opción de exportación no está disponible.', - outOfPocketTaxEnabledError: 'El asiento contable no está disponible cuando los impuestos están activados. Por favor, selecciona una opción de exportación diferente.', - outOfPocketLocationEnabledError: 'Las facturas de proveedores no están disponibles cuando las ubicaciones están activadas. Seleccione otra opción de exportación.', + 'QuickBooks Online no permite impuestos en las exportaciones de entradas a los asientos contables. Como tienes los impuestos activados en tu espacio de trabajo, esta opción de exportación no está disponible.', + outOfPocketTaxEnabledError: 'La anotacion en el diario no está disponible cuando los impuestos están activados. Por favor, selecciona otra opción de exportación diferente.', + outOfPocketLocationEnabledError: + 'Las facturas de proveedores no están disponibles cuando las ubicaciones están activadas. Por favor, selecciona otra opción de exportación diferente.', outOfPocketLocationEnabledDescription: - 'Nota: QuickBooks Online no permite añadir una ubicación a las facturas de proveedores o a los cheques. Al importar ubicaciones como etiquetas, esta opción de exportación no está disponible.', + 'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.', advancedConfig: { advanced: 'Avanzado', autoSync: 'Autosincronización', - autoSyncDescription: 'Los cambios realizados en Quickbooks se reflejarán automáticamente en Expensify.', + autoSyncDescription: 'Sincroniza QuickBooks Online y Expensify automáticamente, todos los días.', inviteEmployees: 'Invitar empleados', inviteEmployeesDescription: 'Importe los registros de los empleados de Quickbooks Online e invítelos a este espacio de trabajo.', createEntities: 'Crear entidades automáticamente', - createEntitiesDescription: - 'Expensify creará automáticamente un proveedor en Quickbooks, si no existe. Expensify también creará automáticamente un cliente al exportar facturas.', + createEntitiesDescription: 'Expensify creará automáticamente proveedores en QuickBooks Online si aún no existen, y creará automáticamente clientes al exportar facturas.', reimbursedReports: 'Sincronizar informes reembolsados', reimbursedReportsDescription: - 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Quickbooks indicadas a continuación.', + 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Quickbooks Online indicadas a continuación.', qboBillPaymentAccount: 'Cuenta de pago de las facturas de QuickBooks', qboInvoiceCollectionAccount: 'Cuenta de cobro de las facturas QuickBooks', - accountSelectDescription: - 'Como has activado la sincronización de los informes de reembolso, tendrás que seleccionar la cuenta bancaria de la que saldrán tus reembolsos y crearemos el pago en QuickBooks.', - invoiceAccountSelectorDescription: - 'Si está exportando facturas de Expensify a Quickbooks Online, ésta es la cuenta en la que aparecerá la factura una vez marcada como pagada.', + accountSelectDescription: 'Elige una cuenta bancaria para los reembolsos y crearemos el pago en QuickBooks Online.', + invoiceAccountSelectorDescription: 'Una vez que una factura se marca como pagada en Expensify y se exporta a QuickBooks Online, aparecerá contra la cuenta a continuación.', }, accounts: { [CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD]: 'Tarjeta de débito', @@ -2111,7 +2108,7 @@ export default { [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]: "Automáticamente relacionaremos el nombre del comerciante de la transacción con tarjeta de crédito con cualquier proveedor correspondiente en QuickBooks. Si no existen proveedores, crearemos un proveedor asociado 'Credit Card Misc.'.", [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]: - 'Crearemos una única factura detallada con los proveedores por cada informe de Expensify, con fecha del último gasto en el informe. Si este período está cerrado, la publicaremos con fecha del día 1 del próximo período abierto. Puede añadir la factura del proveedor a la cuenta A/P de tu elección (a continuación).', + 'Crearemos una factura de proveedor desglosada para cada informe de Expensify con la fecha del último gasto, y la añadiremos a la cuenta a continuación. Si este periodo está cerrado, lo contabilizaremos en el día 1 del siguiente periodo abierto.', [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD}AccountDescription`]: 'Las transacciones con tarjeta de débito se exportarán a la cuenta bancaria que aparece a continuación.”', @@ -2120,30 +2117,30 @@ export default { [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]: 'Selecciona el proveedor que se aplicará a todas las transacciones con tarjeta de crédito.', [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]: - 'Las facturas de proveedores no están disponibles cuando las ubicaciones están habilitadas. Seleccione una opción de exportación diferente.', + 'Las facturas de proveedores no están disponibles cuando las ubicaciones están habilitadas. Por favor, selecciona otra opción de exportación diferente.', [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK}Error`]: - 'La verificación no está disponible cuando las ubicaciones están habilitadas. Seleccione una opción de exportación diferente.', + 'La verificación no está disponible cuando las ubicaciones están habilitadas. Por favor, selecciona otra opción de exportación diferente.', [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY}Error`]: - 'El asiento contable no está disponible cuando los impuestos están habilitados. seleccione una opción de exportación diferente.', + 'El asiento de diario no está disponible cuando los impuestos están habilitados. Por favor, selecciona otra opción de exportación diferente.', }, noAccountsFound: 'No se ha encontrado ninguna cuenta', - noAccountsFoundDescription: 'Añade la cuenta en Quickbooks Online y sincroniza de nuevo la conexión', + noAccountsFoundDescription: 'Añade la cuenta en Quickbooks Online y sincroniza de nuevo la conexión.', }, xero: { organization: 'Organización Xero', organizationDescription: 'Seleccione la organización en Xero desde la que está importando los datos.', importDescription: 'Elija qué configuraciones de codificación se importan de Xero a Expensify.', - accountsDescription: 'Cuando estás conectado a Xero, los planes de cuentas siempre se importan a Expensify como categorías.', - accountsSwitchTitle: 'Elige abajo si las categorías importadas serán activadas o desactivadas por defecto.', + accountsDescription: 'Tu plan de cuentas de Xero se importará a Expensify como categorías.', + accountsSwitchTitle: 'Elige importar cuentas nuevas como categorías activadas o desactivadas.', accountsSwitchDescription: 'Las categorías activas estarán disponibles para ser escogidas cuando se crea un gasto.', trackingCategories: 'Categorías de seguimiento', - trackingCategoriesDescription: 'Elige si deseas importar categorías de seguimiento y ver dónde se muestran.', + trackingCategoriesDescription: 'Elige cómo gestionar categorías de seguimiento de Xero en Expensify.', mapTrackingCategoryTo: ({categoryName}) => `Asignar ${categoryName} de Xero a`, mapTrackingCategoryToDescription: ({categoryName}) => `Elige dónde mapear ${categoryName} al exportar a Xero.`, customers: 'Volver a facturar a los clientes', customersDescription: - 'Importar contactos de clientes. Los gastos facturables necesitan etiquetas para la exportación. Los gastos llevarán la información del cliente a Xero para las facturas de ventas.', - taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', + 'Elige si quieres volver a facturar a los clientes en Expensify. Tus contactos de clientes de Xero se pueden etiquetar como gastos, y se exportarán a Xero como una factura de venta.', + taxesDescription: 'Elige cómo gestionar los impuestos de Xero en Expensify.', notImported: 'No importado', trackingCategoriesOptions: { default: 'Contacto de Xero por defecto', @@ -2157,7 +2154,7 @@ export default { 'Cada gasto exportado se contabiliza como una transacción bancaria en la cuenta bancaria de Xero que selecciones a continuación. Las fechas de las transacciones coincidirán con las fechas de el extracto bancario.', bankTransactions: 'Transacciones bancarias', xeroBankAccount: 'Cuenta bancaria de Xero', - xeroBankAccountDescription: 'Selecciona la cuenta bancaria en la que aparecerán los gastos como transacciones bancarias.', + xeroBankAccountDescription: 'Elige dónde se contabilizarán los gastos como transacciones bancarias.', preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos por cuenta propia como', exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', @@ -2168,17 +2165,15 @@ export default { advancedConfig: { advanced: 'Avanzado', autoSync: 'Autosincronización', - autoSyncDescription: 'Sincroniza Xero y Expensify automáticamente todos los días.', + autoSyncDescription: 'Sincroniza Xero y Expensify automáticamente, todos los días.', purchaseBillStatusTitle: 'Estado de la factura de compra', reimbursedReports: 'Sincronizar informes reembolsados', reimbursedReportsDescription: 'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Xero indicadas a continuación.', xeroBillPaymentAccount: 'Cuenta de pago de las facturas de Xero', xeroInvoiceCollectionAccount: 'Cuenta de cobro de las facturas Xero', - invoiceAccountSelectorDescription: - 'Como ha activado la exportación de facturas de Expensify a Xero, esta es la cuenta en la que aparecerá la factura una vez marcada como pagada.', - xeroBillPaymentAccountDescription: - 'Como has activado la sincronización de los informes reembolsados, tendrás que seleccionar la cuenta bancaria de la que se producen estos reembolsos y crearemos el pago en Xero.', + invoiceAccountSelectorDescription: 'Una vez que una factura se marca como pagada en Expensify y se exporta a Xero, aparecerá contra la cuenta a continuación.', + xeroBillPaymentAccountDescription: 'Elige una cuenta bancaria para los reembolsos y crearemos el pago en Xero.', }, exportDate: { label: 'Fecha de exportación', @@ -2200,7 +2195,7 @@ export default { }, invoiceStatus: { label: 'Estado de la factura de compra', - description: 'Qué estado deben tener las facturas de compra cuando se exportan a Xero.', + description: 'Elige un estado para las facturas de compra exportadas a Xero.', values: { [CONST.XERO_CONFIG.INVOICE_STATUS.DRAFT]: 'Borrador', [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_APPROVAL]: 'Pendiente de aprobación', @@ -2211,7 +2206,7 @@ export default { 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en su cuenta.', noAccountsFound: 'No se ha encontrado ninguna cuenta', - noAccountsFoundDescription: 'Añade la cuenta en Xero y sincroniza de nuevo la conexión', + noAccountsFoundDescription: 'Añade la cuenta en Xero y sincroniza de nuevo la conexión.', }, type: { free: 'Gratis', @@ -2498,7 +2493,7 @@ export default { case 'quickbooksOnlineSyncApplyCategories': return 'Actualizando categorías'; case 'quickbooksOnlineSyncApplyCustomers': - return 'Actualizando Clientes/Proyectos'; + return 'Actualizando clientes/proyectos'; case 'quickbooksOnlineSyncApplyEmployees': return 'Actualizando empleados'; case 'quickbooksOnlineSyncApplyClassesLocations': @@ -2831,6 +2826,7 @@ export default { search: 'Abrir diálogo de búsqueda', newChat: 'Nueva pantalla de chat', copy: 'Copiar comentario', + openDebug: 'Abrir el diálogo de preferencias de pruebas', }, }, guides: { @@ -3778,11 +3774,12 @@ export default { subscriptionSize: 'Tamaño de suscripción', activeMembers: ({size}) => `${size} miembros activos/mes`, subscriptionRenews: 'Renovación de la suscripción', - youCantDowngrade: 'No puedes bajar de categoría durante tu suscripción anual', + youCantDowngrade: 'No puedes bajar de categoría durante tu suscripción anual.', youAlreadyCommitted: ({size, date}) => `Ya se ha comprometido a un tamaño de suscripción anual de ${size} miembros activos al mes hasta el ${date}. Puede cambiar a una suscripción de pago por uso en ${date} desactivando la auto-renovación.`, error: { size: 'Por favor ingrese un tamaño de suscripción valido.', + sameSize: 'Por favor, introduce un número diferente al de tu subscripción actual.', }, }, paymentCard: { diff --git a/src/libs/API/parameters/GenerateSpotnanaTokenParams.ts b/src/libs/API/parameters/GenerateSpotnanaTokenParams.ts new file mode 100644 index 000000000000..bc67e9197502 --- /dev/null +++ b/src/libs/API/parameters/GenerateSpotnanaTokenParams.ts @@ -0,0 +1,5 @@ +type GenerateSpotnanaTokenParams = { + policyID: string; +}; + +export default GenerateSpotnanaTokenParams; diff --git a/src/libs/API/parameters/UpdateSubscriptionSizeParams.ts b/src/libs/API/parameters/UpdateSubscriptionSizeParams.ts new file mode 100644 index 000000000000..17382a7265e6 --- /dev/null +++ b/src/libs/API/parameters/UpdateSubscriptionSizeParams.ts @@ -0,0 +1,5 @@ +type UpdateSubscriptionSizeParams = { + userCount: number; +}; + +export default UpdateSubscriptionSizeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 52e76b842f38..306f2c599a4b 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -227,3 +227,5 @@ export type {default as PayInvoiceParams} from './PayInvoiceParams'; export type {default as MarkAsCashParams} from './MarkAsCashParams'; export type {default as UpdateSubscriptionTypeParams} from './UpdateSubscriptionTypeParams'; export type {default as SignUpUserParams} from './SignUpUserParams'; +export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; +export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index b6ae0ab23f7c..68aca761c5b6 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -224,6 +224,7 @@ const WRITE_COMMANDS = { MARK_AS_CASH: 'MarkAsCash', UPDATE_SUBSCRIPTION_TYPE: 'UpdateSubscriptionType', SIGN_UP_USER: 'SignUpUser', + UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', } as const; type WriteCommand = ValueOf; @@ -448,6 +449,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_TYPE]: Parameters.UpdateSubscriptionTypeParams; [WRITE_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams; + [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; }; const READ_COMMANDS = { @@ -552,6 +554,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { GET_MISSING_ONYX_MESSAGES: 'GetMissingOnyxMessages', JOIN_POLICY_VIA_INVITE_LINK: 'JoinWorkspaceViaInviteLink', RECONNECT_APP: 'ReconnectApp', + GENERATE_SPOTNANA_TOKEN: 'GenerateSpotnanaToken', } as const; type SideEffectRequestCommand = ValueOf; @@ -564,6 +567,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES]: Parameters.GetMissingOnyxMessagesParams; [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; + [SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 7880e36a52e2..fc42ac54a2b4 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -651,7 +651,7 @@ const getDayValidationErrorKey = (inputDate: Date): string => { } if (isAfter(startOfDay(new Date()), startOfDay(inputDate))) { - return 'common.error.invalidDateShouldBeFuture'; + return Localize.translateLocal('common.error.invalidDateShouldBeFuture'); } return ''; }; @@ -665,7 +665,7 @@ const getDayValidationErrorKey = (inputDate: Date): string => { const getTimeValidationErrorKey = (inputTime: Date): string => { const timeNowPlusOneMinute = addMinutes(new Date(), 1); if (isBefore(inputTime, timeNowPlusOneMinute)) { - return 'common.error.invalidTimeShouldBeFuture'; + return Localize.translateLocal('common.error.invalidTimeShouldBeFuture'); } return ''; }; @@ -789,6 +789,11 @@ function getFormattedTransportDate(date: Date): string { return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; } +function doesDateBelongToAPastYear(date: string): boolean { + const transactionYear = new Date(date).getFullYear(); + return transactionYear !== new Date().getFullYear(); +} + const DateUtils = { formatToDayOfWeek, formatToLongDateWithWeekday, @@ -831,6 +836,7 @@ const DateUtils = { getFormattedDateRange, getFormattedReservationRangeDate, getFormattedTransportDate, + doesDateBelongToAPastYear, }; export default DateUtils; diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 5aa999267ead..7a0259de7eef 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -1,6 +1,7 @@ import Config from '../../../tests/e2e/config'; import Routes from '../../../tests/e2e/server/routes'; import type {NetworkCacheMap, TestConfig, TestResult} from './types'; +import {waitForActiveRequestsToBeEmpty} from './utils/NetworkInterceptor'; type NativeCommandPayload = { text: string; @@ -57,7 +58,7 @@ const submitTestResults = (testResult: TestResult): Promise => { }); }; -const submitTestDone = () => fetch(`${SERVER_ADDRESS}${Routes.testDone}`, defaultRequestInit); +const submitTestDone = () => waitForActiveRequestsToBeEmpty().then(() => fetch(`${SERVER_ADDRESS}${Routes.testDone}`, defaultRequestInit)); let currentActiveTestConfig: TestConfig | null = null; diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index 3a4a48f7db53..ad23afeb0c3b 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -1,4 +1,5 @@ /* eslint-disable @lwc/lwc/no-async-await */ +import {DeviceEventEmitter} from 'react-native'; import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types'; const LOG_TAG = `[E2E][NetworkInterceptor]`; @@ -93,6 +94,33 @@ function hashFetchArgs(args: Parameters) { return `${url}${JSON.stringify(headers)}`; } +let activeRequestsCount = 0; + +const ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT = 'activeRequestsQueueIsEmpty'; + +/** + * Assures that ongoing network requests are empty. **Highly desirable** to call this function before closing the app. + * Otherwise if some requests are persisted - they will be executed on the next app start. And it can lead to a situation + * where we can have `N * M` requests (where `N` is the number of app run per test and `M` is the number of test suites) + * and such big amount of requests can lead to a situation, where first app run (in test suite to cache network requests) + * may be blocked by spinners and lead to unbelievable big time execution, which eventually will be bigger than timeout and + * will lead to a test failure. + */ +function waitForActiveRequestsToBeEmpty(): Promise { + console.debug('Waiting for requests queue to be empty...', activeRequestsCount); + + if (activeRequestsCount === 0) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const subscription = DeviceEventEmitter.addListener(ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT, () => { + subscription.remove(); + resolve(); + }); + }); +} + /** * Install a network interceptor by overwriting the global fetch function: * - Overwrites fetch globally with a custom implementation @@ -145,6 +173,8 @@ export default function installNetworkInterceptor( console.debug('!!! Missed cache hit for url:', url); } + activeRequestsCount++; + return originalFetch(...args) .then(async (res) => { if (networkCache != null) { @@ -166,6 +196,16 @@ export default function installNetworkInterceptor( .then((res) => { console.debug(LOG_TAG, 'Network cache updated!'); return res; + }) + .finally(() => { + console.debug('Active requests count:', activeRequestsCount); + + activeRequestsCount--; + + if (activeRequestsCount === 0) { + DeviceEventEmitter.emit(ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT); + } }); }; } +export {waitForActiveRequestsToBeEmpty}; diff --git a/src/libs/Environment/Environment.ts b/src/libs/Environment/Environment.ts index 204e78aa5458..1f5a391d3b13 100644 --- a/src/libs/Environment/Environment.ts +++ b/src/libs/Environment/Environment.ts @@ -17,6 +17,20 @@ const OLDDOT_ENVIRONMENT_URLS = { [CONST.ENVIRONMENT.ADHOC]: CONST.STAGING_EXPENSIFY_URL, }; +const TRAVELDOT_ENVIRONMENT_URLS: Record = { + [CONST.ENVIRONMENT.DEV]: CONST.STAGING_TRAVEL_DOT_URL, + [CONST.ENVIRONMENT.STAGING]: CONST.STAGING_TRAVEL_DOT_URL, + [CONST.ENVIRONMENT.PRODUCTION]: CONST.TRAVEL_DOT_URL, + [CONST.ENVIRONMENT.ADHOC]: CONST.STAGING_TRAVEL_DOT_URL, +}; + +const SPOTNANA_ENVIRONMENT_TMC_ID: Record = { + [CONST.ENVIRONMENT.DEV]: CONST.STAGING_SPOTNANA_TMC_ID, + [CONST.ENVIRONMENT.STAGING]: CONST.STAGING_SPOTNANA_TMC_ID, + [CONST.ENVIRONMENT.PRODUCTION]: CONST.SPOTNANA_TMC_ID, + [CONST.ENVIRONMENT.ADHOC]: CONST.STAGING_SPOTNANA_TMC_ID, +}; + /** * Are we running the app in development? */ @@ -54,4 +68,12 @@ function getOldDotEnvironmentURL(): Promise { return getEnvironment().then((environment) => OLDDOT_ENVIRONMENT_URLS[environment]); } -export {getEnvironment, isInternalTestBuild, isDevelopment, isProduction, getEnvironmentURL, getOldDotEnvironmentURL}; +function getTravelDotEnvironmentURL(): Promise { + return getEnvironment().then((environment) => TRAVELDOT_ENVIRONMENT_URLS[environment]); +} + +function getSpotnanaEnvironmentTMCID(): Promise { + return getEnvironment().then((environment) => SPOTNANA_ENVIRONMENT_TMC_ID[environment]); +} + +export {getEnvironment, isInternalTestBuild, isDevelopment, isProduction, getEnvironmentURL, getOldDotEnvironmentURL, getTravelDotEnvironmentURL, getSpotnanaEnvironmentTMCID}; diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 8af6b706086e..cf852e533a20 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -38,11 +38,19 @@ function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatO } /** - * Method used to get an error object with microsecond as the key. - * @param error - error key or message to be saved + * Creates an error object with a timestamp (in microseconds) as the key and the translated error message as the value. + * @param error - The translation key for the error message. + */ +function getMicroSecondOnyxErrorWithTranslationKey(error: TranslationPaths, errorKey?: number): Errors { + return {[errorKey ?? DateUtils.getMicroseconds()]: Localize.translateLocal(error)}; +} + +/** + * Creates an error object with a timestamp (in microseconds) as the key and the error message as the value. + * @param error - The error message. */ -function getMicroSecondOnyxError(error: string, isTranslated = false, errorKey?: number): Errors { - return {[errorKey ?? DateUtils.getMicroseconds()]: error && [error, {isTranslated}]}; +function getMicroSecondOnyxErrorWithMessage(error: string, errorKey?: number): Errors { + return {[errorKey ?? DateUtils.getMicroseconds()]: error}; } /** @@ -54,15 +62,15 @@ function getMicroSecondOnyxErrorObject(error: Errors, errorKey?: number): ErrorF } // We can assume that if error is a string, it has already been translated because it is server error -function getErrorMessageWithTranslationData(error: Localize.MaybePhraseKey): Localize.MaybePhraseKey { - return typeof error === 'string' ? [error, {isTranslated: true}] : error; +function getErrorMessageWithTranslationData(error: string | null): string { + return error ?? ''; } type OnyxDataWithErrors = { errors?: Errors | null; }; -function getLatestErrorMessage(onyxData: OnyxEntry): Localize.MaybePhraseKey { +function getLatestErrorMessage(onyxData: OnyxEntry | null): string { const errors = onyxData?.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -70,7 +78,7 @@ function getLatestErrorMessage(onyxData: O } const key = Object.keys(errors).sort().reverse()[0]; - return getErrorMessageWithTranslationData(errors[key]); + return getErrorMessageWithTranslationData(errors[key] ?? ''); } function getLatestErrorMessageField(onyxData: OnyxEntry): Errors { @@ -127,16 +135,16 @@ function getLatestErrorFieldForAnyField(errors: Errors, inputID?: string | null, message?: TKey | Localize.MaybePhraseKey) { +function addErrorMessage(errors: Errors, inputID?: string | null, message?: string | null) { if (!message || !inputID) { return; } const errorList = errors; const error = errorList[inputID]; - const translatedMessage = Localize.translateIfPhraseKey(message); if (!error) { - errorList[inputID] = [translatedMessage, {isTranslated: true}]; + errorList[inputID] = message; } else if (typeof error === 'string') { - errorList[inputID] = [`${error}\n${translatedMessage}`, {isTranslated: true}]; - } else if (Array.isArray(error)) { - error[0] = `${error[0]}\n${translatedMessage}`; + errorList[inputID] = `${error}\n${message}`; } } @@ -193,7 +198,8 @@ export { getLatestErrorFieldForAnyField, getLatestErrorMessage, getLatestErrorMessageField, - getMicroSecondOnyxError, + getMicroSecondOnyxErrorWithTranslationKey, + getMicroSecondOnyxErrorWithMessage, getMicroSecondOnyxErrorObject, isReceiptError, }; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 8c13e1648d88..c9eef3170245 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -9,7 +9,6 @@ import translations from '@src/languages/translations'; import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Locale} from '@src/types/onyx'; -import type {ReceiptError} from '@src/types/onyx/Transaction'; import LocaleListener from './LocaleListener'; import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; @@ -175,34 +174,6 @@ function translateLocal(phrase: TKey, ...variable return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } -type MaybePhraseKey = string | null | [string, Record & {isTranslated?: boolean}] | []; - -/** - * Return translated string for given error. - */ -function translateIfPhraseKey(message: MaybePhraseKey): string; -function translateIfPhraseKey(message: ReceiptError): ReceiptError; -function translateIfPhraseKey(message: MaybePhraseKey | ReceiptError): string | ReceiptError; -function translateIfPhraseKey(message: MaybePhraseKey | ReceiptError): string | ReceiptError { - if (!message || (Array.isArray(message) && message.length === 0)) { - return ''; - } - - try { - // check if error message has a variable parameter - const [phrase, variables] = Array.isArray(message) ? message : [message]; - - // This condition checks if the error is already translated. For example, if there are multiple errors per input, we handle translation in ErrorUtils.addErrorMessage due to the inability to concatenate error keys. - if (variables?.isTranslated) { - return phrase; - } - - return translateLocal(phrase as TranslationPaths, variables as never); - } catch (error) { - return Array.isArray(message) ? message[0] : message; - } -} - function getPreferredListFormat(): Intl.ListFormat { if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) { init(); @@ -254,5 +225,5 @@ function getDevicePreferredLocale(): Locale { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } -export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; -export type {PhraseParameters, Phrase, MaybePhraseKey}; +export {translate, translateLocal, formatList, formatMessageElementList, getDevicePreferredLocale}; +export type {PhraseParameters, Phrase}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 410caf77e3c4..2c8e2c6bfa80 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -30,6 +30,7 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import * as PriorityMode from '@userActions/PriorityMode'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; +import toggleTestToolsModal from '@userActions/TestTool'; import Timing from '@userActions/Timing'; import * as User from '@userActions/User'; import CONFIG from '@src/CONFIG'; @@ -190,6 +191,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const shortcutsOverviewShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUTS; const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH; const chatShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_CHAT; + const debugShortcutConfig = CONST.KEYBOARD_SHORTCUTS.DEBUG; const currentUrl = getCurrentUrl(); const isLoggingInAsNewUser = !!session?.email && SessionUtils.isLoggingInAsNewUser(currentUrl, session.email); // Sign out the current user if we're transitioning with a different user @@ -273,10 +275,21 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie true, ); + const unsubscribeDebugShortcut = KeyboardShortcut.subscribe( + debugShortcutConfig.shortcutKey, + () => { + toggleTestToolsModal(); + }, + debugShortcutConfig.descriptionKey, + debugShortcutConfig.modifiers, + true, + ); + return () => { unsubscribeShortcutsOverviewShortcut(); unsubscribeSearchShortcut(); unsubscribeChatShortcut(); + unsubscribeDebugShortcut(); Session.cleanupSession(); }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 9c17d5da53a5..29a2205b2e37 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -26,7 +26,13 @@ function OnboardingModalNavigator() { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useOnboardingLayout(); const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: (onboarding) => !Array.isArray(onboarding) && (onboarding?.hasCompletedGuidedSetupFlow ?? true), + selector: (onboarding) => { + // onboarding is an array for old accounts and accounts created from olddot + if (Array.isArray(onboarding)) { + return true; + } + return onboarding?.hasCompletedGuidedSetupFlow; + }, }); useEffect(() => { diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx index 29b9b1072d3d..46c2d914c94a 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx @@ -1,4 +1,4 @@ -import type {ParamListBase, StackActionHelpers, StackNavigationState} from '@react-navigation/native'; +import type {ParamListBase, RouteProp, StackActionHelpers, StackNavigationState} from '@react-navigation/native'; import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native'; import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; import {StackView} from '@react-navigation/stack'; @@ -61,20 +61,20 @@ function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) { const {stateToRender, searchRoute} = useMemo(() => { const routes = reduceCentralPaneRoutes(state.routes); - const lastRoute = routes[routes.length - 1]; - const isLastRouteSearchRoute = getTopmostCentralPaneRoute({routes: [lastRoute]} as State)?.name === SCREENS.SEARCH.CENTRAL_PANE; + // On narrow layout, if we are on /search route we want to hide the search central pane route. + if (isSmallScreenWidth) { + const isSearchCentralPane = (route: RouteProp) => getTopmostCentralPaneRoute({routes: [route]} as State)?.name === SCREENS.SEARCH.CENTRAL_PANE; - const firstRoute = routes[0]; - - // On narrow layout, if we are on /search route we want to hide all central pane routes and show only the bottom tab navigator. - if (isSmallScreenWidth && isLastRouteSearchRoute) { + const lastRoute = routes[routes.length - 1]; + const lastSearchCentralPane = isSearchCentralPane(lastRoute) ? lastRoute : undefined; + const filteredRoutes = routes.filter((route) => !isSearchCentralPane(route)); return { stateToRender: { ...state, - index: 0, - routes: [firstRoute], + index: filteredRoutes.length - 1, + routes: filteredRoutes, }, - searchRoute: lastRoute, + searchRoute: lastSearchCentralPane, }; } diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index 7d6d62b9a5aa..6cec46eae144 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -181,7 +181,10 @@ export default function linkTo(navigation: NavigationContainerRef, targetFocusedRoute?.params as Record) + ) { return; } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 07dd45701029..1b4288a9b3a9 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -268,7 +268,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_STATUS_CLEAR_AFTER_TIME, }, [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: { - path: ROUTES.SETTINGS_SUBSCRIPTION_SIZE, + path: ROUTES.SETTINGS_SUBSCRIPTION_SIZE.route, + parse: { + canChangeSize: Number, + }, }, [SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: { path: ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e87f0380a2da..f90a91fe0f19 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -249,6 +249,10 @@ type SettingsNavigatorParamList = { orderWeight: number; tagName: string; }; + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: undefined; + [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: { + canChangeSize: 0 | 1; + }; [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: undefined; [SCREENS.WORKSPACE.TAXES_SETTINGS]: { policyID: string; diff --git a/src/libs/OnyxAwareParser.ts b/src/libs/OnyxAwareParser.ts new file mode 100644 index 000000000000..a202118bdf5f --- /dev/null +++ b/src/libs/OnyxAwareParser.ts @@ -0,0 +1,42 @@ +import {ExpensiMark} from 'expensify-common'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const parser = new ExpensiMark(); + +const reportIDToNameMap: Record = {}; +const accountIDToNameMap: Record = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + callback: (report) => { + if (!report) { + return; + } + + reportIDToNameMap[report.reportID] = report.reportName ?? report.displayName ?? report.reportID; + }, +}); + +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (personalDetailsList) => { + Object.values(personalDetailsList ?? {}).forEach((personalDetails) => { + if (!personalDetails) { + return; + } + + accountIDToNameMap[personalDetails.accountID] = personalDetails.login ?? String(personalDetails.accountID); + }); + }, +}); + +function parseHtmlToMarkdown(html: string, reportIDToName?: Record, accountIDToName?: Record): string { + return parser.htmlToMarkdown(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap}); +} + +function parseHtmlToText(html: string, reportIDToName?: Record, accountIDToName?: Record): string { + return parser.htmlToText(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap}); +} + +export {parseHtmlToMarkdown, parseHtmlToText}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 11a689662ef4..b04d007d0557 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -536,14 +536,14 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage?.IOUTransactionID : null; const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; if (TransactionUtils.hasMissingSmartscanFields(transaction ?? null) && !ReportUtils.isSettled(transaction?.reportID)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) { if (ReportUtils.shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !ReportUtils.isSettled(report?.reportID)) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); } } else if (ReportUtils.hasSmartscanError(Object.values(reportActions ?? {}))) { - reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage'); + reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage'); } // All error objects related to the report. Each object in the sources contains error messages keyed by microtime const errorSources = { diff --git a/src/libs/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts index 084356df7449..7e2c0bb83320 100644 --- a/src/libs/PolicyDistanceRatesUtils.ts +++ b/src/libs/PolicyDistanceRatesUtils.ts @@ -4,6 +4,7 @@ import type ONYXKEYS from '@src/ONYXKEYS'; import type {Rate} from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; import getPermittedDecimalSeparator from './getPermittedDecimalSeparator'; +import * as Localize from './Localize'; import * as MoneyRequestUtils from './MoneyRequestUtils'; import * as NumberUtils from './NumberUtils'; @@ -19,9 +20,9 @@ function validateRateValue(values: FormOnyxValues, currency: stri // Allow one more decimal place for accuracy const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{0,${CurrencyUtils.getCurrencyDecimals(currency) + 1}})?$`, 'i'); if (!rateValueRegex.test(parsedRate) || parsedRate === '') { - errors.rate = 'workspace.reimburse.invalidRateError'; + errors.rate = Localize.translateLocal('workspace.reimburse.invalidRateError'); } else if (NumberUtils.parseFloatAnyLocale(parsedRate) <= 0) { - errors.rate = 'workspace.reimburse.lowRateError'; + errors.rate = Localize.translateLocal('workspace.reimburse.lowRateError'); } return errors; } @@ -30,7 +31,7 @@ function validateTaxClaimableValue(values: FormOnyxValues, r const errors: FormInputErrors = {}; if (rate.rate && Number(values.taxClaimableValue) > rate.rate / 100) { - errors.taxClaimableValue = 'workspace.taxes.error.updateTaxClaimableFailureMessage'; + errors.taxClaimableValue = Localize.translateLocal('workspace.taxes.error.updateTaxClaimableFailureMessage'); } return errors; } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ab951919f8b3..107cbd8676dc 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -641,7 +641,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo } function getLastVisibleAction(reportID: string, actionsToMerge: OnyxCollection | OnyxCollectionInputValue = {}): OnyxEntry { - const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)) as Array; + const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)); const visibleReportActions = Object.values(reportActions ?? {}).filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { @@ -703,7 +703,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry shouldReportActionBeVisible(reportAction, key)) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5cdc9e71c54e..a02b24c20e35 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -74,6 +74,7 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; +import {parseHtmlToText} from './OnyxAwareParser'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; @@ -2567,7 +2568,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry, creat } const report = getReport(transaction?.reportID); return { - created: TransactionUtils.getCreated(transaction, createdDateFormat), + created: TransactionUtils.getFormattedCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), taxCode: TransactionUtils.getTaxCode(transaction), @@ -3085,7 +3086,7 @@ function getModifiedExpenseOriginalMessage( originalMessage.newComment = transactionChanges?.comment; } if ('created' in transactionChanges) { - originalMessage.oldCreated = TransactionUtils.getCreated(oldTransaction); + originalMessage.oldCreated = TransactionUtils.getFormattedCreated(oldTransaction); originalMessage.created = transactionChanges?.created; } if ('merchant' in transactionChanges) { @@ -3238,8 +3239,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry, repo const logins = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs); accountIDs.forEach((id, index) => (accountIDToName[id] = logins[index])); - const parser = new ExpensiMark(); - const textMessage = Str.removeSMSDomain(parser.htmlToText(html, {reportIDToName, accountIDToName})); + const textMessage = Str.removeSMSDomain(parseHtmlToText(html, reportIDToName, accountIDToName)); parsedReportActionMessageCache[key] = textMessage; return textMessage; @@ -3609,8 +3609,7 @@ function getReportDescriptionText(report: Report): string { return ''; } - const parser = new ExpensiMark(); - return parser.htmlToText(report.description); + return parseHtmlToText(report.description); } function getPolicyDescriptionText(policy: OnyxEntry): string { @@ -3618,8 +3617,7 @@ function getPolicyDescriptionText(policy: OnyxEntry): string { return ''; } - const parser = new ExpensiMark(); - return parser.htmlToText(policy.description); + return parseHtmlToText(policy.description); } function buildOptimisticAddCommentReportAction( @@ -3630,7 +3628,6 @@ function buildOptimisticAddCommentReportAction( shouldEscapeText?: boolean, reportID?: string, ): OptimisticReportAction { - const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? '', {shouldEscapeText, reportID}); const isAttachmentOnly = file && !text; const isTextOnly = text && !file; @@ -3642,10 +3639,10 @@ function buildOptimisticAddCommentReportAction( textForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML; } else if (isTextOnly) { htmlForNewComment = commentText; - textForNewComment = parser.htmlToText(htmlForNewComment); + textForNewComment = parseHtmlToText(htmlForNewComment); } else { htmlForNewComment = `${commentText}${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`; - textForNewComment = `${parser.htmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`; + textForNewComment = `${parseHtmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`; } const isAttachment = !text && file !== undefined; @@ -5444,6 +5441,16 @@ function shouldReportBeInOptionList({ if (isSelfDM(report)) { return includeSelfDM; } + const parentReportAction = ReportActionsUtils.getParentReportAction(report); + + // Hide chat threads where the parent message is pending removal + if ( + !isEmptyObject(parentReportAction) && + ReportActionsUtils.isPendingRemove(parentReportAction) && + ReportActionsUtils.isThreadParentMessage(parentReportAction, report?.reportID ?? '') + ) { + return false; + } return true; } @@ -6519,8 +6526,9 @@ function hasUpdatedTotal(report: OnyxInputOrEntry, policy: OnyxInputOrEn const hasPendingTransaction = transactions.some((transaction) => !!transaction.pendingAction); const hasTransactionWithDifferentCurrency = transactions.some((transaction) => transaction.currency !== report.currency); const hasDifferentWorkspaceCurrency = report.pendingFields?.createChat && isExpenseReport(report) && report.currency !== policy?.outputCurrency; + const hasOptimisticHeldExpense = hasHeldExpenses(report.reportID) && report?.unheldTotal === undefined; - return !(hasPendingTransaction && (hasTransactionWithDifferentCurrency || hasDifferentWorkspaceCurrency)) && !(hasHeldExpenses(report.reportID) && report?.unheldTotal === undefined); + return !(hasPendingTransaction && (hasTransactionWithDifferentCurrency || hasDifferentWorkspaceCurrency)) && !hasOptimisticHeldExpense; } /** @@ -6748,7 +6756,9 @@ function getIndicatedMissingPaymentMethod(userWallet: OnyxEntry, rep */ function hasMissingPaymentMethod(userWallet: OnyxEntry, iouReportID: string): boolean { const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`] ?? {}; - return Object.values(reportActions).some((action) => getIndicatedMissingPaymentMethod(userWallet, iouReportID, action) !== undefined); + return Object.values(reportActions) + .filter(Boolean) + .some((action) => getIndicatedMissingPaymentMethod(userWallet, iouReportID, action) !== undefined); } /** @@ -6769,12 +6779,18 @@ function getTripTransactions(tripRoomReportID: string | undefined): Transaction[ return tripTransactionReportIDs.flatMap((reportID) => TransactionUtils.getAllReportTransactions(reportID)); } +function getTripIDFromTransactionParentReport(transactionParentReport: OnyxEntry | undefined | null): string | undefined { + return getReport(transactionParentReport?.parentReportID)?.tripData?.tripID; +} + /** * Checks if report contains actions with errors */ function hasActionsWithErrors(reportID: string): boolean { const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}; - return Object.values(reportActions).some((action) => !isEmptyObject(action.errors)); + return Object.values(reportActions) + .filter(Boolean) + .some((action) => !isEmptyObject(action.errors)); } function isNonAdminOrOwnerOfPolicyExpenseChat(report: OnyxInputOrEntry, policy: OnyxInputOrEntry): boolean { @@ -6876,7 +6892,9 @@ function createDraftTransactionAndNavigateToParticipantSelector(transactionID: s return; } - const linkedTrackedExpenseReportAction = Object.values(reportActions).find((action) => (action.originalMessage as IOUMessage)?.IOUTransactionID === transactionID); + const linkedTrackedExpenseReportAction = Object.values(reportActions) + .filter(Boolean) + .find((action) => (action.originalMessage as IOUMessage)?.IOUTransactionID === transactionID); const {created, amount, currency, merchant, mccGroup} = getTransactionDetails(transaction) ?? {}; const comment = getTransactionCommentObject(transaction); @@ -7210,6 +7228,7 @@ export { updateReportPreview, temporary_getMoneyRequestOptions, getTripTransactions, + getTripIDFromTransactionParentReport, buildOptimisticInvoiceReport, getInvoiceChatByParticipants, shouldShowMerchantColumn, diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index a5d2ac1d9df2..2f90918a78b0 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchAccountDetails, SearchDataTypes, SearchPersonalDetails, SearchTransaction, SearchTypeToItemMap, SectionsType} from '@src/types/onyx/SearchResults'; +import DateUtils from './DateUtils'; import getTopmostCentralPaneRoute from './Navigation/getTopmostCentralPaneRoute'; import navigationRef from './Navigation/navigationRef'; import type {CentralPaneNavigatorParamList, RootStackParamList, State} from './Navigation/types'; @@ -76,9 +77,46 @@ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { }); } +const currentYear = new Date().getFullYear(); + +function isReportListItemType(item: TransactionListItemType | ReportListItemType): item is ReportListItemType { + return 'transactions' in item; +} + +function shouldShowYear(data: TransactionListItemType[] | ReportListItemType[] | OnyxTypes.SearchResults['data']): boolean { + if (Array.isArray(data)) { + return data.some((item: TransactionListItemType | ReportListItemType) => { + if (isReportListItemType(item)) { + // If the item is a ReportListItemType, iterate over its transactions and check them + return item.transactions.some((transaction) => { + const transactionYear = new Date(TransactionUtils.getCreated(transaction)).getFullYear(); + return transactionYear !== currentYear; + }); + } + + const createdYear = new Date(item?.modifiedCreated ? item.modifiedCreated : item?.created || '').getFullYear(); + return createdYear !== currentYear; + }); + } + + for (const [key, transactionItem] of Object.entries(data)) { + if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) { + const item = transactionItem as SearchTransaction; + const date = TransactionUtils.getCreated(item); + + if (DateUtils.doesDateBelongToAPastYear(date)) { + return true; + } + } + } + return false; +} + function getTransactionsSections(data: OnyxTypes.SearchResults['data']): TransactionListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); + const doesDataContainAPastYearTransaction = shouldShowYear(data); + return Object.entries(data) .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) .map(([, transactionItem]) => { @@ -104,6 +142,7 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac shouldShowTag: true, shouldShowTax: true, keyForList: transactionItem.transactionID, + shouldShowYear: doesDataContainAPastYearTransaction, }; }); } @@ -111,6 +150,8 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListItemType[] { const shouldShowMerchant = getShouldShowMerchant(data); + const doesDataContainAPastYearTransaction = shouldShowYear(data); + const reportIDToTransactions: Record = {}; for (const key in data) { if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) { @@ -146,6 +187,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte shouldShowTag: true, shouldShowTax: true, keyForList: transactionItem.transactionID, + shouldShowYear: doesDataContainAPastYearTransaction, }; if (reportIDToTransactions[reportKey]?.transactions) { reportIDToTransactions[reportKey].transactions.push(transaction); @@ -230,5 +272,5 @@ function getSearchParams() { return topmostCentralPaneRoute?.params as CentralPaneNavigatorParamList['Search_Central_Pane']; } -export {getListItem, getQueryHash, getSections, getSortedSections, getShouldShowMerchant, getSearchType, getSearchParams}; +export {getListItem, getQueryHash, getSections, getSortedSections, getShouldShowMerchant, getSearchType, getSearchParams, shouldShowYear}; export type {SearchColumnType, SortOrder}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 820444443d78..c96d3c511320 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -108,7 +108,8 @@ function getOrderedReportIDs( const isHidden = report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const isFocused = report.reportID === currentReportId; const allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) ?? {}; - const hasErrorsOtherThanFailedReceipt = doesReportHaveViolations || Object.values(allReportErrors).some((error) => error?.[0] !== 'report.genericSmartscanFailureMessage'); + const hasErrorsOtherThanFailedReceipt = + doesReportHaveViolations || Object.values(allReportErrors).some((error) => error?.[0] !== Localize.translateLocal('iou.error.genericSmartscanFailureMessage')); const isSystemChat = ReportUtils.isSystemChat(report); const shouldOverrideHidden = hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || report.isPinned; if (isHidden && !shouldOverrideHidden) { diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 0172864141af..cfeb866e572f 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -459,13 +459,16 @@ function getTagForDisplay(transaction: OnyxEntry, tagIndex?: number return getCleanedTagName(getTag(transaction, tagIndex)); } +function getCreated(transaction: OnyxInputOrEntry): string { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; +} + /** * Return the created field from the transaction, return the modifiedCreated if present. */ -function getCreated(transaction: OnyxInputOrEntry, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || ''; - +function getFormattedCreated(transaction: OnyxInputOrEntry, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string { + const created = getCreated(transaction); return DateUtils.formatWithUTCTimeZone(created, dateFormat); } @@ -813,6 +816,7 @@ export { getMerchant, getMCCGroup, getCreated, + getFormattedCreated, getCategory, getBillable, getTag, diff --git a/src/libs/Url.ts b/src/libs/Url.ts index 69894147a242..970e6b3ed195 100644 --- a/src/libs/Url.ts +++ b/src/libs/Url.ts @@ -11,6 +11,13 @@ function addTrailingForwardSlash(url: string): string { return url; } +function addLeadingForwardSlash(url: string): string { + if (!url.startsWith('/')) { + return `/${url}`; + } + return url; +} + /** * Get path from URL string */ @@ -63,4 +70,4 @@ function hasURL(text: string) { return urlPattern.test(text); } -export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL}; +export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam, hasURL, addLeadingForwardSlash}; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 946c92fed19d..c5291a2864d7 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -212,7 +212,6 @@ export { generateAccountID, getAvatar, getAvatarUrl, - getDefaultAvatar, getDefaultAvatarURL, getFullSizeAvatar, getLoginListBrickRoadIndicator, diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 1dc5fa847d72..bad92b8ece4e 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -10,7 +10,7 @@ import type {OnyxFormKey} from '@src/ONYXKEYS'; import type {Report, TaxRates} from '@src/types/onyx'; import * as CardUtils from './CardUtils'; import DateUtils from './DateUtils'; -import type {MaybePhraseKey} from './Localize'; +import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; import {parsePhoneNumber} from './PhoneNumber'; import StringUtils from './StringUtils'; @@ -111,7 +111,7 @@ function getFieldRequiredErrors(values: FormOnyxVal return; } - errors[fieldKey] = 'common.error.fieldRequired'; + errors[fieldKey] = Localize.translateLocal('common.error.fieldRequired'); }); return errors; @@ -190,12 +190,12 @@ function meetsMaximumAgeRequirement(date: string): boolean { /** * Validate that given date is in a specified range of years before now. */ -function getAgeRequirementError(date: string, minimumAge: number, maximumAge: number): MaybePhraseKey { +function getAgeRequirementError(date: string, minimumAge: number, maximumAge: number): string { const currentDate = startOfDay(new Date()); const testDate = parse(date, CONST.DATE.FNS_FORMAT_STRING, currentDate); if (!isValid(testDate)) { - return 'common.error.dateInvalid'; + return Localize.translateLocal('common.error.dateInvalid'); } const maximalDate = subYears(currentDate, minimumAge); @@ -206,10 +206,10 @@ function getAgeRequirementError(date: string, minimumAge: number, maximumAge: nu } if (isSameDay(testDate, maximalDate) || isAfter(testDate, maximalDate)) { - return ['privatePersonalDetails.error.dateShouldBeBefore', {dateString: format(maximalDate, CONST.DATE.FNS_FORMAT_STRING)}]; + return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeBefore', {dateString: format(maximalDate, CONST.DATE.FNS_FORMAT_STRING)}); } - return ['privatePersonalDetails.error.dateShouldBeAfter', {dateString: format(minimalDate, CONST.DATE.FNS_FORMAT_STRING)}]; + return Localize.translateLocal('privatePersonalDetails.error.dateShouldBeAfter', {dateString: format(minimalDate, CONST.DATE.FNS_FORMAT_STRING)}); } /** @@ -221,14 +221,14 @@ function getDatePassedError(inputDate: string): string { // If input date is not valid, return an error if (!isValid(parsedDate)) { - return 'common.error.dateInvalid'; + return Localize.translateLocal('common.error.dateInvalid'); } // Clear time for currentDate so comparison is based solely on the date currentDate.setHours(0, 0, 0, 0); if (parsedDate < currentDate) { - return 'common.error.dateInvalid'; + return Localize.translateLocal('common.error.dateInvalid'); } return ''; @@ -484,7 +484,7 @@ function isExistingTaxName(taxName: string, taxRates: TaxRates): boolean { */ function isValidSubscriptionSize(subscriptionSize: string): boolean { const parsedSubscriptionSize = Number(subscriptionSize); - return !Number.isNaN(parsedSubscriptionSize) && parsedSubscriptionSize > 0 && parsedSubscriptionSize <= CONST.SUBSCRIPTION_SIZE_LIMIT; + return !Number.isNaN(parsedSubscriptionSize) && parsedSubscriptionSize > 0 && parsedSubscriptionSize <= CONST.SUBSCRIPTION_SIZE_LIMIT && Number.isInteger(parsedSubscriptionSize); } export { diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6872e1bf560f..5b2b3d617e58 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -13,6 +13,7 @@ import type { } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; +import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -161,7 +162,7 @@ function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: { isLoading: false, - errors: ErrorUtils.getMicroSecondOnyxError('walletPage.addBankAccountFailure'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('walletPage.addBankAccountFailure'), }, }, ], @@ -244,7 +245,7 @@ function addPersonalBankAccount(account: PlaidBankAccount) { key: ONYXKEYS.PERSONAL_BANK_ACCOUNT, value: { isLoading: false, - errors: ErrorUtils.getMicroSecondOnyxError('walletPage.addBankAccountFailure'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('walletPage.addBankAccountFailure'), }, }, ], @@ -537,7 +538,7 @@ function validatePlaidSelection(values: FormOnyxValues): Form const errorFields: FormInputErrors = {}; if (!values.selectedPlaidAccountID) { - errorFields.selectedPlaidAccountID = 'bankAccount.error.youNeedToSelectAnOption'; + errorFields.selectedPlaidAccountID = Localize.translateLocal('bankAccount.error.youNeedToSelectAnOption'); } return errorFields; diff --git a/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts b/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts index 7111cab41ad1..b31fbd6dea8b 100644 --- a/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts +++ b/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts @@ -1,3 +1,4 @@ -import {Device} from 'expensify-common'; +// Don't import this file with '* as Device'. It's known to make VSCode IntelliSense crash. +import {getOSAndName} from 'expensify-common/dist/Device'; -export default Device.getOSAndName; +export default getOSAndName; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7ea9d02a14c8..bbc377b780d0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -444,7 +444,7 @@ function updateDistanceRequestRate(transactionID: string, rateID: string, policy /** Helper function to get the receipt error for expenses, or the generic error if there's no receipt */ function getReceiptError(receipt?: Receipt, filename?: string, isScanRequest = true, errorKey?: number): Errors | ErrorFields { return isEmptyObject(receipt) || !isScanRequest - ? ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage', false, errorKey) + ? ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage', errorKey) : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source?.toString() ?? '', filename: filename ?? ''}, errorKey); } @@ -741,7 +741,7 @@ function buildOnyxDataForMoneyRequest( ...(isNewChatReport ? { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, } : {}), @@ -753,7 +753,7 @@ function buildOnyxDataForMoneyRequest( value: { pendingFields: null, errorFields: { - ...(shouldCreateNewMoneyRequestReport ? {createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage')} : {}), + ...(shouldCreateNewMoneyRequestReport ? {createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')} : {}), }, }, }, @@ -765,7 +765,7 @@ function buildOnyxDataForMoneyRequest( errorFields: existingTransactionThreadReport ? null : { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -792,7 +792,7 @@ function buildOnyxDataForMoneyRequest( errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest, errorKey), }, [iouAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, } : { @@ -812,7 +812,7 @@ function buildOnyxDataForMoneyRequest( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }); @@ -1081,7 +1081,7 @@ function buildOnyxDataForInvoice( ...(isNewChatReport ? { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, } : {}), @@ -1093,7 +1093,7 @@ function buildOnyxDataForInvoice( value: { pendingFields: null, errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -1102,7 +1102,7 @@ function buildOnyxDataForInvoice( key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, value: { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -1110,7 +1110,7 @@ function buildOnyxDataForInvoice( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, value: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateInvoiceFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage'), pendingAction: null, pendingFields: clearedPendingFields, }, @@ -1125,7 +1125,7 @@ function buildOnyxDataForInvoice( errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, false, errorKey), }, [iouAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateInvoiceFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage'), }, }, }, @@ -1134,7 +1134,7 @@ function buildOnyxDataForInvoice( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateInvoiceFailureMessage', false, errorKey), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage', errorKey), }, }, }, @@ -1430,7 +1430,7 @@ function buildOnyxDataForTrackExpense( value: { pendingFields: null, errorFields: { - ...(shouldCreateNewMoneyRequestReport ? {createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage')} : {}), + ...(shouldCreateNewMoneyRequestReport ? {createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')} : {}), }, }, }, @@ -1446,7 +1446,7 @@ function buildOnyxDataForTrackExpense( errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), }, [iouAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, } : { @@ -1491,7 +1491,7 @@ function buildOnyxDataForTrackExpense( errorFields: existingTransactionThreadReport ? null : { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -1511,7 +1511,7 @@ function buildOnyxDataForTrackExpense( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }, @@ -1705,7 +1705,7 @@ function getDeleteTrackExpenseInformation( [reportAction.reportActionID]: { ...reportAction, pendingAction: null, - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericDeleteFailureMessage'), }, }, }, @@ -2547,7 +2547,7 @@ function getUpdateMoneyRequestParams( value: { [updatedReportAction.reportActionID]: { ...(updatedReportAction as OnyxTypes.ReportAction), - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage'), }, }, }); @@ -2832,7 +2832,7 @@ function getUpdateTrackExpenseParams( value: { [updatedReportAction.reportActionID]: { ...(updatedReportAction as OnyxTypes.ReportAction), - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage'), }, }, }); @@ -3160,7 +3160,7 @@ const getConvertTrackedExpenseInformation = ( value: { [modifiedExpenseReportAction.reportActionID]: { ...(modifiedExpenseReportAction as OnyxTypes.ReportAction), - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage'), }, }, }); @@ -3954,7 +3954,7 @@ function createSplitsAndOnyxData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`, value: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, ]; @@ -3965,7 +3965,7 @@ function createSplitsAndOnyxData( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`, value: { [splitIOUReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }); @@ -3976,7 +3976,7 @@ function createSplitsAndOnyxData( key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`, value: { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -3985,7 +3985,7 @@ function createSplitsAndOnyxData( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`, value: { [splitIOUReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }, @@ -4519,7 +4519,7 @@ function startSplitBill({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`, value: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, ]; @@ -4541,7 +4541,7 @@ function startSplitBill({ key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`, value: { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -4550,7 +4550,7 @@ function startSplitBill({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`, value: { [splitChatCreatedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, [splitIOUReportAction.reportActionID]: { errors: getReceiptError(receipt, filename), @@ -4727,7 +4727,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, value: { ...unmodifiedTransaction, - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, { @@ -4736,7 +4736,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA value: { [reportAction.reportActionID]: { ...reportAction, - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }, @@ -5107,7 +5107,7 @@ function editRegularMoneyRequest( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, value: { [updatedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage'), }, }, }, @@ -5477,7 +5477,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor ...reportAction, pendingAction: null, errors: { - [errorKey]: ['iou.error.genericDeleteFailureMessage', {isTranslated: false}], + [errorKey]: Localize.translateLocal('iou.error.genericDeleteFailureMessage'), }, }, }, @@ -5501,7 +5501,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor ...reportPreviewAction, pendingAction: null, errors: { - [errorKey]: ['iou.error.genericDeleteFailureMessage', {isTranslated: false}], + [errorKey]: Localize.translateLocal('iou.error.genericDeleteFailureMessage'), }, }, }, @@ -5788,7 +5788,7 @@ function getSendMoneyParams( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), }, }, { @@ -5796,7 +5796,7 @@ function getSendMoneyParams( key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, value: { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -5805,7 +5805,7 @@ function getSendMoneyParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, value: { [optimisticCreatedActionForTransactionThread.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }, @@ -5824,7 +5824,7 @@ function getSendMoneyParams( key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, value: { errorFields: { - createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + createChat: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), }, }, }, @@ -5833,7 +5833,7 @@ function getSendMoneyParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`, value: { [optimisticIOUReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, }, @@ -5849,7 +5849,7 @@ function getSendMoneyParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticIOUReport.reportID}`, value: { [optimisticIOUReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), }, }, }); @@ -6002,7 +6002,7 @@ function getPayMoneyRequestParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { [optimisticIOUReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), }, }, }, @@ -6252,7 +6252,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), }, }, }, @@ -6406,7 +6406,7 @@ function submitReport(expenseReport: OnyxTypes.Report) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { [optimisticSubmittedReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), }, }, }); @@ -6494,7 +6494,7 @@ function cancelPayment(expenseReport: OnyxTypes.Report, chatReport: OnyxTypes.Re key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { [optimisticReportAction.reportActionID ?? '-1']: { - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), }, }, }, @@ -6585,7 +6585,7 @@ function detachReceipt(transactionID: string) { key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, value: { ...(transaction ?? null), - errors: ErrorUtils.getMicroSecondOnyxError('iou.error.receiptDeleteFailureError'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.receiptDeleteFailureError'), }, }, ]; @@ -6823,7 +6823,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string) { comment: { hold: null, }, - errors: ErrorUtils.getMicroSecondOnyxError('iou.genericHoldExpenseFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericHoldExpenseFailureMessage'), }, }, ]; @@ -6885,7 +6885,7 @@ function unholdRequest(transactionID: string, reportID: string) { key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, value: { pendingAction: null, - errors: ErrorUtils.getMicroSecondOnyxError('iou.genericUnholdExpenseFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericUnholdExpenseFailureMessage'), }, }, ]; diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 7e37cf159bad..f97c4e17a9a5 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import * as API from '@libs/API'; +import type {GenerateSpotnanaTokenParams} from '@libs/API/parameters'; import {SIDE_EFFECT_REQUEST_COMMANDS} from '@libs/API/types'; import asyncOpenURL from '@libs/asyncOpenURL'; import * as Environment from '@libs/Environment/Environment'; @@ -64,6 +66,40 @@ function openOldDotLink(url: string) { ); } +function buildTravelDotURL(spotnanaToken?: string, postLoginPath?: string): Promise { + return Promise.all([Environment.getTravelDotEnvironmentURL(), Environment.getSpotnanaEnvironmentTMCID()]).then(([environmentURL, tmcID]) => { + const authCode = spotnanaToken ? `authCode=${spotnanaToken}` : ''; + const redirectURL = postLoginPath ? `redirectUrl=${Url.addLeadingForwardSlash(postLoginPath)}` : ''; + const tmcIDParam = `tmcId=${tmcID}`; + + const paramsArray = [authCode, tmcIDParam, redirectURL]; + const params = paramsArray.filter(Boolean).join('&'); + const travelDotDomain = Url.addTrailingForwardSlash(environmentURL); + return `${travelDotDomain}auth/code?${params}`; + }); +} + +/** + * @param postLoginPath When provided, we will redirect the user to this path post login on travelDot. eg: 'trips/:tripID' + */ +function openTravelDotLink(policyID: OnyxEntry, postLoginPath?: string) { + if (policyID === null || policyID === undefined) { + return; + } + + const parameters: GenerateSpotnanaTokenParams = { + policyID, + }; + + asyncOpenURL( + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN, parameters, {}) + .then((response) => (response?.spotnanaToken ? buildTravelDotURL(response.spotnanaToken, postLoginPath) : buildTravelDotURL())) + .catch(() => buildTravelDotURL()), + (travelDotURL) => travelDotURL, + ); +} + function getInternalNewExpensifyPath(href: string) { const attrPath = Url.getPathFromURL(href); return (Url.hasSameExpensifyOrigin(href, CONST.NEW_EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONST.STAGING_NEW_EXPENSIFY_URL) || href.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) && @@ -121,4 +157,4 @@ function openLink(href: string, environmentURL: string, isAttachment = false) { openExternalLink(href); } -export {buildOldDotURL, openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath}; +export {buildOldDotURL, openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath, openTravelDotLink}; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index fc90b936e32e..d639c68216df 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -84,7 +84,7 @@ function buildOptimisticPolicyCategories(policyID: string, categories: readonly const failureCategoryMap = categories.reduce>>((acc, category) => { acc[category] = { - errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.createFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.createFailureMessage'), pendingAction: null, }; return acc; @@ -202,7 +202,7 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor acc[key] = { ...policyCategories[key], ...categoriesToUpdate[key], - errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.updateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateFailureMessage'), pendingFields: { enabled: null, }, @@ -315,7 +315,7 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string [policyCategory.oldName]: { ...policyCategoryToUpdate, name: policyCategory.oldName, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.updateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateFailureMessage'), pendingAction: null, }, }, @@ -368,7 +368,7 @@ function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolea key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { requiresCategory: !requiresCategory, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.updateFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateFailureMessage'), pendingFields: { requiresCategory: null, }, @@ -434,7 +434,7 @@ function deleteWorkspaceCategories(policyID: string, categoryNamesToDelete: stri value: categoryNamesToDelete.reduce>>((acc, categoryName) => { acc[categoryName] = { pendingAction: null, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.categories.deleteFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.deleteFailureMessage'), }; return acc; }, {}), @@ -567,7 +567,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn customUnits: { [currentCustomUnit.customUnitID]: { ...currentCustomUnit, - errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {defaultCategory: null}, }, }, diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts index 48405a2aa174..d22299e9e2fc 100644 --- a/src/libs/actions/Policy/DistanceRate.ts +++ b/src/libs/actions/Policy/DistanceRate.ts @@ -182,7 +182,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom [customUnitID]: { rates: { [customUnitRate.customUnitRateID ?? '']: { - errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, }, @@ -288,7 +288,7 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU customUnits: { [currentCustomUnit.customUnitID]: { ...currentCustomUnit, - errorFields: {attributes: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {attributes: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {attributes: null}, }, }, @@ -319,7 +319,7 @@ function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, failureRates[rateID] = { ...currentRates[rateID], pendingFields: {rate: null}, - errorFields: {rate: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {rate: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }; } } @@ -390,7 +390,7 @@ function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, failureRates[rateID] = { ...currentRates[rateID], pendingFields: {enabled: null}, - errorFields: {enabled: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {enabled: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }; } } @@ -461,7 +461,7 @@ function deletePolicyDistanceRates(policyID: string, customUnit: CustomUnit, rat failureRates[rateID] = { ...currentRates[rateID], pendingAction: null, - errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }; } else { optimisticRates[rateID] = currentRates[rateID]; @@ -535,7 +535,7 @@ function updateDistanceTaxClaimableValue(policyID: string, customUnit: CustomUni failureRates[rateID] = { ...currentRates[rateID], pendingFields: {taxClaimablePercentage: null}, - errorFields: {taxClaimablePercentage: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {taxClaimablePercentage: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }; } } @@ -606,7 +606,7 @@ function updateDistanceTaxRate(policyID: string, customUnit: CustomUnit, customU failureRates[rateID] = { ...currentRates[rateID], pendingFields: {taxRateExternalID: null}, - errorFields: {taxRateExternalID: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {taxRateExternalID: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }; } } diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index fc28a01b043c..e1c48e4d0522 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -238,7 +238,7 @@ function removeMembers(accountIDs: number[], policyID: string) { emailList.forEach((email) => { optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}; successMembersState[email] = null; - failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove')}; + failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove')}; }); const optimisticData: OnyxUpdate[] = [ @@ -414,7 +414,7 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { employeeList: previousEmployeeList, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.editor.genericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), }, }, ]; @@ -537,7 +537,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.POLICY.ROLE.USER}; successMembersState[email] = {pendingAction: null}; failureMembersState[email] = { - errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericAdd'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd'), }; }); @@ -617,7 +617,7 @@ function inviteMemberToWorkspace(policyID: string, inviterEmail: string) { { onyxMethod: Onyx.METHOD.MERGE, key: memberJoinKey, - value: {...failureMembersState, errors: ErrorUtils.getMicroSecondOnyxError('common.genericEditFailureMessage')}, + value: {...failureMembersState, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage')}, }, ]; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index a6162051e6fd..eb190d9b1e67 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -366,7 +366,7 @@ function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency }, autoReportingFrequency: policy.autoReportingFrequency ?? null, pendingFields: {autoReporting: null}, - errorFields: {autoReporting: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.autoReportingErrorMessage')}, + errorFields: {autoReporting: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsDelayedSubmissionPage.autoReportingErrorMessage')}, }, }, ]; @@ -407,7 +407,7 @@ function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf value: { autoReportingFrequency: policy.autoReportingFrequency ?? null, pendingFields: {autoReportingFrequency: null}, - errorFields: {autoReportingFrequency: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.autoReportingFrequencyErrorMessage')}, + errorFields: {autoReportingFrequency: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsDelayedSubmissionPage.autoReportingFrequencyErrorMessage')}, }, }, ]; @@ -448,7 +448,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingO value: { autoReportingOffset: policy.autoReportingOffset ?? null, pendingFields: {autoReportingOffset: null}, - errorFields: {autoReportingOffset: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.monthlyOffsetErrorMessage')}, + errorFields: {autoReportingOffset: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsDelayedSubmissionPage.monthlyOffsetErrorMessage')}, }, }, ]; @@ -494,7 +494,7 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo approver: policy.approver ?? null, approvalMode: policy.approvalMode ?? null, pendingFields: {approvalMode: null}, - errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxError('workflowsApprovalPage.genericErrorMessage')}, + errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsApprovalPage.genericErrorMessage')}, }, }, ]; @@ -552,7 +552,7 @@ function setWorkspacePayer(policyID: string, reimburserEmail: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { achAccount: {reimburser: policy.achAccount?.reimburser ?? null}, - errorFields: {reimburser: ErrorUtils.getMicroSecondOnyxError('workflowsPayerPage.genericErrorMessage')}, + errorFields: {reimburser: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsPayerPage.genericErrorMessage')}, pendingFields: {reimburser: null}, }, }, @@ -612,7 +612,7 @@ function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueO isLoadingWorkspaceReimbursement: false, reimbursementChoice: policy.reimbursementChoice ?? null, achAccount: {reimburser: policy.achAccount?.reimburser ?? null}, - errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {reimbursementChoice: null}, }, }, @@ -666,7 +666,7 @@ function leaveWorkspace(policyID: string) { pendingAction: policy?.pendingAction, employeeList: { [sessionEmail]: { - errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove'), }, }, }, @@ -862,6 +862,9 @@ function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: I }, isOptimisticReport: false, pendingChatMembers: null, + participants: { + [accountID]: allPersonalDetails && allPersonalDetails[accountID] ? {} : null, + }, }, }); workspaceMembersChats.onyxSuccessData.push({ @@ -966,7 +969,7 @@ function deleteWorkspaceAvatar(policyID: string) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { errorFields: { - avatarURL: ErrorUtils.getMicroSecondOnyxError('avatarWithImagePicker.deleteWorkspaceError'), + avatarURL: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('avatarWithImagePicker.deleteWorkspaceError'), }, }, }, @@ -1025,7 +1028,7 @@ function updateGeneralSettings(policyID: string, name: string, currencyValue?: s failureRates[rateID] = { ...currentRates[rateID], pendingFields: {currency: null}, - errorFields: {currency: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {currency: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }; } } @@ -1086,7 +1089,7 @@ function updateGeneralSettings(policyID: string, name: string, currencyValue?: s key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { errorFields: { - generalSettings: ErrorUtils.getMicroSecondOnyxError('workspace.editor.genericFailureMessage'), + generalSettings: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), }, ...(customUnitID && { customUnits: { @@ -1151,7 +1154,7 @@ function updateWorkspaceDescription(policyID: string, description: string, curre key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { errorFields: { - description: ErrorUtils.getMicroSecondOnyxError('workspace.editor.genericFailureMessage'), + description: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), }, }, }, @@ -1300,7 +1303,7 @@ function updateWorkspaceCustomUnitAndRate(policyID: string, currentCustomUnit: C rates: { [newCustomUnit.rates.customUnitRateID]: { ...currentCustomUnit.rates, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.reimburse.updateCustomUnitError'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.reimburse.updateCustomUnitError'), }, }, }, @@ -2815,7 +2818,7 @@ function setPolicyCustomTaxName(policyID: string, customTaxName: string) { taxRates: { name: originalCustomTaxName, pendingFields: {name: null}, - errorFields: {name: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {name: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }, }, }, @@ -2867,7 +2870,7 @@ function setWorkspaceCurrencyDefault(policyID: string, taxCode: string) { taxRates: { defaultExternalID: originalDefaultExternalID, pendingFields: {defaultExternalID: null}, - errorFields: {defaultExternalID: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {defaultExternalID: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }, }, }, @@ -2919,7 +2922,7 @@ function setForeignCurrencyDefault(policyID: string, taxCode: string) { taxRates: { foreignTaxDefault: originalDefaultForeignCurrencyID, pendingFields: {foreignTaxDefault: null}, - errorFields: {foreignTaxDefault: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + errorFields: {foreignTaxDefault: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, }, }, }, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 7e2fb68ab125..1e5a4650eb1a 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -160,7 +160,7 @@ function createPolicyTag(policyID: string, tagName: string) { [policyTag.name]: { tags: { [newTagName]: { - errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), pendingAction: null, }, }, @@ -243,7 +243,7 @@ function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record>>>((acc, tagName) => { - acc[tagName] = {pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.deleteFailureMessage')}; + acc[tagName] = {pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.deleteFailureMessage')}; return acc; }, {}), }, @@ -437,7 +437,7 @@ function renamePolicyTag(policyID: string, policyTag: {oldName: string; newName: pendingFields: { name: null, }, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), }, }, }, @@ -557,7 +557,7 @@ function renamePolicyTaglist(policyID: string, policyTagListName: {oldName: stri value: { errors: { [oldName]: oldName, - [newName]: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + [newName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), }, [newName]: null, [oldName]: oldPolicyTags, @@ -610,7 +610,7 @@ function setPolicyRequiresTag(policyID: string, requiresTag: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { requiresTag: !requiresTag, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), pendingFields: { requiresTag: null, }, @@ -668,7 +668,7 @@ function setPolicyTagsRequired(policyID: string, requiresTag: boolean, tagListIn required: policyTag.required, pendingFields: {required: null}, errorFields: { - required: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + required: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.tags.genericFailureMessage'), }, }, }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index c8d2e2875d1e..1d42ac2d50c0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -61,6 +61,7 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; +import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; @@ -558,7 +559,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { failureReportActions[actionKey] = { // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style ...(action as OptimisticAddCommentReportAction), - errors: ErrorUtils.getMicroSecondOnyxError('report.genericAddCommentFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'), }; }); @@ -1476,14 +1477,15 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry { } // `Onyx.clear` reinitializes the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` - Onyx.merge(ONYXKEYS.SESSION, {errors: ErrorUtils.getMicroSecondOnyxError(errorMessage)}); + Onyx.merge(ONYXKEYS.SESSION, {errors: ErrorUtils.getMicroSecondOnyxErrorWithMessage(errorMessage)}); }); } diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index c5623d63db57..eb5dd131c66b 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -6,6 +6,7 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import type {SubscriptionType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxData} from '@src/types/onyx/Request'; /** * Fetches data when the user opens the SubscriptionSettingsPage @@ -61,4 +62,61 @@ function updateSubscriptionType(type: SubscriptionType) { }); } -export {openSubscriptionPage, updateSubscriptionType}; +function updateSubscriptionSize(newSubscriptionSize: number, currentSubscriptionSize: number) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, + value: { + userCount: newSubscriptionSize, + errorFields: { + userCount: null, + }, + pendingFields: { + userCount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, + value: { + userCount: newSubscriptionSize, + errorFields: { + userCount: null, + }, + pendingFields: { + userCount: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, + value: { + userCount: currentSubscriptionSize, + pendingFields: { + userCount: null, + }, + }, + }, + ], + }; + + API.write(WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE, {userCount: newSubscriptionSize}, onyxData); +} + +function clearUpdateSubscriptionSizeError() { + Onyx.merge(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, { + errorFields: { + userCount: null, + }, + }); +} + +export {openSubscriptionPage, updateSubscriptionType, updateSubscriptionSize, clearUpdateSubscriptionSizeError}; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c8f8bb17dda6..51fcbb53bc5f 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -269,7 +269,7 @@ function createTaskAndNavigate( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, value: { [optimisticAddCommentReport.reportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('task.genericCreateTaskFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('task.genericCreateTaskFailureMessage'), }, }, }); @@ -347,7 +347,7 @@ function completeTask(taskReport: OnyxEntry) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`, value: { [completedTaskReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('task.messages.error'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('task.messages.error'), }, }, }, @@ -416,7 +416,7 @@ function reopenTask(taskReport: OnyxEntry) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`, value: { [reopenedTaskReportAction.reportActionID]: { - errors: ErrorUtils.getMicroSecondOnyxError('task.messages.error'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('task.messages.error'), }, }, }, diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 99ce116d1361..4792304cb9ce 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -4,6 +4,7 @@ import type {FormOnyxValues} from '@components/Form/types'; import * as API from '@libs/API'; import type {CreatePolicyTaxParams, DeletePolicyTaxesParams, RenamePolicyTaxParams, SetPolicyTaxesEnabledParams, UpdatePolicyTaxValueParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; +import {translateLocal} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import * as ErrorUtils from '@src/libs/ErrorUtils'; @@ -39,7 +40,7 @@ const validateTaxName = (policy: Policy, values: FormOnyxValues((acc, taxID) => { acc[taxID] = { pendingAction: null, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.deleteFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.taxes.error.deleteFailureMessage'), }; return acc; }, {}), @@ -380,7 +381,7 @@ function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) value: originalTaxRate.value, pendingFields: {value: null}, pendingAction: null, - errorFields: {value: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.updateFailureMessage')}, + errorFields: {value: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.taxes.error.updateFailureMessage')}, }, }, }, @@ -444,7 +445,7 @@ function renamePolicyTax(policyID: string, taxID: string, newName: string) { name: originalTaxRate.name, pendingFields: {name: null}, pendingAction: null, - errorFields: {name: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.updateFailureMessage')}, + errorFields: {name: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.taxes.error.updateFailureMessage')}, }, }, }, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index c70eb73ec2a5..eb3d56158b0c 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -157,7 +157,7 @@ function requestContactMethodValidateCode(contactMethod: string) { [contactMethod]: { validateCodeSent: false, errorFields: { - validateCodeSent: ErrorUtils.getMicroSecondOnyxError('contacts.genericFailureMessages.requestContactMethodValidateCode'), + validateCodeSent: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('contacts.genericFailureMessages.requestContactMethodValidateCode'), }, pendingFields: { validateCodeSent: null, @@ -240,7 +240,7 @@ function deleteContactMethod(contactMethod: string, loginList: Record [settingName, null])), - errorFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')])), + errorFields: Object.fromEntries( + Object.keys(configUpdate).map((settingName) => [settingName, ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')]), + ), }, }, }, diff --git a/src/libs/compose.ts b/src/libs/compose.ts deleted file mode 100644 index dadc586d0f0d..000000000000 --- a/src/libs/compose.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable import/export */ - -/** - * This is a utility function taken directly from Redux. (We don't want to add Redux as a dependency) - * It enables functional composition, useful for the chaining/composition of HOCs. - * - * For example, instead of: - * - * export default hoc1(config1, hoc2(config2, hoc3(config3)))(Component); - * - * Use this instead: - * - * export default compose( - * hoc1(config1), - * hoc2(config2), - * hoc3(config3), - * )(Component) - */ -export default function compose(): (a: R) => R; - -export default function compose(f: F): F; - -/* two functions */ -export default function compose(f1: (...args: A) => R1, f2: (a: R1) => R2): (...args: A) => R2; - -/* three functions */ -export default function compose(f1: (...args: A) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (...args: A) => R3; - -/* four functions */ -export default function compose(f1: (...args: A) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (...args: A) => R4; - -/* five functions */ -export default function compose( - f1: (...args: A) => R1, - f2: (a: R1) => R2, - f3: (a: R2) => R3, - f4: (a: R3) => R4, - f5: (a: R4) => R5, -): (...args: A) => R5; - -/* six functions */ -export default function compose( - f1: (...args: A) => R1, - f2: (a: R1) => R2, - f3: (a: R2) => R3, - f4: (a: R3) => R4, - f5: (a: R4) => R5, - f6: (a: R5) => R6, -): (...args: A) => R6; - -/* rest */ -export default function compose(f1: (a: unknown) => R, ...funcs: Function[]): (...args: unknown[]) => R; - -export default function compose(...funcs: Function[]): Function { - if (funcs.length === 0) { - // infer the argument type so it is usable in inference down the line - return (arg: T) => arg; - } - - if (funcs.length === 1) { - return funcs[0]; - } - - return funcs.reduce( - (a, b) => - (...args: unknown[]) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - a(b(...args)), - ); -} diff --git a/src/libs/getSplashBackgroundColor/index.native.ts b/src/libs/getSplashBackgroundColor/index.native.ts deleted file mode 100644 index 1d21b7a004e1..000000000000 --- a/src/libs/getSplashBackgroundColor/index.native.ts +++ /dev/null @@ -1,7 +0,0 @@ -import colors from '@styles/theme/colors'; - -function getSplashBackgroundColor() { - return colors.green400; -} - -export default getSplashBackgroundColor; diff --git a/src/libs/getSplashBackgroundColor/index.ts b/src/libs/getSplashBackgroundColor/index.ts deleted file mode 100644 index 97484d49163e..000000000000 --- a/src/libs/getSplashBackgroundColor/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import colors from '@styles/theme/colors'; - -function getSplashBackgroundColor() { - return colors.productDark100; -} - -export default getSplashBackgroundColor; diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index 0ff04deb09f0..8dc4c06650e5 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -13,7 +13,6 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import type {MaybePhraseKey} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -59,7 +58,7 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa shouldInitialize: isScreenTransitionEnd, }); - const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; + const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const [, debouncedSearchValueInServer, setSearchValueInServer] = useDebouncedState('', 500); diff --git a/src/pages/EditReportFieldDate.tsx b/src/pages/EditReportFieldDate.tsx index e7021f9123d6..38209ba1083b 100644 --- a/src/pages/EditReportFieldDate.tsx +++ b/src/pages/EditReportFieldDate.tsx @@ -36,11 +36,11 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f (value: FormOnyxValues) => { const errors: FormInputErrors = {}; if (isRequired && value[fieldKey].trim() === '') { - errors[fieldKey] = 'common.error.fieldRequired'; + errors[fieldKey] = translate('common.error.fieldRequired'); } return errors; }, - [fieldKey, isRequired], + [fieldKey, isRequired, translate], ); return ( diff --git a/src/pages/EditReportFieldText.tsx b/src/pages/EditReportFieldText.tsx index 0649d59cd2ee..d619eb52b695 100644 --- a/src/pages/EditReportFieldText.tsx +++ b/src/pages/EditReportFieldText.tsx @@ -36,11 +36,11 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f (values: FormOnyxValues) => { const errors: FormInputErrors = {}; if (isRequired && values[fieldKey].trim() === '') { - errors[fieldKey] = 'common.error.fieldRequired'; + errors[fieldKey] = translate('common.error.fieldRequired'); } return errors; }, - [fieldKey, isRequired], + [fieldKey, isRequired, translate], ); return ( diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index 4756db4d43ec..cc41738fa581 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -76,32 +76,32 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO if (values.dob) { if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { - errors.dob = 'bankAccount.error.dob'; + errors.dob = translate('bankAccount.error.dob'); } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { - errors.dob = 'bankAccount.error.age'; + errors.dob = translate('bankAccount.error.age'); } } if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { - errors.addressStreet = 'bankAccount.error.addressStreet'; + errors.addressStreet = translate('bankAccount.error.addressStreet'); } if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { - errors.addressZipCode = 'bankAccount.error.zipCode'; + errors.addressZipCode = translate('bankAccount.error.zipCode'); } if (values.phoneNumber && !ValidationUtils.isValidUSPhone(values.phoneNumber, true)) { - errors.phoneNumber = 'bankAccount.error.phoneNumber'; + errors.phoneNumber = translate('bankAccount.error.phoneNumber'); } // walletAdditionalDetails stores errors returned by the server. If the server returns an SSN error // then the user needs to provide the full 9 digit SSN. if (walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN) { if (values.ssn && !ValidationUtils.isValidSSNFullNine(values.ssn)) { - errors.ssn = 'additionalDetailsStep.ssnFull9Error'; + errors.ssn = translate('additionalDetailsStep.ssnFull9Error'); } } else if (values.ssn && !ValidationUtils.isValidSSNLastFour(values.ssn)) { - errors.ssn = 'bankAccount.error.ssnLast4'; + errors.ssn = translate('bankAccount.error.ssnLast4'); } return errors; diff --git a/src/pages/EnablePayments/FeesAndTerms/substeps/TermsStep.tsx b/src/pages/EnablePayments/FeesAndTerms/substeps/TermsStep.tsx index edd01c35b9a7..7998d9511b0d 100644 --- a/src/pages/EnablePayments/FeesAndTerms/substeps/TermsStep.tsx +++ b/src/pages/EnablePayments/FeesAndTerms/substeps/TermsStep.tsx @@ -49,7 +49,7 @@ function TermsStep({onNext}: SubStepProps) { const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(walletTerms ?? {}) ?? ''; + const errorMessage = error ? translate('common.error.acceptTerms') : ErrorUtils.getLatestErrorMessage(walletTerms ?? {}) ?? ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx index b11fa3e9d725..0f9819f31c66 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -25,20 +25,6 @@ const INPUT_KEYS = { const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.STREET, PERSONAL_INFO_STEP_KEY.CITY, PERSONAL_INFO_STEP_KEY.STATE, PERSONAL_INFO_STEP_KEY.ZIP_CODE]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { - errors.addressStreet = 'bankAccount.error.addressStreet'; - } - - if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { - errors.addressZipCode = 'bankAccount.error.zipCode'; - } - - return errors; -}; - function AddressStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -52,6 +38,23 @@ function AddressStep({onNext, isEditing}: SubStepProps) { zipCode: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', }; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { + errors.addressStreet = translate('bankAccount.error.addressStreet'); + } + + if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { + errors.addressZipCode = translate('bankAccount.error.zipCode'); + } + + return errors; + }, + [translate], + ); + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx index cfd0f4c5e3f7..d476fdcc5c86 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx @@ -1,5 +1,5 @@ import {subYears} from 'date-fns'; -import React from 'react'; +import React, {useCallback} from 'react'; import {useOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; @@ -19,20 +19,6 @@ import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB; const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.dob) { - if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { - errors.dob = 'bankAccount.error.dob'; - } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { - errors.dob = 'bankAccount.error.age'; - } - } - - return errors; -}; - const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); @@ -40,6 +26,23 @@ function DateOfBirthStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.dob) { + if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { + errors.dob = translate('bankAccount.error.dob'); + } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { + errors.dob = translate('bankAccount.error.age'); + } + } + + return errors; + }, + [translate], + ); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); const dobDefaultValue = walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? ''; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index f63d5ef84879..b40fb2202943 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -19,18 +19,6 @@ import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.legalFirstName && !ValidationUtils.isValidLegalName(values.legalFirstName)) { - errors.legalFirstName = 'bankAccount.error.firstName'; - } - - if (values.legalLastName && !ValidationUtils.isValidLegalName(values.legalLastName)) { - errors.legalLastName = 'bankAccount.error.lastName'; - } - return errors; -}; - function FullNameStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -42,6 +30,21 @@ function FullNameStep({onNext, isEditing}: SubStepProps) { lastName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', }; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + if (values.legalFirstName && !ValidationUtils.isValidLegalName(values.legalFirstName)) { + errors.legalFirstName = translate('bankAccount.error.firstName'); + } + + if (values.legalLastName && !ValidationUtils.isValidLegalName(values.legalLastName)) { + errors.legalLastName = translate('bankAccount.error.lastName'); + } + return errors; + }, + [translate], + ); + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx index 86b6a40948fc..60bfa431ca78 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -19,14 +19,6 @@ import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.PHONE_NUMBER]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.phoneNumber && !ValidationUtils.isValidUSPhone(values.phoneNumber, true)) { - errors.phoneNumber = 'bankAccount.error.phoneNumber'; - } - return errors; -}; function PhoneNumberStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -35,6 +27,18 @@ function PhoneNumberStep({onNext, isEditing}: SubStepProps) { const defaultPhoneNumber = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.PHONE_NUMBER] ?? ''; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.phoneNumber && !ValidationUtils.isValidUSPhone(values.phoneNumber, true)) { + errors.phoneNumber = translate('bankAccount.error.phoneNumber'); + } + return errors; + }, + [translate], + ); + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx index 4c366c5a1873..bdaa3fe98f67 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -19,19 +19,23 @@ import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.ssn && !ValidationUtils.isValidSSNLastFour(values.ssn)) { - errors.ssn = 'bankAccount.error.ssnLast4'; - } - - return errors; -}; function SocialSecurityNumberStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.ssn && !ValidationUtils.isValidSSNLastFour(values.ssn)) { + errors.ssn = translate('bankAccount.error.ssnLast4'); + } + + return errors; + }, + [translate], + ); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); const defaultSsnLast4 = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index f5b05998c061..09bf0da163bc 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -58,7 +58,7 @@ function TermsStep(props: TermsStepProps) { const [error, setError] = useState(false); const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) ?? ''; + const errorMessage = error ? translate('common.error.acceptTerms') : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) ?? ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 54e09d51b2b2..d6449bc2cf44 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -96,25 +96,25 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat // First we validate the first name field if (values.firstName.replace(CONST.REGEX.ANY_SPACE, '').length === 0) { - ErrorUtils.addErrorMessage(errors, 'firstName', 'onboarding.error.requiredFirstName'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('onboarding.error.requiredFirstName')); } if (!ValidationUtils.isValidDisplayName(values.firstName)) { - ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('personalDetails.error.hasInvalidCharacter')); } else if (values.firstName.length > CONST.DISPLAY_NAME.MAX_LENGTH) { - ErrorUtils.addErrorMessage(errors, 'firstName', ['common.error.characterLimitExceedCounter', {length: values.firstName.length, limit: CONST.DISPLAY_NAME.MAX_LENGTH}]); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('common.error.characterLimitExceedCounter', {length: values.firstName.length, limit: CONST.DISPLAY_NAME.MAX_LENGTH})); } if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { - ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('personalDetails.error.containsReservedWord')); } // Then we validate the last name field if (!ValidationUtils.isValidDisplayName(values.lastName)) { - ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.hasInvalidCharacter'); + ErrorUtils.addErrorMessage(errors, 'lastName', translate('personalDetails.error.hasInvalidCharacter')); } else if (values.lastName.length > CONST.DISPLAY_NAME.MAX_LENGTH) { - ErrorUtils.addErrorMessage(errors, 'lastName', ['common.error.characterLimitExceedCounter', {length: values.lastName.length, limit: CONST.DISPLAY_NAME.MAX_LENGTH}]); + ErrorUtils.addErrorMessage(errors, 'lastName', translate('common.error.characterLimitExceedCounter', {length: values.lastName.length, limit: CONST.DISPLAY_NAME.MAX_LENGTH})); } if (ValidationUtils.doesContainReservedWord(values.lastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { - ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.containsReservedWord'); + ErrorUtils.addErrorMessage(errors, 'lastName', translate('personalDetails.error.containsReservedWord')); } return errors; diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 5b186ccbe7a9..244b997f6ec6 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -83,7 +83,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS); }, [selectedPurpose]); - const [errorMessage, setErrorMessage] = useState<'onboarding.purpose.errorSelection' | 'onboarding.purpose.errorContinue' | ''>(''); + const [errorMessage, setErrorMessage] = useState(''); const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => { const translationKey = `onboarding.purpose.${choice}` as const; @@ -110,11 +110,11 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on const handleOuterClick = useCallback(() => { if (!selectedPurpose) { - setErrorMessage('onboarding.purpose.errorSelection'); + setErrorMessage(translate('onboarding.purpose.errorSelection')); } else { - setErrorMessage('onboarding.purpose.errorContinue'); + setErrorMessage(translate('onboarding.purpose.errorContinue')); } - }, [selectedPurpose]); + }, [selectedPurpose, setErrorMessage, translate]); const onboardingLocalRef = useRef(null); useImperativeHandle(isFocused ? OnboardingRefManager.ref : onboardingLocalRef, () => ({handleOuterClick}), [handleOuterClick]); @@ -147,7 +147,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on buttonText={translate('common.continue')} onSubmit={() => { if (!selectedPurpose) { - setErrorMessage('onboarding.purpose.errorSelection'); + setErrorMessage(translate('onboarding.purpose.errorSelection')); return; } setErrorMessage(''); diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index b0e01d0c8caa..9b8824300d30 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -59,11 +59,11 @@ function BaseOnboardingWork({shouldUseNativeStyles, onboardingPurposeSelected, o const work = values.work.trim(); if (!ValidationUtils.isRequiredFulfilled(work)) { - errors.work = 'workspace.editor.nameIsRequiredError'; + errors.work = translate('workspace.editor.nameIsRequiredError'); } else if ([...work].length > CONST.TITLE_CHARACTER_LIMIT) { // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 // code units. - ErrorUtils.addErrorMessage(errors, 'work', ['common.error.characterLimitExceedCounter', {length: [...work].length, limit: CONST.TITLE_CHARACTER_LIMIT}]); + ErrorUtils.addErrorMessage(errors, 'work', translate('common.error.characterLimitExceedCounter', {length: [...work].length, limit: CONST.TITLE_CHARACTER_LIMIT})); } return errors; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 7d0bbce26867..596e49c880e6 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,6 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import {ExpensiMark, Str} from 'expensify-common'; +import {Str} from 'expensify-common'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; @@ -19,6 +19,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; +import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; @@ -50,9 +51,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri const {translate} = useLocalize(); // We need to edit the note in markdown format, but display it in HTML format - const parser = new ExpensiMark(); const [privateNote, setPrivateNote] = useState( - () => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), + () => ReportActions.getDraftPrivateNote(report.reportID).trim() || parseHtmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), ); /** @@ -93,7 +93,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); + editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parseHtmlToMarkdown(originalNote).trim()); ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); } diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 8a380ec48e2c..4ddffa3d802b 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -198,6 +198,7 @@ function ProfilePage({route}: ProfilePageProps) { imageStyles={[styles.avatarXLarge]} source={details.avatar} avatarID={accountID} + type={CONST.ICON_TYPE_AVATAR} size={CONST.AVATAR_SIZE.XLARGE} fallbackIcon={fallbackIcon} /> diff --git a/src/pages/ReimbursementAccount/AddressFormFields.tsx b/src/pages/ReimbursementAccount/AddressFormFields.tsx index 48af00cd4925..a863d3cc5952 100644 --- a/src/pages/ReimbursementAccount/AddressFormFields.tsx +++ b/src/pages/ReimbursementAccount/AddressFormFields.tsx @@ -52,7 +52,7 @@ function AddressFormFields({shouldSaveDraft = false, defaultValues, values, erro value={values?.street} defaultValue={defaultValues?.street} onInputChange={onFieldChange} - errorText={errors?.street ? 'bankAccount.error.addressStreet' : ''} + errorText={errors?.street ? translate('bankAccount.error.addressStreet') : ''} renamedInputKeys={inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} isLimitedToUSA @@ -68,7 +68,7 @@ function AddressFormFields({shouldSaveDraft = false, defaultValues, values, erro value={values?.city} defaultValue={defaultValues?.city} onChangeText={(value) => onFieldChange?.({city: value})} - errorText={errors?.city ? 'bankAccount.error.addressCity' : ''} + errorText={errors?.city ? translate('bankAccount.error.addressCity') : ''} containerStyles={styles.mt6} /> @@ -80,7 +80,7 @@ function AddressFormFields({shouldSaveDraft = false, defaultValues, values, erro value={values?.state as State} defaultValue={defaultValues?.state} onInputChange={(value) => onFieldChange?.({state: value})} - errorText={errors?.state ? 'bankAccount.error.addressState' : ''} + errorText={errors?.state ? translate('bankAccount.error.addressState') : ''} /> onFieldChange?.({zipCode: value})} - errorText={errors?.zipCode ? 'bankAccount.error.zipCode' : ''} + errorText={errors?.zipCode ? translate('bankAccount.error.zipCode') : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} + hint={translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} containerStyles={styles.mt3} /> diff --git a/src/pages/ReimbursementAccount/BankInfo/substeps/Manual.tsx b/src/pages/ReimbursementAccount/BankInfo/substeps/Manual.tsx index 3f58d7a2073e..1111bd8fcc47 100644 --- a/src/pages/ReimbursementAccount/BankInfo/substeps/Manual.tsx +++ b/src/pages/ReimbursementAccount/BankInfo/substeps/Manual.tsx @@ -8,6 +8,7 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -37,32 +38,41 @@ function Manual({reimbursementAccount, onNext}: ManualProps) { [BANK_INFO_STEP_KEYS.ACCOUNT_NUMBER]: reimbursementAccount?.achData?.[BANK_INFO_STEP_KEYS.ACCOUNT_NUMBER] ?? '', }; - const validate = useCallback((values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - const routingNumber = values.routingNumber?.trim(); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + const routingNumber = values.routingNumber?.trim(); - if ( - values.accountNumber && - !CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) && - !CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) - ) { - errors.accountNumber = 'bankAccount.error.accountNumber'; - } else if (values.accountNumber && values.accountNumber === routingNumber) { - errors.accountNumber = 'bankAccount.error.routingAndAccountNumberCannotBeSame'; - } - if (routingNumber && (!CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber))) { - errors.routingNumber = 'bankAccount.error.routingNumber'; - } + if ( + values.accountNumber && + !CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) && + !CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) + ) { + errors.accountNumber = translate('bankAccount.error.accountNumber'); + } else if (values.accountNumber && values.accountNumber === routingNumber) { + errors.accountNumber = translate('bankAccount.error.routingAndAccountNumberCannotBeSame'); + } + if (routingNumber && (!CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !ValidationUtils.isValidRoutingNumber(routingNumber))) { + errors.routingNumber = translate('bankAccount.error.routingNumber'); + } - return errors; - }, []); + return errors; + }, + [translate], + ); const shouldDisableInputs = !!(reimbursementAccount?.achData?.bankAccountID ?? ''); + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: true, + }); + return ( ): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, stepFields); if (values[ssnLast4InputID] && !ValidationUtils.isValidSSNLastFour(values[ssnLast4InputID])) { - errors[ssnLast4InputID] = 'bankAccount.error.ssnLast4'; + errors[ssnLast4InputID] = translate('bankAccount.error.ssnLast4'); } return errors; }; diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx index cca9e5619d59..bd9524626a14 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -32,27 +32,30 @@ const INPUT_KEYS = { const STEP_FIELDS = [COMPANY_BUSINESS_INFO_KEY.STREET, COMPANY_BUSINESS_INFO_KEY.CITY, COMPANY_BUSINESS_INFO_KEY.STATE, COMPANY_BUSINESS_INFO_KEY.ZIP_CODE]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); +function AddressBusiness({reimbursementAccount, onNext, isEditing}: AddressBusinessProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); - if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { - errors.addressStreet = 'bankAccount.error.addressStreet'; - } + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.addressCity && !ValidationUtils.isValidAddress(values.addressCity)) { - errors.addressCity = 'bankAccount.error.addressCity'; - } + if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { + errors.addressStreet = translate('bankAccount.error.addressStreet'); + } - if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { - errors.addressZipCode = 'bankAccount.error.zipCode'; - } + if (values.addressCity && !ValidationUtils.isValidAddress(values.addressCity)) { + errors.addressCity = translate('bankAccount.error.addressCity'); + } - return errors; -}; + if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { + errors.addressZipCode = translate('bankAccount.error.zipCode'); + } -function AddressBusiness({reimbursementAccount, onNext, isEditing}: AddressBusinessProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); + return errors; + }, + [translate], + ); const defaultValues = { street: reimbursementAccount?.achData?.addressStreet ?? '', diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx index 4c8331a23d8f..23b59d76ffa7 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx @@ -1,5 +1,5 @@ import type {CONST as COMMON_CONST} from 'expensify-common'; -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; @@ -37,16 +37,6 @@ type States = keyof typeof COMMON_CONST.STATES; const BUSINESS_INFO_STEP_KEYS = INPUT_IDS.BUSINESS_INFO_STEP; const BUSINESS_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.BUSINESS_INFO; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, [BUSINESS_INFO_STEP_KEYS.HAS_NO_CONNECTION_TO_CANNABIS]); - - if (!values.hasNoConnectionToCannabis) { - errors.hasNoConnectionToCannabis = 'bankAccount.error.restrictedBusiness'; - } - - return errors; -}; - function ConfirmCompanyLabel() { const {translate} = useLocalize(); @@ -62,6 +52,19 @@ function ConfirmationBusiness({reimbursementAccount, reimbursementAccountDraft, const {translate} = useLocalize(); const styles = useThemeStyles(); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [BUSINESS_INFO_STEP_KEYS.HAS_NO_CONNECTION_TO_CANNABIS]); + + if (!values.hasNoConnectionToCannabis) { + errors.hasNoConnectionToCannabis = translate('bankAccount.error.restrictedBusiness'); + } + + return errors; + }, + [translate], + ); + const values = useMemo(() => getSubstepValues(BUSINESS_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); const defaultCheckboxState = reimbursementAccountDraft?.[BUSINESS_INFO_STEP_KEYS.HAS_NO_CONNECTION_TO_CANNABIS] ?? false; diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/IncorporationDateBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/IncorporationDateBusiness.tsx index 8fecbe46164f..274e087cc415 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/IncorporationDateBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/IncorporationDateBusiness.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; @@ -29,22 +29,25 @@ type IncorporationDateBusinessProps = IncorporationDateBusinessOnyxProps & SubSt const COMPANY_INCORPORATION_DATE_KEY = INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_DATE; const STEP_FIELDS = [COMPANY_INCORPORATION_DATE_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.incorporationDate && !ValidationUtils.isValidDate(values.incorporationDate)) { - errors.incorporationDate = 'common.error.dateInvalid'; - } else if (values.incorporationDate && !ValidationUtils.isValidPastDate(values.incorporationDate)) { - errors.incorporationDate = 'bankAccount.error.incorporationDateFuture'; - } - - return errors; -}; - function IncorporationDateBusiness({reimbursementAccount, reimbursementAccountDraft, onNext, isEditing}: IncorporationDateBusinessProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.incorporationDate && !ValidationUtils.isValidDate(values.incorporationDate)) { + errors.incorporationDate = translate('common.error.dateInvalid'); + } else if (values.incorporationDate && !ValidationUtils.isValidPastDate(values.incorporationDate)) { + errors.incorporationDate = translate('bankAccount.error.incorporationDateFuture'); + } + + return errors; + }, + [translate], + ); + const defaultCompanyIncorporationDate = reimbursementAccount?.achData?.incorporationDate ?? reimbursementAccountDraft?.incorporationDate ?? ''; const handleSubmit = useReimbursementAccountStepFormSubmit({ diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/NameBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/NameBusiness.tsx index e37afae17084..0879607c9566 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/NameBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/NameBusiness.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -26,16 +26,6 @@ type NameBusinessProps = NameBusinessOnyxProps & SubStepProps; const COMPANY_NAME_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_NAME; const STEP_FIELDS = [COMPANY_NAME_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.companyName && !ValidationUtils.isValidCompanyName(values.companyName)) { - errors.companyName = 'bankAccount.error.companyName'; - } - - return errors; -}; - function NameBusiness({reimbursementAccount, onNext, isEditing}: NameBusinessProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -45,6 +35,19 @@ function NameBusiness({reimbursementAccount, onNext, isEditing}: NameBusinessPro const shouldDisableCompanyName = !!(bankAccountID && defaultCompanyName && reimbursementAccount?.achData?.state !== 'SETUP'); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.companyName && !ValidationUtils.isValidCompanyName(values.companyName)) { + errors.companyName = translate('bankAccount.error.companyName'); + } + + return errors; + }, + [translate], + ); + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx index 018922610080..e0c83737b6f8 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -26,21 +26,24 @@ type PhoneNumberBusinessProps = PhoneNumberBusinessOnyxProps & SubStepProps; const COMPANY_PHONE_NUMBER_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE; const STEP_FIELDS = [COMPANY_PHONE_NUMBER_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { - errors.companyPhone = 'bankAccount.error.phoneNumber'; - } - - return errors; -}; - function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNumberBusinessProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const defaultCompanyPhoneNumber = reimbursementAccount?.achData?.companyPhone ?? ''; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { + errors.companyPhone = translate('bankAccount.error.phoneNumber'); + } + + return errors; + }, + [translate], + ); + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx index 67e77f8127d8..2063211f41f4 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -25,17 +25,6 @@ type TaxIdBusinessProps = TaxIdBusinessOnyxProps & SubStepProps; const COMPANY_TAX_ID_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_TAX_ID; const STEP_FIELDS = [COMPANY_TAX_ID_KEY]; - -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.companyTaxID && !ValidationUtils.isValidTaxID(values.companyTaxID)) { - errors.companyTaxID = 'bankAccount.error.taxID'; - } - - return errors; -}; - function TaxIdBusiness({reimbursementAccount, onNext, isEditing}: TaxIdBusinessProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -43,6 +32,19 @@ function TaxIdBusiness({reimbursementAccount, onNext, isEditing}: TaxIdBusinessP const bankAccountID = reimbursementAccount?.achData?.bankAccountID ?? -1; const shouldDisableCompanyTaxID = !!(bankAccountID && defaultCompanyTaxId && reimbursementAccount?.achData?.state !== 'SETUP'); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.companyTaxID && !ValidationUtils.isValidTaxID(values.companyTaxID)) { + errors.companyTaxID = translate('bankAccount.error.taxID'); + } + + return errors; + }, + [translate], + ); + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx index e06c4d9d575e..541e47f6cb10 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -34,16 +34,6 @@ type WebsiteBusinessProps = WebsiteBusinessOnyxProps & SubStepProps; const COMPANY_WEBSITE_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_WEBSITE; const STEP_FIELDS = [COMPANY_WEBSITE_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.website && !ValidationUtils.isValidWebsite(values.website)) { - errors.website = 'bankAccount.error.website'; - } - - return errors; -}; - function WebsiteBusiness({reimbursementAccount, user, session, onNext, isEditing}: WebsiteBusinessProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -51,6 +41,18 @@ function WebsiteBusiness({reimbursementAccount, user, session, onNext, isEditing const defaultWebsiteExample = useMemo(() => getDefaultCompanyWebsite(session, user), [session, user]); const defaultCompanyWebsite = reimbursementAccount?.achData?.website ?? defaultWebsiteExample; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.website && !ValidationUtils.isValidWebsite(values.website)) { + errors.website = translate('bankAccount.error.website'); + } + + return errors; + }, + [translate], + ); const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx b/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx index 03a178f186ee..71d7a80ab58b 100644 --- a/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx +++ b/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; @@ -30,24 +30,6 @@ const STEP_FIELDS = [ INPUT_IDS.COMPLETE_VERIFICATION.CERTIFY_TRUE_INFORMATION, ]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (!ValidationUtils.isRequiredFulfilled(values.acceptTermsAndConditions)) { - errors.acceptTermsAndConditions = 'common.error.acceptTerms'; - } - - if (!ValidationUtils.isRequiredFulfilled(values.certifyTrueInformation)) { - errors.certifyTrueInformation = 'completeVerificationStep.certifyTrueAndAccurateError'; - } - - if (!ValidationUtils.isRequiredFulfilled(values.isAuthorizedToUseBankAccount)) { - errors.isAuthorizedToUseBankAccount = 'completeVerificationStep.isAuthorizedToUseBankAccountError'; - } - - return errors; -}; - function IsAuthorizedToUseBankAccountLabel() { const {translate} = useLocalize(); return {translate('completeVerificationStep.isAuthorizedToUseBankAccount')}; @@ -76,6 +58,26 @@ function ConfirmAgreements({onNext, reimbursementAccount}: ConfirmAgreementsProp certifyTrueInformation: reimbursementAccount?.achData?.certifyTrueInformation ?? false, acceptTermsAndConditions: reimbursementAccount?.achData?.acceptTermsAndConditions ?? false, }; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (!ValidationUtils.isRequiredFulfilled(values.acceptTermsAndConditions)) { + errors.acceptTermsAndConditions = translate('common.error.acceptTerms'); + } + + if (!ValidationUtils.isRequiredFulfilled(values.certifyTrueInformation)) { + errors.certifyTrueInformation = translate('completeVerificationStep.certifyTrueAndAccurateError'); + } + + if (!ValidationUtils.isRequiredFulfilled(values.isAuthorizedToUseBankAccount)) { + errors.isAuthorizedToUseBankAccount = translate('completeVerificationStep.isAuthorizedToUseBankAccountError'); + } + + return errors; + }, + [translate], + ); return ( ): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { - errors.requestorAddressStreet = 'bankAccount.error.addressStreet'; - } - - if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { - errors.requestorAddressZipCode = 'bankAccount.error.zipCode'; - } - - return errors; -}; - function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -59,6 +45,23 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { zipCode: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', }; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { + errors.requestorAddressStreet = translate('bankAccount.error.addressStreet'); + } + + if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { + errors.requestorAddressZipCode = translate('bankAccount.error.zipCode'); + } + + return errors; + }, + [translate], + ); + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx index 2454331f12a6..6fe391bbe957 100644 --- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx @@ -1,5 +1,5 @@ import {subYears} from 'date-fns'; -import React from 'react'; +import React, {useCallback} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; @@ -32,24 +32,27 @@ type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps; const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB; const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.dob) { - if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { - errors.dob = 'bankAccount.error.dob'; - } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { - errors.dob = 'bankAccount.error.age'; - } - } - - return errors; -}; - function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, isEditing}: DateOfBirthProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.dob) { + if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { + errors.dob = translate('bankAccount.error.dob'); + } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { + errors.dob = translate('bankAccount.error.age'); + } + } + + return errors; + }, + [translate], + ); + const dobDefaultValue = reimbursementAccount?.achData?.[PERSONAL_INFO_DOB_KEY] ?? reimbursementAccountDraft?.[PERSONAL_INFO_DOB_KEY] ?? ''; const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx index 1d225c1c32f2..bab467707fd4 100644 --- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -27,19 +27,6 @@ type FullNameProps = FullNameOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; - -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.firstName && !ValidationUtils.isValidLegalName(values.firstName)) { - errors.firstName = 'bankAccount.error.firstName'; - } - - if (values.lastName && !ValidationUtils.isValidLegalName(values.lastName)) { - errors.lastName = 'bankAccount.error.lastName'; - } - return errors; -}; - function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -49,6 +36,21 @@ function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) { lastName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', }; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + if (values.firstName && !ValidationUtils.isValidLegalName(values.firstName)) { + errors.firstName = translate('bankAccount.error.firstName'); + } + + if (values.lastName && !ValidationUtils.isValidLegalName(values.lastName)) { + errors.lastName = translate('bankAccount.error.lastName'); + } + return errors; + }, + [translate], + ); + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx index e647fd768fb1..390880fa65f2 100644 --- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -28,21 +28,25 @@ type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; -const validate = (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - - if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { - errors.ssnLast4 = 'bankAccount.error.ssnLast4'; - } - - return errors; -}; function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialSecurityNumberProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const defaultSsnLast4 = reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { + errors.ssnLast4 = translate('bankAccount.error.ssnLast4'); + } + + return errors; + }, + [translate], + ); + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index d17ee8a792c1..1e3e232b2f49 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -160,7 +160,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD isAnonymousAction: false, shouldShowRightIcon: true, action: () => { - if (isUserCreatedPolicyRoom || isChatThread || isPolicyExpenseChat) { + if (isUserCreatedPolicyRoom || isChatThread || (isPolicyExpenseChat && isPolicyAdmin)) { Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '-1')); } else { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '-1')); @@ -204,7 +204,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }); } - if (!isThread && (isGroupChat || (isChatRoom && ReportUtils.canLeaveChat(report, policy)))) { + if (!isThread && (isGroupChat || (isChatRoom && ReportUtils.canLeaveChat(report, policy)) || (isPolicyExpenseChat && !isPolicyAdmin))) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.LEAVE_ROOM, translationKey: 'common.leave', @@ -232,6 +232,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD isChatThread, isPolicyEmployee, isPolicyExpenseChat, + isPolicyAdmin, isUserCreatedPolicyRoom, participants.length, report, diff --git a/src/pages/ReportParticipantDetailsPage.tsx b/src/pages/ReportParticipantDetailsPage.tsx index 24104b6d4353..f03b2475d5a7 100644 --- a/src/pages/ReportParticipantDetailsPage.tsx +++ b/src/pages/ReportParticipantDetailsPage.tsx @@ -87,6 +87,7 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic imageStyles={[styles.avatarXLarge]} source={details.avatar} avatarID={accountID} + type={CONST.ICON_TYPE_AVATAR} size={CONST.AVATAR_SIZE.XLARGE} fallbackIcon={fallbackIcon} /> diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx index e95fa84c09c9..9f8586292895 100644 --- a/src/pages/RoomDescriptionPage.tsx +++ b/src/pages/RoomDescriptionPage.tsx @@ -1,5 +1,4 @@ import {useFocusEffect} from '@react-navigation/native'; -import {ExpensiMark} from 'expensify-common'; import React, {useCallback, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; @@ -13,6 +12,7 @@ import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; @@ -32,8 +32,7 @@ type RoomDescriptionPageProps = { function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) { const styles = useThemeStyles(); - const parser = new ExpensiMark(); - const [description, setDescription] = useState(() => parser.htmlToMarkdown(report?.description ?? '')); + const [description, setDescription] = useState(() => parseHtmlToMarkdown(report?.description ?? '')); const reportDescriptionInputRef = useRef(null); const focusTimeoutRef = useRef | null>(null); const {translate} = useLocalize(); diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index a96b09374b29..d4827bad69c3 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -55,6 +55,8 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { const [searchValue, setSearchValue] = useState(''); const [didLoadRoomMembers, setDidLoadRoomMembers] = useState(false); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]); + const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(report), [report]); const isFocusedScreen = useIsFocused(); @@ -182,12 +184,14 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { return; } const pendingChatMember = report?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); + const isAdmin = !!(policy && policy.employeeList && details.login && policy.employeeList[details.login]?.role === CONST.POLICY.ROLE.ADMIN); + const isDisabled = (isPolicyExpenseChat && isAdmin) || accountID === session?.accountID || pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; result.push({ keyForList: String(accountID), accountID, isSelected: selectedMembers.includes(accountID), - isDisabled: accountID === session?.accountID || pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + isDisabled, text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: details?.login ? formatPhoneNumber(details.login) : '', icons: [ diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx index eabd8642c287..ca48cf807276 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx @@ -50,27 +50,27 @@ function IntroSchoolPrincipalPage(props: IntroSchoolPrincipalPageProps) { const errors: FormInputErrors = {}; if (!values.firstName || !ValidationUtils.isValidPersonName(values.firstName)) { - ErrorUtils.addErrorMessage(errors, 'firstName', 'bankAccount.error.firstName'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('bankAccount.error.firstName')); } if (!values.lastName || !ValidationUtils.isValidPersonName(values.lastName)) { - ErrorUtils.addErrorMessage(errors, 'lastName', 'bankAccount.error.lastName'); + ErrorUtils.addErrorMessage(errors, 'lastName', translate('bankAccount.error.lastName')); } if (!values.partnerUserID) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'teachersUnitePage.error.enterEmail'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('teachersUnitePage.error.enterEmail')); } if (values.partnerUserID && props.loginList?.[values.partnerUserID.toLowerCase()]) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'teachersUnitePage.error.tryDifferentEmail'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('teachersUnitePage.error.tryDifferentEmail')); } if (values.partnerUserID && !Str.isValidEmail(values.partnerUserID)) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'teachersUnitePage.error.enterValidEmail'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('teachersUnitePage.error.enterValidEmail')); } if (values.partnerUserID && LoginUtils.isEmailPublicDomain(values.partnerUserID)) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'teachersUnitePage.error.tryDifferentEmail'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('teachersUnitePage.error.tryDifferentEmail')); } return errors; }, - [props.loginList], + [props.loginList, translate], ); return ( diff --git a/src/pages/TeachersUnite/KnowATeacherPage.tsx b/src/pages/TeachersUnite/KnowATeacherPage.tsx index aaca679e49b8..e4bfc16bf24c 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.tsx +++ b/src/pages/TeachersUnite/KnowATeacherPage.tsx @@ -59,24 +59,24 @@ function KnowATeacherPage(props: KnowATeacherPageProps) { const validateIfNumber = LoginUtils.validateNumber(phoneLogin); if (!values.firstName || !ValidationUtils.isValidPersonName(values.firstName)) { - ErrorUtils.addErrorMessage(errors, 'firstName', 'bankAccount.error.firstName'); + ErrorUtils.addErrorMessage(errors, 'firstName', translate('bankAccount.error.firstName')); } if (!values.lastName || !ValidationUtils.isValidPersonName(values.lastName)) { - ErrorUtils.addErrorMessage(errors, 'lastName', 'bankAccount.error.lastName'); + ErrorUtils.addErrorMessage(errors, 'lastName', translate('bankAccount.error.lastName')); } if (!values.partnerUserID) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'teachersUnitePage.error.enterPhoneEmail'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('teachersUnitePage.error.enterPhoneEmail')); } if (values.partnerUserID && props.loginList?.[validateIfNumber || values.partnerUserID.toLowerCase()]) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'teachersUnitePage.error.tryDifferentEmail'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('teachersUnitePage.error.tryDifferentEmail')); } if (values.partnerUserID && !(validateIfNumber || Str.isValidEmail(values.partnerUserID))) { - ErrorUtils.addErrorMessage(errors, 'partnerUserID', 'contacts.genericFailureMessages.invalidContactMethod'); + ErrorUtils.addErrorMessage(errors, 'partnerUserID', translate('contacts.genericFailureMessages.invalidContactMethod')); } return errors; }, - [props.loginList], + [props.loginList, translate], ); return ( diff --git a/src/pages/Travel/TravelTerms.tsx b/src/pages/Travel/TravelTerms.tsx index 50df99d10581..1c22ea51b178 100644 --- a/src/pages/Travel/TravelTerms.tsx +++ b/src/pages/Travel/TravelTerms.tsx @@ -23,7 +23,7 @@ function TravelTerms() { const [hasAcceptedTravelTerms, setHasAcceptedTravelTerms] = useState(false); const [error, setError] = useState(false); - const errorMessage = error ? 'travel.termsAndConditions.error' : ''; + const errorMessage = error ? translate('travel.termsAndConditions.error') : ''; const toggleTravelTerms = () => { setHasAcceptedTravelTerms(!hasAcceptedTravelTerms); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index a037ea8c74c8..941b5fbafb64 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,4 +1,4 @@ -import {ExpensiMark, Str} from 'expensify-common'; +import {Str} from 'expensify-common'; import type {MutableRefObject} from 'react'; import React from 'react'; import {InteractionManager} from 'react-native'; @@ -18,6 +18,7 @@ import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import * as Localize from '@libs/Localize'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; +import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -40,13 +41,12 @@ function getActionHtml(reportAction: OnyxInputOrEntry): string { /** Sets the HTML string to Clipboard */ function setClipboardMessage(content: string) { - const parser = new ExpensiMark(); if (!Clipboard.canSetHtml()) { - Clipboard.setString(parser.htmlToMarkdown(content)); + Clipboard.setString(parseHtmlToMarkdown(content)); } else { const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; const isAnchorTag = anchorRegex.test(content); - const plainText = isAnchorTag ? parser.htmlToMarkdown(content) : parser.htmlToText(content); + const plainText = isAnchorTag ? parseHtmlToMarkdown(content) : parseHtmlToText(content); Clipboard.setHtml(content, plainText); } } @@ -238,8 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } const editAction = () => { if (!draftMessage) { - const parser = new ExpensiMark(); - Report.saveReportActionDraft(reportID, reportAction, parser.htmlToMarkdown(getActionHtml(reportAction))); + Report.saveReportActionDraft(reportID, reportAction, parseHtmlToMarkdown(getActionHtml(reportAction))); } else { Report.deleteReportActionDraft(reportID, reportAction); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 45e22786b6f4..fd0eaa32d20e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,5 +1,4 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; -import {ExpensiMark} from 'expensify-common'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react'; import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; @@ -35,6 +34,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import getPlatform from '@libs/getPlatform'; import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener'; +import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -542,8 +542,7 @@ function ComposerWithSuggestions( ) { event.preventDefault(); if (lastReportAction) { - const parser = new ExpensiMark(); - Report.saveReportActionDraft(reportID, lastReportAction, parser.htmlToMarkdown(lastReportAction.message?.at(-1)?.html ?? '')); + Report.saveReportActionDraft(reportID, lastReportAction, parseHtmlToMarkdown(lastReportAction.message?.at(-1)?.html ?? '')); } } }, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 7f169ef15918..682d3e8605b9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -1,5 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect} from 'react'; +import {Keyboard} from 'react-native'; import E2EClient from '@libs/E2E/client'; import type {ComposerRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions'; @@ -26,11 +27,26 @@ function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: Fo // We need to wait for the component to be mounted before focusing setTimeout(() => { - if (!(ref && 'current' in ref)) { - return; - } + const setFocus = () => { + if (!(ref && 'current' in ref)) { + return; + } - ref.current?.focus(true); + ref.current?.focus(true); + + setTimeout(() => { + // and actually let's verify that the keyboard is visible + if (Keyboard.isVisible()) { + return; + } + + ref.current?.blur(); + setFocus(); + // 500ms is enough time for any keyboard to open + }, 500); + }; + + setFocus(); }, 1); }, [ref]); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index f49552fc620b..3b9d6ee12c12 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,7 +1,8 @@ import {Str} from 'expensify-common'; +import lodashMapValues from 'lodash/mapValues'; import lodashSortBy from 'lodash/sortBy'; import type {ForwardedRef} from 'react'; -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -9,18 +10,20 @@ import type {Mention} from '@components/MentionSuggestions'; import MentionSuggestions from '@components/MentionSuggestions'; import {usePersonalDetails} from '@components/OnyxProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useCurrentReportID from '@hooks/useCurrentReportID'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import * as LoginUtils from '@libs/LoginUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import {isValidRoomName} from '@libs/ValidationUtils'; import * as ReportUserActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Report} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, Report} from '@src/types/onyx'; import type {SuggestionsRef} from './ReportActionCompose'; import type {SuggestionProps} from './Suggestions'; @@ -45,6 +48,14 @@ const defaultSuggestionsValues: SuggestionValues = { prefixType: '', }; +type SuggestionPersonalDetailsList = Record< + string, + | (PersonalDetails & { + weight: number; + }) + | null +>; + function SuggestionMention( {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused, isGroupPolicyReport, policyID}: SuggestionProps, ref: ForwardedRef, @@ -58,6 +69,36 @@ function SuggestionMention( const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isMentionSuggestionsMenuVisible = !!suggestionValues.suggestedMentions.length && suggestionValues.shouldShowSuggestionMenu; + const currentReportID = useCurrentReportID(); + const currentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${currentReportID?.currentReportID}`]; + // Smaller weight means higher order in suggestion list + const getPersonalDetailsWeight = useCallback( + (detail: PersonalDetails, policyEmployeeAccountIDs: number[]): number => { + if (ReportUtils.isReportParticipant(detail.accountID, currentReport)) { + return 0; + } + if (policyEmployeeAccountIDs.includes(detail.accountID)) { + return 1; + } + return 2; + }, + [currentReport], + ); + const weightedPersonalDetails: PersonalDetailsList | SuggestionPersonalDetailsList = useMemo(() => { + const policyEmployeeAccountIDs = getPolicyEmployeeAccountIDs(policyID); + if (!ReportUtils.isGroupChat(currentReport) && !ReportUtils.doesReportBelongToWorkspace(currentReport, policyEmployeeAccountIDs, policyID)) { + return personalDetails; + } + return lodashMapValues(personalDetails, (detail) => + detail + ? { + ...detail, + weight: getPersonalDetailsWeight(detail, policyEmployeeAccountIDs), + } + : null, + ); + }, [policyID, currentReport, personalDetails, getPersonalDetailsWeight]); + const [highlightedMentionIndex, setHighlightedMentionIndex] = useArrowKeyFocusManager({ isActive: isMentionSuggestionsMenuVisible, maxIndex: suggestionValues.suggestedMentions.length - 1, @@ -190,7 +231,7 @@ function SuggestionMention( ); const getUserMentionOptions = useCallback( - (personalDetailsParam: PersonalDetailsList, searchValue = ''): Mention[] => { + (personalDetailsParam: PersonalDetailsList | SuggestionPersonalDetailsList, searchValue = ''): Mention[] => { const suggestions = []; if (CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT.includes(searchValue.toLowerCase())) { @@ -231,8 +272,7 @@ function SuggestionMention( return true; }); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing cannot be used if left side can be empty string - const sortedPersonalDetails = lodashSortBy(filteredPersonalDetails, (detail) => detail?.displayName || detail?.login); + const sortedPersonalDetails = lodashSortBy(filteredPersonalDetails, ['weight', 'displayName', 'login']); sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: formatLoginPrivateDomain(PersonalDetailsUtils.getDisplayNameOrDefault(detail), detail?.login), @@ -320,7 +360,7 @@ function SuggestionMention( }; if (isMentionCode(suggestionWord) && prefixType === '@') { - const suggestions = getUserMentionOptions(personalDetails, prefix); + const suggestions = getUserMentionOptions(weightedPersonalDetails, prefix); nextState.suggestedMentions = suggestions; nextState.shouldShowSuggestionMenu = !!suggestions.length; } @@ -340,7 +380,7 @@ function SuggestionMention( })); setHighlightedMentionIndex(0); }, - [isComposerFocused, value, isGroupPolicyReport, setHighlightedMentionIndex, resetSuggestions, getUserMentionOptions, personalDetails, getRoomMentionOptions, reports], + [isComposerFocused, value, isGroupPolicyReport, setHighlightedMentionIndex, resetSuggestions, getUserMentionOptions, weightedPersonalDetails, getRoomMentionOptions, reports], ); useEffect(() => { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 67a1216b89e7..9fdaf95f86b4 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -1,4 +1,3 @@ -import {ExpensiMark} from 'expensify-common'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -27,6 +26,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import type {Selection} from '@libs/focusComposerWithDelay/types'; import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; +import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import onyxSubscribe from '@libs/onyxSubscribe'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -108,14 +108,13 @@ function ReportActionItemMessageEdit( const isCommentPendingSaved = useRef(false); useEffect(() => { - const parser = new ExpensiMark(); draftMessageVideoAttributeCache.clear(); - const originalMessage = parser.htmlToMarkdown(action.message?.[0]?.html ?? '', { + + const originalMessage = parseHtmlToMarkdown(action.message?.[0]?.html ?? '', undefined, undefined, { cacheVideoAttributes: (videoSource, attrs) => { draftMessageVideoAttributeCache.set(videoSource, attrs); }, }); - if (ReportActionsUtils.isDeletedAction(action) || !!(action.message && draftMessage === originalMessage) || !!(prevDraftMessage === draftMessage || isCommentPendingSaved.current)) { return; } diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 39d63ebc53ce..cd85939e8bec 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -7,7 +7,6 @@ import {withOnyx} from 'react-native-onyx'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import withWindowDimensions from '@components/withWindowDimensions'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; -import compose from '@libs/compose'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import type {FlagCommentNavigatorParamList, SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; @@ -103,7 +102,7 @@ export default function , OnyxProps>({ report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, @@ -138,9 +137,8 @@ export default function = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.COMMENT]); if (!values.comment) { - errors.comment = 'common.error.fieldRequired'; + errors.comment = translate('common.error.fieldRequired'); } // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false // as we do not allow requestee to edit fields like description and amount. // But, we still want the requestee to be able to put the request on hold if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { const formErrors = {}; - ErrorUtils.addErrorMessage(formErrors, 'reportModified', 'common.error.requestModified'); + ErrorUtils.addErrorMessage(formErrors, 'reportModified', translate('common.error.requestModified')); FormActions.setErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM, formErrors); } return errors; }, - [parentReportAction, isWorkspaceRequest], + [parentReportAction, isWorkspaceRequest, translate], ); useEffect(() => { diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index 667c27c96a8c..ff061e7382c6 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -17,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import type {MaybePhraseKey} from '@libs/Localize'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; @@ -103,7 +102,7 @@ function MoneyRequestAmountForm( const textInput = useRef(null); const moneyRequestAmountInput = useRef(null); - const [formError, setFormError] = useState(''); + const [formError, setFormError] = useState(''); const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); const isFocused = useIsFocused(); @@ -215,18 +214,18 @@ function MoneyRequestAmountForm( // Skip the check for tax amount form as 0 is a valid input const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { - setFormError('iou.error.invalidAmount'); + setFormError(translate('iou.error.invalidAmount')); return; } if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { - setFormError(['iou.error.invalidTaxAmount', {amount: formattedTaxAmount}]); + setFormError(translate('iou.error.invalidTaxAmount', {amount: formattedTaxAmount})); return; } onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType}); }, - [taxAmount, onSubmitButtonPress, currency, formattedTaxAmount], + [taxAmount, onSubmitButtonPress, currency, translate, formattedTaxAmount], ); const buttonText: string = useMemo(() => { diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index f7cd13e14af0..0caa8b6cd7a6 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -19,7 +19,6 @@ import usePermissions from '@hooks/usePermissions'; import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import type {MaybePhraseKey} from '@libs/Localize'; import type {Options} from '@libs/OptionsListUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -66,7 +65,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic shouldInitialize: didScreenTransitionEnd, }); - const offlineMessage: MaybePhraseKey = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; + const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action); @@ -309,7 +308,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic )} diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 2a546c93be7c..18ddc19644ef 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -3,13 +3,13 @@ import React, {useEffect} from 'react'; import {ActivityIndicator, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import BlockingView from '@components/BlockingViews/BlockingView'; import Button from '@components/Button'; import CategoryPicker from '@components/CategoryPicker'; import FixedFooter from '@components/FixedFooter'; import * as Illustrations from '@components/Icon/Illustrations'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; @@ -18,7 +18,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -169,13 +168,12 @@ function IOURequestStepCategory({ )} {shouldShowEmptyState && ( -