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/deploy.yml b/.github/workflows/deploy.yml index a792d069151b..624c00de6831 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -65,6 +65,6 @@ jobs: PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --notes ${{ steps.getReleaseBody.outputs.RELEASE_BODY }} + run: gh release create ${{ env.PRODUCTION_VERSION }} --generate-notes env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d4b708ccea4d..0b93611527c5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -57,3 +57,5 @@ compliance with this Code is required to maintain your status as an Expensify co By signing up to participate as an contributor, you are acknowledging your understanding of and consent to (i) what is expected of you under this Code and (ii) notwithstanding anything to the contrary in any agreement you have with Expensify, Expensify’s right, but not obligation, to terminate your participation in the Expensify Contributor Community upon any breach of the Code, as determined in Expensify’s sole Discretion. + +Violations of our two rules may lead to removal from the contributor program. Severe violations can lead to an immediate ban, while lesser ones may result in a formal warning. Multiple warnings will also lead to removal. diff --git a/android/app/build.gradle b/android/app/build.gradle index 0a0a0ee04e90..988d0e187b6c 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 1001048505 + versionName "1.4.85-5" // 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/assets/animations/MagicCode.lottie b/assets/animations/MagicCode.lottie deleted file mode 100644 index ea94f1138f97..000000000000 Binary files a/assets/animations/MagicCode.lottie and /dev/null differ diff --git a/assets/images/bed.svg b/assets/images/bed.svg index fd654c036a7c..8ee733733ab2 100644 --- a/assets/images/bed.svg +++ b/assets/images/bed.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/car-with-key.svg b/assets/images/car-with-key.svg index 1586c0dfecfa..e4770fdad970 100644 --- a/assets/images/car-with-key.svg +++ b/assets/images/car-with-key.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/check-circle.svg b/assets/images/check-circle.svg index c13b83cbf281..3f6f1da4f827 100644 --- a/assets/images/check-circle.svg +++ b/assets/images/check-circle.svg @@ -1,13 +1 @@ - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/checkmark-circle.svg b/assets/images/checkmark-circle.svg index 3497548bc1bc..102598b55d8a 100644 --- a/assets/images/checkmark-circle.svg +++ b/assets/images/checkmark-circle.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/credit-card-exclamation.svg b/assets/images/credit-card-exclamation.svg index 67e686516baa..9cf946a56a5c 100644 --- a/assets/images/credit-card-exclamation.svg +++ b/assets/images/credit-card-exclamation.svg @@ -1,14 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/crosshair.svg b/assets/images/crosshair.svg index 357faab49178..66ee21774d02 100644 --- a/assets/images/crosshair.svg +++ b/assets/images/crosshair.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/emptystate__routepending.svg b/assets/images/emptystate__routepending.svg index aba08554d02f..685696f04abf 100644 --- a/assets/images/emptystate__routepending.svg +++ b/assets/images/emptystate__routepending.svg @@ -1,18 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/inbox.svg b/assets/images/inbox.svg index f9059e78ec5a..29ab7f916616 100644 --- a/assets/images/inbox.svg +++ b/assets/images/inbox.svg @@ -1,12 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/money-search.svg b/assets/images/money-search.svg index 90dedae0a2fb..72a77352f861 100644 --- a/assets/images/money-search.svg +++ b/assets/images/money-search.svg @@ -1,16 +1 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/plane.svg b/assets/images/plane.svg index bf4d56875239..635bdc4b1ed7 100644 --- a/assets/images/plane.svg +++ b/assets/images/plane.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/product-illustrations/emptystate__travel.svg b/assets/images/product-illustrations/emptystate__travel.svg index 416b27eb5bee..287f99996860 100644 --- a/assets/images/product-illustrations/emptystate__travel.svg +++ b/assets/images/product-illustrations/emptystate__travel.svg @@ -1,575 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/receipt-slash.svg b/assets/images/receipt-slash.svg index 2af3fcbc60e6..f7e7457e3e64 100644 --- a/assets/images/receipt-slash.svg +++ b/assets/images/receipt-slash.svg @@ -1,12 +1 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg index a96a7e5dc0af..66d2b1e5b0e2 100644 --- a/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg +++ b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg @@ -1,21 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg b/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg index 17ff47e6ca12..26b1ea7f2c31 100644 --- a/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg +++ b/assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg @@ -1,57 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__sendmoney.svg b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg index 80393e3c30cf..1975c15d5d24 100644 --- a/assets/images/simple-illustrations/simple-illustration__sendmoney.svg +++ b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg @@ -1,72 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg index e158bc5588cb..6dcb4a422f0a 100644 --- a/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg +++ b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg @@ -1,82 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg index d70d2d1ef552..39b2e4a12542 100644 --- a/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg +++ b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg @@ -1,27 +1 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/subscription-details__approvedlogo--light.svg b/assets/images/subscription-details__approvedlogo--light.svg index 580ee60c597c..6582fdf13fcd 100644 --- a/assets/images/subscription-details__approvedlogo--light.svg +++ b/assets/images/subscription-details__approvedlogo--light.svg @@ -1,91 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/subscription-details__approvedlogo.svg b/assets/images/subscription-details__approvedlogo.svg index 7722e2526657..73615be28528 100644 --- a/assets/images/subscription-details__approvedlogo.svg +++ b/assets/images/subscription-details__approvedlogo.svg @@ -1,94 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file 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/docs/_sass/_main.scss b/docs/_sass/_main.scss index eb59388159bf..29b02d8aeb00 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -524,6 +524,10 @@ button { padding: 0; margin: 0; } + + li { + margin-left: 12px; + } } } } diff --git a/docs/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png new file mode 100644 index 000000000000..3ff21c1f34cb Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_1_Light.png differ diff --git a/docs/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png new file mode 100644 index 000000000000..dea262434e59 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ConnectBankAccount_2_Light.png differ diff --git a/docs/assets/images/simple-illustration__monitor-remotesync.svg b/docs/assets/images/simple-illustration__monitor-remotesync.svg index e4ed84a35851..f0f6f363036e 100644 --- a/docs/assets/images/simple-illustration__monitor-remotesync.svg +++ b/docs/assets/images/simple-illustration__monitor-remotesync.svg @@ -1,30 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ed5fdc1b6a77..c23b5b731123 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.83 + 1.4.85 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.83.1 + 1.4.85.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c52d60fe34ed..7919fa67575d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.83 + 1.4.85 CFBundleSignature ???? CFBundleVersion - 1.4.83.1 + 1.4.85.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index ca21f2331ad3..834c5d27ee16 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.83 + 1.4.85 CFBundleVersion - 1.4.83.1 + 1.4.85.5 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aca46d6b18ed..1e1626e5d73e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1303,6 +1303,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-keyboard-controller (1.12.2): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-launch-arguments (4.0.2): - React - react-native-netinfo (11.2.1): @@ -1852,7 +1871,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 +1889,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) @@ -2137,6 +2156,7 @@ DEPENDENCIES: - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-key-command (from `../node_modules/react-native-key-command`) + - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) @@ -2335,6 +2355,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-image-picker" react-native-key-command: :path: "../node_modules/react-native-key-command" + react-native-keyboard-controller: + :path: "../node_modules/react-native-keyboard-controller" react-native-launch-arguments: :path: "../node_modules/react-native-launch-arguments" react-native-netinfo: @@ -2541,6 +2563,7 @@ SPEC CHECKSUMS: react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 + react-native-keyboard-controller: 47c01b0741ae5fc84e53cf282e61cfa5c2edb19b react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: 02d31de0e08ab043d48f2a1a8baade109d7b6ca5 react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa @@ -2589,7 +2612,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 88030b7d9a31f5f6e67743df48ad952d64513b4a + RNLiveMarkdown: fff70dc755ed8199a449f61e76cbadec7cd20440 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 @@ -2606,7 +2629,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: 66a5c97ae1059e4da1993a4ad95abe5d819f555b diff --git a/jest/setup.ts b/jest/setup.ts index 416306ce8426..f11a8a4ed631 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -53,3 +53,6 @@ jest.mock('react-native-sound', () => { jest.mock('react-native-share', () => ({ default: jest.fn(), })); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); diff --git a/package-lock.json b/package-lock.json index 7ba03f0417df..85883cf595f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.83-1", + "version": "1.4.85-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.83-1", + "version": "1.4.85-5", "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", @@ -59,7 +59,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", @@ -99,11 +99,12 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.8", + "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "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.50", "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", @@ -20769,9 +20770,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", @@ -31892,6 +31893,16 @@ "version": "5.0.1", "license": "MIT" }, + "node_modules/react-native-keyboard-controller": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.2.tgz", + "integrity": "sha512-10Sy0+neSHGJxOmOxrUJR8TQznnrQ+jTFQtM1PP6YnblNQeAw1eOa+lO6YLGenRr5WuNSMZbks/3Ay0e2yMKLw==", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-reanimated": ">=2.3.0" + } + }, "node_modules/react-native-launch-arguments": { "version": "4.0.2", "license": "MIT", @@ -31939,9 +31950,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.50", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.50.tgz", + "integrity": "sha512-8pZX3G4GDsJerEOs9Q4srwh3ySg8T0DRt3hzcz0rBpVf0ZQOWJxWVhxgnN/M9bEh0gm5K5b0yfOHDZ/DdgtakA==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 16b77e6e754c..ce94775cd9d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.83-1", + "version": "1.4.85-5", "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,7 +65,7 @@ "@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", @@ -111,7 +111,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", @@ -151,11 +151,12 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.8", + "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "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.50", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", diff --git a/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch b/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch new file mode 100644 index 000000000000..1a5b4c40477b --- /dev/null +++ b/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch @@ -0,0 +1,70 @@ +diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +index 88ae3f3..497569a 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +@@ -36,6 +36,54 @@ static jsi::Value textInputMetricsPayload( + return payload; + }; + ++static jsi::Value textInputMetricsScrollPayload( ++ jsi::Runtime& runtime, ++ const TextInputMetrics& textInputMetrics) { ++ auto payload = jsi::Object(runtime); ++ ++ { ++ auto contentOffset = jsi::Object(runtime); ++ contentOffset.setProperty(runtime, "x", textInputMetrics.contentOffset.x); ++ contentOffset.setProperty(runtime, "y", textInputMetrics.contentOffset.y); ++ payload.setProperty(runtime, "contentOffset", contentOffset); ++ } ++ ++ { ++ auto contentInset = jsi::Object(runtime); ++ contentInset.setProperty(runtime, "top", textInputMetrics.contentInset.top); ++ contentInset.setProperty( ++ runtime, "left", textInputMetrics.contentInset.left); ++ contentInset.setProperty( ++ runtime, "bottom", textInputMetrics.contentInset.bottom); ++ contentInset.setProperty( ++ runtime, "right", textInputMetrics.contentInset.right); ++ payload.setProperty(runtime, "contentInset", contentInset); ++ } ++ ++ { ++ auto contentSize = jsi::Object(runtime); ++ contentSize.setProperty( ++ runtime, "width", textInputMetrics.contentSize.width); ++ contentSize.setProperty( ++ runtime, "height", textInputMetrics.contentSize.height); ++ payload.setProperty(runtime, "contentSize", contentSize); ++ } ++ ++ { ++ auto layoutMeasurement = jsi::Object(runtime); ++ layoutMeasurement.setProperty( ++ runtime, "width", textInputMetrics.layoutMeasurement.width); ++ layoutMeasurement.setProperty( ++ runtime, "height", textInputMetrics.layoutMeasurement.height); ++ payload.setProperty(runtime, "layoutMeasurement", layoutMeasurement); ++ } ++ ++ payload.setProperty(runtime, "zoomScale", textInputMetrics.zoomScale ?: 1); ++ ++ ++ return payload; ++ }; ++ + static jsi::Value textInputMetricsContentSizePayload( + jsi::Runtime& runtime, + const TextInputMetrics& textInputMetrics) { +@@ -140,7 +188,9 @@ void TextInputEventEmitter::onKeyPressSync( + + void TextInputEventEmitter::onScroll( + const TextInputMetrics& textInputMetrics) const { +- dispatchTextInputEvent("scroll", textInputMetrics); ++ dispatchEvent("scroll", [textInputMetrics](jsi::Runtime& runtime) { ++ return textInputMetricsScrollPayload(runtime, textInputMetrics); ++ }); + } + + void TextInputEventEmitter::dispatchTextInputEvent( diff --git a/patches/react-native-keyboard-controller+1.12.2.patch.patch b/patches/react-native-keyboard-controller+1.12.2.patch.patch new file mode 100644 index 000000000000..3c8034354481 --- /dev/null +++ b/patches/react-native-keyboard-controller+1.12.2.patch.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +index 83884d8..5d9e989 100644 +--- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt ++++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +@@ -99,12 +99,12 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R + } + + private fun goToEdgeToEdge(edgeToEdge: Boolean) { +- reactContext.currentActivity?.let { +- WindowCompat.setDecorFitsSystemWindows( +- it.window, +- !edgeToEdge, +- ) +- } ++ // reactContext.currentActivity?.let { ++ // WindowCompat.setDecorFitsSystemWindows( ++ // it.window, ++ // !edgeToEdge, ++ // ) ++ // } + } + + private fun setupKeyboardCallbacks() { +@@ -158,13 +158,13 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R + // region State managers + private fun enable() { + this.goToEdgeToEdge(true) +- this.setupWindowInsets() ++ // this.setupWindowInsets() + this.setupKeyboardCallbacks() + } + + private fun disable() { + this.goToEdgeToEdge(false) +- this.setupWindowInsets() ++ // this.setupWindowInsets() + this.removeKeyboardCallbacks() + } + // endregion \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 9eda57816e9d..1ce17ea095bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import {PortalProvider} from '@gorhom/portal'; import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import {KeyboardProvider} from 'react-native-keyboard-controller'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; @@ -84,6 +85,7 @@ function App({url}: AppProps) { FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, + KeyboardProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 6a936bc97087..3d67a951111e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -356,6 +356,7 @@ const CONST = { CHRONOS_IN_CASH: 'chronosInCash', DEFAULT_ROOMS: 'defaultRooms', VIOLATIONS: 'violations', + DUPE_DETECTION: 'dupeDetection', REPORT_FIELDS: 'reportFields', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', @@ -523,6 +524,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, @@ -1236,6 +1247,8 @@ const CONST = { MAX_AMOUNT_OF_SUGGESTIONS: 20, MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5, HERE_TEXT: '@here', + SUGGESTION_BOX_MAX_SAFE_DISTANCE: 38, + BIG_SCREEN_SUGGESTION_WIDTH: 300, }, COMPOSER_MAX_HEIGHT: 125, CHAT_FOOTER_SECONDARY_ROW_HEIGHT: 15, @@ -3309,6 +3322,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', @@ -4865,9 +4883,10 @@ type Country = keyof typeof CONST.ALL_COUNTRIES; type IOUType = ValueOf; type IOUAction = ValueOf; type IOURequestType = ValueOf; +type FeedbackSurveyOptionID = ValueOf, 'ID'>>; type SubscriptionType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID}; export default CONST; 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/TIMEZONES.ts b/src/TIMEZONES.ts index 69ef89e7467e..0fb340a2d88d 100644 --- a/src/TIMEZONES.ts +++ b/src/TIMEZONES.ts @@ -551,6 +551,17 @@ const timezoneBackwardMap: Record> = { 'US/Pacific': 'America/Los_Angeles', 'US/Samoa': 'Pacific/Pago_Pago', 'W-SU': 'Europe/Moscow', + CET: 'Europe/Paris', + CST6CDT: 'America/Chicago', + EET: 'Europe/Sofia', + EST: 'America/Cancun', + EST5EDT: 'America/New_York', + HST: 'Pacific/Honolulu', + MET: 'Europe/Paris', + MST: 'America/Phoenix', + MST7MDT: 'America/Denver', + PST8PDT: 'America/Los_Angeles', + WET: 'Europe/Lisbon', }; export {timezoneBackwardMap}; 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/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 3db946ce387e..ae09757b66e6 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -18,7 +18,6 @@ import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import useNativeDriver from '@libs/useNativeDriver'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; @@ -359,28 +358,6 @@ function AttachmentModal({ [isValidFile, getModalType, isDirectoryCheck], ); - /** - * In order to gracefully hide/show the confirm button when the keyboard - * opens/closes, apply an animation to fade the confirm button out/in. And since - * we're only updating the opacity of the confirm button, we must also conditionally - * disable it. - * - * @param shouldFadeOut If true, fade out confirm button. Otherwise fade in. - */ - const updateConfirmButtonVisibility = useCallback( - (shouldFadeOut: boolean) => { - setIsConfirmButtonDisabled(shouldFadeOut); - const toValue = shouldFadeOut ? 0 : 1; - - Animated.timing(confirmButtonFadeAnimation, { - toValue, - duration: 100, - useNativeDriver, - }).start(); - }, - [confirmButtonFadeAnimation], - ); - /** * close the modal */ @@ -547,7 +524,7 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} - onToggleKeyboard={updateConfirmButtonVisibility} + onToggleKeyboard={setIsConfirmButtonDisabled} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} fallbackSource={fallbackSource} @@ -559,7 +536,7 @@ function AttachmentModal({ ))} {/* If we have an onConfirm method show a confirmation button */} - {!!onConfirm && ( + {!!onConfirm && !isConfirmButtonDisabled && ( {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts new file mode 100644 index 000000000000..5bb671c5edac --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts @@ -0,0 +1,5 @@ +function getBottomSuggestionPadding(): number { + return 16; +} + +export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts new file mode 100644 index 000000000000..3ad9bbe7b152 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts @@ -0,0 +1,5 @@ +function getBottomSuggestionPadding(): number { + return 0; +} + +export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx new file mode 100644 index 000000000000..9848d77e479e --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx @@ -0,0 +1,33 @@ +import {Portal} from '@gorhom/portal'; +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; +import useStyleUtils from '@hooks/useStyleUtils'; +import getBottomSuggestionPadding from './getBottomSuggestionPadding'; +import type {AutoCompleteSuggestionsPortalProps} from './types'; + +function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps) { + const StyleUtils = useStyleUtils(); + const styles = useMemo(() => StyleUtils.getBaseAutoCompleteSuggestionContainerStyle({left, width, bottom: bottom + getBottomSuggestionPadding()}), [StyleUtils, left, width, bottom]); + + if (!width) { + return null; + } + + return ( + + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + width={width} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + + + ); +} + +AutoCompleteSuggestionsPortal.displayName = 'AutoCompleteSuggestionsPortal'; + +export default AutoCompleteSuggestionsPortal; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx new file mode 100644 index 000000000000..2d1d533c2859 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import type {ReactElement} from 'react'; +import ReactDOM from 'react-dom'; +import {View} from 'react-native'; +import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; +import useStyleUtils from '@hooks/useStyleUtils'; +import getBottomSuggestionPadding from './getBottomSuggestionPadding'; +import type {AutoCompleteSuggestionsPortalProps} from './types'; + +/** + * On the mobile-web platform, when long-pressing on auto-complete suggestions, + * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). + * The desired pattern for all platforms is to do nothing on long-press. + * On the native platform, tapping on auto-complete suggestions will not blur the main input. + */ + +function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false { + const StyleUtils = useStyleUtils(); + + const bodyElement = document.querySelector('body'); + + const componentToRender = ( + + width={width} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); + + return ( + !!width && + bodyElement && + ReactDOM.createPortal( + {componentToRender}, + bodyElement, + ) + ); +} + +AutoCompleteSuggestionsPortal.displayName = 'AutoCompleteSuggestionsPortal'; + +export default AutoCompleteSuggestionsPortal; +export type {AutoCompleteSuggestionsPortalProps}; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts new file mode 100644 index 000000000000..61fa3e8dcd48 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts @@ -0,0 +1,13 @@ +import type {AutoCompleteSuggestionsProps} from '@components/AutoCompleteSuggestions/types'; + +type ExternalProps = Omit, 'measureParentContainerAndReportCursor'>; + +type AutoCompleteSuggestionsPortalProps = ExternalProps & { + left: number; + width: number; + bottom: number; + measuredHeightOfSuggestionRows: number; +}; + +// eslint-disable-next-line import/prefer-default-export +export type {AutoCompleteSuggestionsPortalProps}; diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 4c11f1f0e35c..70d70a8c1844 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,49 +1,32 @@ import type {ReactElement} from 'react'; import React, {useCallback, useEffect, useRef} from 'react'; import {FlatList} from 'react-native-gesture-handler'; -import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; +import type {AutoCompleteSuggestionsPortalProps} from './AutoCompleteSuggestionsPortal'; +import type {RenderSuggestionMenuItemProps} from './types'; -const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: boolean): number => { - if (isSuggestionPickerLarge) { - if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { - // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available - return CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - if (numRows > 2) { - // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible - return CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; -}; - -/** - * On the mobile-web platform, when long-pressing on auto-complete suggestions, - * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). - * The desired pattern for all platforms is to do nothing on long-press. - * On the native platform, tapping on auto-complete suggestions will not blur the main input. - */ +type ExternalProps = Omit, 'left' | 'bottom'>; function BaseAutoCompleteSuggestions({ - highlightedSuggestionIndex, + highlightedSuggestionIndex = 0, onSelect, accessibilityLabelExtractor, renderSuggestionMenuItem, suggestions, - isSuggestionPickerLarge, keyExtractor, -}: AutoCompleteSuggestionsProps) { + measuredHeightOfSuggestionRows, +}: ExternalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); + const prevRowHeightRef = useRef(measuredHeightOfSuggestionRows); + const fadeInOpacity = useSharedValue(0); const scrollRef = useRef>(null); /** * Render a suggestion menu item component. @@ -56,7 +39,6 @@ function BaseAutoCompleteSuggestions({ onMouseDown={(e) => e.preventDefault()} onPress={() => onSelect(index)} onLongPress={() => {}} - shouldUseHapticsOnLongPress={false} accessibilityLabel={accessibilityLabelExtractor(item, index)} > {renderSuggestionMenuItem(item, index)} @@ -66,26 +48,45 @@ function BaseAutoCompleteSuggestions({ ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); + + const animatedStyles = useAnimatedStyle(() => ({ + opacity: fadeInOpacity.value, + ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value), + })); useEffect(() => { - rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { - duration: 100, - easing: Easing.inOut(Easing.ease), - }); - }, [suggestions.length, isSuggestionPickerLarge, rowHeight]); + if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) { + fadeInOpacity.value = withTiming(1, { + duration: 70, + easing: Easing.inOut(Easing.ease), + }); + rowHeight.value = measuredHeightOfSuggestionRows; + } else { + fadeInOpacity.value = 1; + rowHeight.value = withTiming(measuredHeightOfSuggestionRows, { + duration: 100, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }); + } + + prevRowHeightRef.current = measuredHeightOfSuggestionRows; + }, [suggestions.length, rowHeight, measuredHeightOfSuggestionRows, prevRowHeightRef, fadeInOpacity]); useEffect(() => { if (!scrollRef.current) { return; } - scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); + // When using cursor control (moving the cursor with the space bar on the keyboard) on Android, moving the cursor too fast may cause an error. + try { + scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); + } catch (e) { + // eslint-disable-next-line no-console + } }, [highlightedSuggestionIndex]); return ( { if (DeviceCapabilities.hasHoverSupport()) { return; diff --git a/src/components/AutoCompleteSuggestions/index.native.tsx b/src/components/AutoCompleteSuggestions/index.native.tsx deleted file mode 100644 index fbfa7d953581..000000000000 --- a/src/components/AutoCompleteSuggestions/index.native.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {Portal} from '@gorhom/portal'; -import React from 'react'; -import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; -import type {AutoCompleteSuggestionsProps} from './types'; - -function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { - return ( - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - {...props} /> - - ); -} - -AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; - -export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index c7f2aaea4d82..8634d6dd0ca0 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -1,38 +1,134 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import {View} from 'react-native'; +import React, {useEffect} from 'react'; +import useKeyboardState from '@hooks/useKeyboardState'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; -import type {AutoCompleteSuggestionsProps} from './types'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import CONST from '@src/CONST'; +import AutoCompleteSuggestionsPortal from './AutoCompleteSuggestionsPortal'; +import type {AutoCompleteSuggestionsProps, MeasureParentContainerAndCursor} from './types'; -function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { - const StyleUtils = useStyleUtils(); - const {windowHeight, windowWidth} = useWindowDimensions(); - const [{width, left, bottom}, setContainerState] = React.useState({ +const measureHeightOfSuggestionRows = (numRows: number, canBeBig: boolean): number => { + if (canBeBig) { + if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { + // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available + return CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + if (numRows > 2) { + // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible + return CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; +}; +function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSpaceAboveForSmall: boolean): boolean { + return isEnoughSpaceAboveForBig || isEnoughSpaceAboveForSmall; +} + +/** + * On the mobile-web platform, when long-pressing on auto-complete suggestions, + * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). + * The desired pattern for all platforms is to do nothing on long-press. + * On the native platform, tapping on auto-complete suggestions will not blur the main input. + */ +function AutoCompleteSuggestions({measureParentContainerAndReportCursor = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const containerRef = React.useRef(null); + const isInitialRender = React.useRef(true); + const isSuggestionAboveRef = React.useRef(false); + const leftValue = React.useRef(0); + const prevLeftValue = React.useRef(0); + const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions(); + const [suggestionHeight, setSuggestionHeight] = React.useState(0); + const [containerState, setContainerState] = React.useState({ width: 0, left: 0, bottom: 0, }); + const StyleUtils = useStyleUtils(); + const insets = useSafeAreaInsets(); + const {keyboardHeight} = useKeyboardState(); + const {paddingBottom: bottomInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); - React.useEffect(() => { - if (!measureParentContainer) { + useEffect(() => { + const container = containerRef.current; + if (!container) { + return () => {}; + } + container.onpointerdown = (e) => { + if (DeviceCapabilities.hasHoverSupport()) { + return; + } + e.preventDefault(); + }; + return () => (container.onpointerdown = null); + }, []); + + const suggestionsLength = props.suggestions.length; + + useEffect(() => { + if (!measureParentContainerAndReportCursor) { return; } - measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); - }, [measureParentContainer, windowHeight, windowWidth]); - const componentToRender = ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - /> - ); + measureParentContainerAndReportCursor(({x, y, width, scrollValue, cursorCoordinates}: MeasureParentContainerAndCursor) => { + const xCoordinatesOfCursor = x + cursorCoordinates.x; + const leftValueForBigScreen = + xCoordinatesOfCursor + CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH > windowWidth + ? windowWidth - CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH + : xCoordinatesOfCursor; + + let bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - (keyboardHeight || bottomInset); + const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; + + const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true); + const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false); + const isEnoughSpaceAboveForBig = windowHeight - bottomValue - contentMaxHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; + const isEnoughSpaceAboveForSmall = windowHeight - bottomValue - contentMinHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; - const bodyElement = document.querySelector('body'); + const newLeftValue = isSmallScreenWidth ? x : leftValueForBigScreen; + // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup + const isAdjustmentNeeded = Math.abs(prevLeftValue.current - leftValueForBigScreen) > 150; + if (isInitialRender.current || isAdjustmentNeeded) { + isSuggestionAboveRef.current = isSuggestionRenderedAbove(isEnoughSpaceAboveForBig, isEnoughSpaceAboveForSmall); + leftValue.current = newLeftValue; + isInitialRender.current = false; + prevLeftValue.current = newLeftValue; + } + let measuredHeight = 0; + if (isSuggestionAboveRef.current && isEnoughSpaceAboveForBig) { + // calculation for big suggestion box above the cursor + measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); + } else if (isSuggestionAboveRef.current && isEnoughSpaceAboveForSmall) { + // calculation for small suggestion box above the cursor + measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, false); + } else { + // calculation for big suggestion box below the cursor + measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); + bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + setSuggestionHeight(measuredHeight); + setContainerState({ + left: leftValue.current, + bottom: bottomValue, + width: widthValue, + }); + }); + }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset]); + + if (containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) { + return null; + } return ( - !!width && bodyElement && ReactDOM.createPortal({componentToRender}, bodyElement) + ); } diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 61d614dcf2e4..48bb6b713032 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -1,6 +1,15 @@ import type {ReactElement} from 'react'; -type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; +type MeasureParentContainerAndCursor = { + x: number; + y: number; + width: number; + height: number; + scrollValue: number; + cursorCoordinates: {x: number; y: number}; +}; + +type MeasureParentContainerAndCursorCallback = (props: MeasureParentContainerAndCursor) => void; type RenderSuggestionMenuItemProps = { item: TSuggestion; @@ -31,8 +40,8 @@ type AutoCompleteSuggestionsProps = { /** create accessibility label for each item */ accessibilityLabelExtractor: (item: TSuggestion, index: number) => string; - /** Meaures the parent container's position and dimensions. */ - measureParentContainer?: (callback: MeasureParentContainerCallback) => void; + /** Measures the parent container's position and dimensions. Also add a cursor coordinates */ + measureParentContainerAndReportCursor?: (props: MeasureParentContainerAndCursorCallback) => void; }; -export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; +export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps, MeasureParentContainerAndCursorCallback, MeasureParentContainerAndCursor}; 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/Composer/index.tsx b/src/components/Composer/index.tsx index 5bd8aa9175d3..3a8a4e724948 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -91,6 +91,8 @@ function Composer( | { start: number; end?: number; + positionX?: number; + positionY?: number; } | undefined >({ diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 0ff91111bd07..9c7a5a215c1c 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -3,6 +3,12 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelection type TextSelection = { start: number; end?: number; + positionX?: number; + positionY?: number; +}; +type CustomSelectionChangeEvent = NativeSyntheticEvent & { + positionX?: number; + positionY?: number; }; type ComposerProps = TextInputProps & { @@ -45,7 +51,7 @@ type ComposerProps = TextInputProps & { autoFocus?: boolean; /** Update selection position on change */ - onSelectionChange?: (event: NativeSyntheticEvent) => void; + onSelectionChange?: (event: CustomSelectionChangeEvent) => void; /** Selection Object */ selection?: TextSelection; @@ -75,4 +81,4 @@ type ComposerProps = TextInputProps & { isGroupPolicyReport?: boolean; }; -export type {TextSelection, ComposerProps}; +export type {TextSelection, ComposerProps, CustomSelectionChangeEvent}; 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/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 1c0306741048..3781507b544c 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -7,10 +7,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; +import type {MeasureParentContainerAndCursorCallback} from './AutoCompleteSuggestions/types'; import Text from './Text'; -type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; - type EmojiSuggestionsProps = { /** The index of the highlighted emoji */ highlightedEmojiIndex?: number; @@ -33,8 +32,8 @@ type EmojiSuggestionsProps = { /** Stores user's preferred skin tone */ preferredSkinToneIndex: number; - /** Meaures the parent container's position and dimensions. */ - measureParentContainer: (callback: MeasureParentContainerCallback) => void; + /** Measures the parent container's position and dimensions. Also add cursor coordinates */ + measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; }; /** @@ -42,7 +41,15 @@ type EmojiSuggestionsProps = { */ const keyExtractor = (item: Emoji, index: number): string => `${item.name}+${index}}`; -function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { +function EmojiSuggestions({ + emojis, + onSelect, + prefix, + isEmojiPickerLarge, + preferredSkinToneIndex, + highlightedEmojiIndex = 0, + measureParentContainerAndReportCursor = () => {}, +}: EmojiSuggestionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); /** @@ -85,7 +92,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr onSelect={onSelect} isSuggestionPickerLarge={isEmojiPickerLarge} accessibilityLabelExtractor={keyExtractor} - measureParentContainer={measureParentContainer} + measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} /> ); } diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index 3b7d6475262b..925448046076 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -5,11 +5,13 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import type {FeedbackSurveyOptionID} from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import FixedFooter from './FixedFooter'; import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; import SingleOptionSelector from './SingleOptionSelector'; import Text from './Text'; +import TextInput from './TextInput'; type FeedbackSurveyProps = { /** Title of the survey */ @@ -19,14 +21,14 @@ type FeedbackSurveyProps = { description: string; /** Callback to be called when the survey is submitted */ - onSubmit: (reason: Option) => void; + onSubmit: (reason: FeedbackSurveyOptionID, note?: string) => void; /** Styles for the option row element */ optionRowStyles?: StyleProp; }; type Option = { - key: string; + key: FeedbackSurveyOptionID; label: TranslationPaths; }; @@ -44,6 +46,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: Feedbac const selectCircleStyles: StyleProp = {borderColor: theme.border}; const [reason, setReason] = useState