From 256a839c49353ccd8096b22ed580053065c59a5a Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 18 Sep 2023 08:37:50 +0200 Subject: [PATCH 001/184] Revert "Revert "Add focus trap to the RHP"" This reverts commit 6a9f592efdd4c6008acdebbe33823eab19cf68d5. --- package-lock.json | 50 +++++++++++++ package.json | 1 + src/components/FocusTrapView/index.js | 75 +++++++++++++++++++ src/components/FocusTrapView/index.native.js | 11 +++ src/components/ScreenWrapper/index.js | 35 +++++---- src/components/ScreenWrapper/propTypes.js | 8 ++ src/pages/ProfilePage.js | 2 +- src/pages/home/ReportScreen.js | 1 + .../SidebarScreen/BaseSidebarScreen.js | 1 + 9 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 src/components/FocusTrapView/index.js create mode 100644 src/components/FocusTrapView/index.native.js diff --git a/package-lock.json b/package-lock.json index 382dcf45f55e..216a447fda82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", "fbjs": "^3.0.2", + "focus-trap-react": "^10.2.1", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-when": "^3.5.2", @@ -28294,6 +28295,28 @@ "readable-stream": "^2.3.6" } }, + "node_modules/focus-trap": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz", + "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/focus-trap-react": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz", + "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==", + "dependencies": { + "focus-trap": "^7.5.2", + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -44671,6 +44694,11 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", @@ -67682,6 +67710,23 @@ "readable-stream": "^2.3.6" } }, + "focus-trap": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz", + "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==", + "requires": { + "tabbable": "^6.2.0" + } + }, + "focus-trap-react": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.1.tgz", + "integrity": "sha512-UrAKOn52lvfHF6lkUMfFhlQxFgahyNW5i6FpHWkDxAeD4FSk3iwx9n4UEA4Sims0G5WiGIi0fAyoq3/UVeNCYA==", + "requires": { + "focus-trap": "^7.5.2", + "tabbable": "^6.2.0" + } + }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -78809,6 +78854,11 @@ "version": "2.0.15", "dev": true }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "table": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", diff --git a/package.json b/package.json index 0073dedb741c..a2615415d080 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "domhandler": "^4.3.0", "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#35bff866a8d345b460ea6256f0a0f0a8a7f81086", "fbjs": "^3.0.2", + "focus-trap-react": "^10.2.1", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-when": "^3.5.2", diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js new file mode 100644 index 000000000000..2dcab7b9d998 --- /dev/null +++ b/src/components/FocusTrapView/index.js @@ -0,0 +1,75 @@ +/* + * The FocusTrap is only used on web and desktop + */ +import React, {useEffect, useRef} from 'react'; +import FocusTrap from 'focus-trap-react'; +import {View} from 'react-native'; +import {PropTypes} from 'prop-types'; +import {useIsFocused} from '@react-navigation/native'; + +const propTypes = { + /** Children to wrap with FocusTrap */ + children: PropTypes.node.isRequired, + + /** Whether to enable the FocusTrap */ + enabled: PropTypes.bool, + + /** + * Whether to disable auto focus + * It is used when the component inside the FocusTrap have their own auto focus logic + */ + shouldEnableAutoFocus: PropTypes.bool, +}; + +const defaultProps = { + enabled: true, + shouldEnableAutoFocus: false, +}; + +function FocusTrapView({enabled, shouldEnableAutoFocus, ...props}) { + const isFocused = useIsFocused(); + + /** + * Focus trap always needs a focusable element. + * In case that we don't have any focusable elements in the modal, + * the FocusTrap will use fallback View element using this ref. + */ + const ref = useRef(null); + + /** + * We have to set the 'tabindex' attribute to 0 to make the View focusable. + * Currently, it is not possible to set this through props. + * After the upgrade of 'react-native-web' to version 0.19 we can use 'tabIndex={0}' prop instead. + */ + useEffect(() => { + if (!ref.current) { + return; + } + ref.current.setAttribute('tabindex', '0'); + }, []); + + return enabled ? ( + shouldEnableAutoFocus && ref.current, + fallbackFocus: () => ref.current, + clickOutsideDeactivates: true, + }} + > + + + ) : ( + props.children + ); +} + +FocusTrapView.displayName = 'FocusTrapView'; +FocusTrapView.propTypes = propTypes; +FocusTrapView.defaultProps = defaultProps; + +export default FocusTrapView; diff --git a/src/components/FocusTrapView/index.native.js b/src/components/FocusTrapView/index.native.js new file mode 100644 index 000000000000..5720601f5a2b --- /dev/null +++ b/src/components/FocusTrapView/index.native.js @@ -0,0 +1,11 @@ +/* + * The FocusTrap is only used on web and desktop + */ + +function FocusTrapView({children}) { + return children; +} + +FocusTrapView.displayName = 'FocusTrapView'; + +export default FocusTrapView; diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index f760e5d5aeb4..f0f8b8a4b09b 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -3,6 +3,7 @@ import React from 'react'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {PickerAvoidingView} from 'react-native-picker-select'; +import FocusTrapView from '../FocusTrapView'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; import CONST from '../../CONST'; import styles from '../../styles/styles'; @@ -124,20 +125,26 @@ class ScreenWrapper extends React.Component { style={styles.flex1} enabled={this.props.shouldEnablePickerAvoiding} > - - {this.props.environment === CONST.ENVIRONMENT.DEV && } - {this.props.environment === CONST.ENVIRONMENT.DEV && } - { - // If props.children is a function, call it to provide the insets to the children. - _.isFunction(this.props.children) - ? this.props.children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd: this.state.didScreenTransitionEnd, - }) - : this.props.children - } - {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && } + + + {this.props.environment === CONST.ENVIRONMENT.DEV && } + {this.props.environment === CONST.ENVIRONMENT.DEV && } + { + // If props.children is a function, call it to provide the insets to the children. + _.isFunction(this.props.children) + ? this.props.children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd: this.state.didScreenTransitionEnd, + }) + : this.props.children + } + {this.props.isSmallScreenWidth && this.props.shouldShowOfflineIndicator && } + diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js index 83033d9e97b7..c3538b3c026d 100644 --- a/src/components/ScreenWrapper/propTypes.js +++ b/src/components/ScreenWrapper/propTypes.js @@ -48,6 +48,12 @@ const propTypes = { /** Styles for the offline indicator */ offlineIndicatorStyle: stylePropTypes, + + /** Whether to disable the focus trap */ + shouldDisableFocusTrap: PropTypes.bool, + + /** Whether to disable auto focus of the focus trap */ + shouldEnableAutoFocus: PropTypes.bool, }; const defaultProps = { @@ -63,6 +69,8 @@ const defaultProps = { shouldShowOfflineIndicator: true, offlineIndicatorStyle: [], headerGapStyles: [], + shouldDisableFocusTrap: false, + shouldEnableAutoFocus: false, }; export {propTypes, defaultProps}; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 19f2b1fdc0c6..b306164a8ba0 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -144,7 +144,7 @@ function ProfilePage(props) { const chatReportWithCurrentUser = !isCurrentUser && !Session.isAnonymousUser() ? ReportUtils.getChatByParticipants([accountID]) : 0; return ( - + Navigation.goBack(navigateBackTo)} diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a4145843ab87..5e41e33b18a4 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -319,6 +319,7 @@ function ReportScreen({ {({insets}) => ( <> From 8f1c89252af275b33ca430cd205029e99f358ee9 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 18 Sep 2023 09:15:18 +0200 Subject: [PATCH 002/184] fix initial state in the TwoFactorAuthSteps --- .../TwoFactorAuth/TwoFactorAuthSteps.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js index e0094267742b..d06612967ff9 100644 --- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js +++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js @@ -13,22 +13,21 @@ import {defaultAccount, TwoFactorAuthPropTypes} from './TwoFactorAuthPropTypes'; import useAnimatedStepContext from '../../../../components/AnimatedStep/useAnimatedStepContext'; function TwoFactorAuthSteps({account = defaultAccount}) { - const [currentStep, setCurrentStep] = useState(CONST.TWO_FACTOR_AUTH_STEPS.CODES); + const calculateCurrentStep = () => { + if (account.twoFactorAuthStep) { + return account.twoFactorAuthStep; + } + return account.requiresTwoFactorAuth ? CONST.TWO_FACTOR_AUTH_STEPS.ENABLED : CONST.TWO_FACTOR_AUTH_STEPS.CODES; + }; + + const [currentStep, setCurrentStep] = useState(calculateCurrentStep); const {setAnimationDirection} = useAnimatedStepContext(); useEffect(() => () => TwoFactorAuthActions.clearTwoFactorAuthData(), []); useEffect(() => { - if (account.twoFactorAuthStep) { - setCurrentStep(account.twoFactorAuthStep); - return; - } - if (account.requiresTwoFactorAuth) { - setCurrentStep(CONST.TWO_FACTOR_AUTH_STEPS.ENABLED); - } else { - setCurrentStep(CONST.TWO_FACTOR_AUTH_STEPS.CODES); - } + setCurrentStep(calculateCurrentStep); // we don't want to trigger the hook every time the step changes, only when the requiresTwoFactorAuth changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [account.requiresTwoFactorAuth]); From b8ca73d9219dd9b64db8d713e152c977ec5ed774 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:37:33 +0200 Subject: [PATCH 003/184] fix focus issue --- src/components/FocusTrapView/index.js | 11 ++++++----- src/components/KeyboardShortcutsModal.js | 1 + src/components/Modal/index.web.js | 10 +++++++++- src/components/Modal/modalPropTypes.js | 3 +++ src/components/ScreenWrapper/index.js | 1 + 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js index 2dcab7b9d998..38d7ac5730f8 100644 --- a/src/components/FocusTrapView/index.js +++ b/src/components/FocusTrapView/index.js @@ -5,7 +5,6 @@ import React, {useEffect, useRef} from 'react'; import FocusTrap from 'focus-trap-react'; import {View} from 'react-native'; import {PropTypes} from 'prop-types'; -import {useIsFocused} from '@react-navigation/native'; const propTypes = { /** Children to wrap with FocusTrap */ @@ -19,16 +18,18 @@ const propTypes = { * It is used when the component inside the FocusTrap have their own auto focus logic */ shouldEnableAutoFocus: PropTypes.bool, + + /** Whether the FocusTrap is active */ + active: PropTypes.bool, }; const defaultProps = { enabled: true, shouldEnableAutoFocus: false, + active: false, }; -function FocusTrapView({enabled, shouldEnableAutoFocus, ...props}) { - const isFocused = useIsFocused(); - +function FocusTrapView({enabled = true, active = true, shouldEnableAutoFocus, ...props}) { /** * Focus trap always needs a focusable element. * In case that we don't have any focusable elements in the modal, @@ -50,7 +51,7 @@ function FocusTrapView({enabled, shouldEnableAutoFocus, ...props}) { return enabled ? ( shouldEnableAutoFocus && ref.current, fallbackFocus: () => ref.current, diff --git a/src/components/KeyboardShortcutsModal.js b/src/components/KeyboardShortcutsModal.js index 6ca3cce6412c..f262f69a40d9 100644 --- a/src/components/KeyboardShortcutsModal.js +++ b/src/components/KeyboardShortcutsModal.js @@ -158,6 +158,7 @@ function KeyboardShortcutsModal({isShortcutsModalOpen = false, isSmallScreenWidt type={modalType} innerContainerStyle={{...styles.keyboardShortcutModalContainer, ...StyleUtils.getKeyboardShortcutsModalWidth(isSmallScreenWidth)}} onClose={KeyboardShortcutsActions.hideKeyboardShortcutModal} + shouldEnableFocusTrap > - {props.children} + + {props.children} + ); } diff --git a/src/components/Modal/modalPropTypes.js b/src/components/Modal/modalPropTypes.js index 58de5a6c57ca..a5ebcab89fab 100644 --- a/src/components/Modal/modalPropTypes.js +++ b/src/components/Modal/modalPropTypes.js @@ -66,6 +66,8 @@ const propTypes = { * */ hideModalContentWhileAnimating: PropTypes.bool, + shouldEnableFocusTrap: PropTypes.bool, + ...windowDimensionsPropTypes, }; @@ -84,6 +86,7 @@ const defaultProps = { statusBarTranslucent: true, avoidKeyboard: false, hideModalContentWhileAnimating: false, + shouldEnableFocusTrap: false, }; export {propTypes, defaultProps}; diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index f0f8b8a4b09b..c44e9f9c1b51 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -129,6 +129,7 @@ class ScreenWrapper extends React.Component { style={[styles.flex1, styles.noSelect]} enabled={!this.props.shouldDisableFocusTrap} shouldEnableAutoFocus={this.props.shouldEnableAutoFocus} + active={this.props.navigation.isFocused()} > {this.props.environment === CONST.ENVIRONMENT.DEV && } From 550d3852436593a945b3f6ec5b8fc6a56f9b3538 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:01:49 +0200 Subject: [PATCH 004/184] Refactor --- src/components/Modal/modalPropTypes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Modal/modalPropTypes.js b/src/components/Modal/modalPropTypes.js index a5ebcab89fab..f02414a76704 100644 --- a/src/components/Modal/modalPropTypes.js +++ b/src/components/Modal/modalPropTypes.js @@ -66,6 +66,7 @@ const propTypes = { * */ hideModalContentWhileAnimating: PropTypes.bool, + /** Should the modal use custom focus trap logic */ shouldEnableFocusTrap: PropTypes.bool, ...windowDimensionsPropTypes, From 9a0673542daeac40d2ecff339d63478b67d761d1 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:16:07 +0200 Subject: [PATCH 005/184] refactor calculateCurrentStep --- .../Security/TwoFactorAuth/TwoFactorAuthSteps.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js index b93d14510c5d..dad0e5f5007e 100644 --- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js +++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import CodesStep from './Steps/CodesStep'; import DisabledStep from './Steps/DisabledStep'; @@ -13,12 +13,12 @@ import {defaultAccount, TwoFactorAuthPropTypes} from './TwoFactorAuthPropTypes'; import useAnimatedStepContext from '../../../../components/AnimatedStep/useAnimatedStepContext'; function TwoFactorAuthSteps({account = defaultAccount}) { - const calculateCurrentStep = () => { + const calculateCurrentStep = useMemo(() => { if (account.twoFactorAuthStep) { return account.twoFactorAuthStep; } return account.requiresTwoFactorAuth ? CONST.TWO_FACTOR_AUTH_STEPS.ENABLED : CONST.TWO_FACTOR_AUTH_STEPS.CODES; - }; + }, [account.requiresTwoFactorAuth, account.twoFactorAuthStep]); const [currentStep, setCurrentStep] = useState(calculateCurrentStep); @@ -28,8 +28,7 @@ function TwoFactorAuthSteps({account = defaultAccount}) { useEffect(() => { setCurrentStep(calculateCurrentStep); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [account.requiresTwoFactorAuth, account.twoFactorAuthStep]); + }, [calculateCurrentStep]); const handleSetStep = useCallback( (step, animationDirection = CONST.ANIMATION_DIRECTION.IN) => { From be7a36826f4dd6d56b1ecc39b5c089a420338ccd Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:36:37 +0200 Subject: [PATCH 006/184] use useIsFocused hook --- src/components/ScreenWrapper/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 607ca4fef7da..be57ed44f4bc 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -3,7 +3,7 @@ import React, {useEffect, useRef, useState} from 'react'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {PickerAvoidingView} from 'react-native-picker-select'; -import {useNavigation} from '@react-navigation/native'; +import {useNavigation, useIsFocused} from '@react-navigation/native'; import FocusTrapView from '../FocusTrapView'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; import CONST from '../../CONST'; @@ -44,6 +44,7 @@ function ScreenWrapper({ const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); const navigation = useNavigation(); + const isFocused = useIsFocused(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const isKeyboardShown = lodashGet(keyboardState, 'isKeyboardShown', false); @@ -140,7 +141,7 @@ function ScreenWrapper({ style={[styles.flex1, styles.noSelect]} enabled={!shouldDisableFocusTrap} shouldEnableAutoFocus={shouldEnableAutoFocus} - active={navigation.isFocused()} + active={isFocused} > {isDevelopment && } From 9fb1ff75e914e1316693b20e4af14a453eb63196 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:10:29 +0200 Subject: [PATCH 007/184] enable focus trap in the ConfirmModal --- src/components/ConfirmModal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ConfirmModal.js b/src/components/ConfirmModal.js index 705a05ec2058..491c6b834ce4 100755 --- a/src/components/ConfirmModal.js +++ b/src/components/ConfirmModal.js @@ -98,6 +98,7 @@ function ConfirmModal(props) { shouldSetModalVisibility={props.shouldSetModalVisibility} onModalHide={props.onModalHide} type={props.isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} + shouldEnableFocusTrap > Date: Thu, 19 Oct 2023 10:05:34 +0200 Subject: [PATCH 008/184] migrate all files to TypeScript --- .../AnimatedStep/AnimatedStepContext.js | 5 --- .../AnimatedStep/AnimatedStepContext.ts | 15 +++++++ .../AnimatedStep/AnimatedStepProvider.js | 17 -------- .../AnimatedStep/AnimatedStepProvider.tsx | 15 +++++++ .../AnimatedStep/{index.js => index.tsx} | 40 +++++++------------ ...epContext.js => useAnimatedStepContext.ts} | 4 +- 6 files changed, 47 insertions(+), 49 deletions(-) delete mode 100644 src/components/AnimatedStep/AnimatedStepContext.js create mode 100644 src/components/AnimatedStep/AnimatedStepContext.ts delete mode 100644 src/components/AnimatedStep/AnimatedStepProvider.js create mode 100644 src/components/AnimatedStep/AnimatedStepProvider.tsx rename src/components/AnimatedStep/{index.js => index.tsx} (54%) rename src/components/AnimatedStep/{useAnimatedStepContext.js => useAnimatedStepContext.ts} (69%) diff --git a/src/components/AnimatedStep/AnimatedStepContext.js b/src/components/AnimatedStep/AnimatedStepContext.js deleted file mode 100644 index 30377147fdb8..000000000000 --- a/src/components/AnimatedStep/AnimatedStepContext.js +++ /dev/null @@ -1,5 +0,0 @@ -import {createContext} from 'react'; - -const AnimatedStepContext = createContext(); - -export default AnimatedStepContext; diff --git a/src/components/AnimatedStep/AnimatedStepContext.ts b/src/components/AnimatedStep/AnimatedStepContext.ts new file mode 100644 index 000000000000..eb68f67953c3 --- /dev/null +++ b/src/components/AnimatedStep/AnimatedStepContext.ts @@ -0,0 +1,15 @@ +import React, {createContext} from 'react'; +import {ValueOf} from 'type-fest'; +import CONST from '../../CONST'; + +type AnimationDirection = ValueOf; + +type StepContext = { + animationDirection: AnimationDirection; + setAnimationDirection: React.Dispatch>>; +}; + +const AnimatedStepContext = createContext(null); + +export default AnimatedStepContext; +export type {StepContext, AnimationDirection}; diff --git a/src/components/AnimatedStep/AnimatedStepProvider.js b/src/components/AnimatedStep/AnimatedStepProvider.js deleted file mode 100644 index 280fbd1a2776..000000000000 --- a/src/components/AnimatedStep/AnimatedStepProvider.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, {useState} from 'react'; -import PropTypes from 'prop-types'; -import AnimatedStepContext from './AnimatedStepContext'; -import CONST from '../../CONST'; - -const propTypes = { - children: PropTypes.node.isRequired, -}; - -function AnimatedStepProvider({children}) { - const [animationDirection, setAnimationDirection] = useState(CONST.ANIMATION_DIRECTION.IN); - - return {children}; -} - -AnimatedStepProvider.propTypes = propTypes; -export default AnimatedStepProvider; diff --git a/src/components/AnimatedStep/AnimatedStepProvider.tsx b/src/components/AnimatedStep/AnimatedStepProvider.tsx new file mode 100644 index 000000000000..ea114c3e87b8 --- /dev/null +++ b/src/components/AnimatedStep/AnimatedStepProvider.tsx @@ -0,0 +1,15 @@ +import React, {useState} from 'react'; +import AnimatedStepContext, {AnimationDirection} from './AnimatedStepContext'; +import CONST from '../../CONST'; +import ChildrenProps from '../../types/utils/ChildrenProps'; + +type AnimatedStepProviderProps = ChildrenProps; + +function AnimatedStepProvider({children}: AnimatedStepProviderProps): React.ReactNode { + const [animationDirection, setAnimationDirection] = useState(CONST.ANIMATION_DIRECTION.IN); + + return {children}; +} + +AnimatedStepProvider.displayName = 'AnimatedStepProvider'; +export default AnimatedStepProvider; diff --git a/src/components/AnimatedStep/index.js b/src/components/AnimatedStep/index.tsx similarity index 54% rename from src/components/AnimatedStep/index.js rename to src/components/AnimatedStep/index.tsx index 5b0dc8bc78fa..45078b193ed1 100644 --- a/src/components/AnimatedStep/index.js +++ b/src/components/AnimatedStep/index.tsx @@ -1,62 +1,52 @@ import React from 'react'; -import PropTypes from 'prop-types'; import * as Animatable from 'react-native-animatable'; +import {StyleProp, ViewStyle} from 'react-native'; import CONST from '../../CONST'; import styles from '../../styles/styles'; import useNativeDriver from '../../libs/useNativeDriver'; +import {AnimationDirection} from './AnimatedStepContext'; +import ChildrenProps from '../../types/utils/ChildrenProps'; -const propTypes = { - /** Children to wrap in AnimatedStep. */ - children: PropTypes.node.isRequired, - +type AnimatedStepProps = ChildrenProps & { /** Styles to be assigned to Container */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.arrayOf(PropTypes.object), + style: StyleProp; /** Whether we're animating the step in or out */ - direction: PropTypes.oneOf(['in', 'out']), + direction: AnimationDirection; /** Callback to fire when the animation ends */ - onAnimationEnd: PropTypes.func, -}; - -const defaultProps = { - direction: 'in', - style: [], - onAnimationEnd: () => {}, + onAnimationEnd: () => void; }; -function getAnimationStyle(direction) { +function getAnimationStyle(direction: AnimationDirection) { let transitionValue; if (direction === 'in') { transitionValue = CONST.ANIMATED_TRANSITION_FROM_VALUE; - } else if (direction === 'out') { + } else { transitionValue = -CONST.ANIMATED_TRANSITION_FROM_VALUE; } return styles.makeSlideInTranslation('translateX', transitionValue); } -function AnimatedStep(props) { +function AnimatedStep({onAnimationEnd, direction = 'in', style = [], children}: AnimatedStepProps) { return ( { - if (!props.onAnimationEnd) { + if (!onAnimationEnd) { return; } - props.onAnimationEnd(); + onAnimationEnd(); }} duration={CONST.ANIMATED_TRANSITION} - animation={getAnimationStyle(props.direction)} + animation={getAnimationStyle(direction)} useNativeDriver={useNativeDriver} - style={props.style} + style={style} > - {props.children} + {children} ); } -AnimatedStep.propTypes = propTypes; -AnimatedStep.defaultProps = defaultProps; AnimatedStep.displayName = 'AnimatedStep'; export default AnimatedStep; diff --git a/src/components/AnimatedStep/useAnimatedStepContext.js b/src/components/AnimatedStep/useAnimatedStepContext.ts similarity index 69% rename from src/components/AnimatedStep/useAnimatedStepContext.js rename to src/components/AnimatedStep/useAnimatedStepContext.ts index e2af9514e20e..3edc71e5289e 100644 --- a/src/components/AnimatedStep/useAnimatedStepContext.js +++ b/src/components/AnimatedStep/useAnimatedStepContext.ts @@ -1,7 +1,7 @@ import {useContext} from 'react'; -import AnimatedStepContext from './AnimatedStepContext'; +import AnimatedStepContext, {StepContext} from './AnimatedStepContext'; -function useAnimatedStepContext() { +function useAnimatedStepContext(): StepContext { const context = useContext(AnimatedStepContext); if (!context) { throw new Error('useAnimatedStepContext must be used within an AnimatedStepContextProvider'); From e2a294e2826c72df37825050950f2b1a2a484305 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 19 Oct 2023 11:23:22 +0200 Subject: [PATCH 009/184] minor style changes --- src/components/AnimatedStep/AnimatedStepProvider.tsx | 4 +--- src/components/AnimatedStep/index.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/AnimatedStep/AnimatedStepProvider.tsx b/src/components/AnimatedStep/AnimatedStepProvider.tsx index ea114c3e87b8..a6896e76298d 100644 --- a/src/components/AnimatedStep/AnimatedStepProvider.tsx +++ b/src/components/AnimatedStep/AnimatedStepProvider.tsx @@ -3,9 +3,7 @@ import AnimatedStepContext, {AnimationDirection} from './AnimatedStepContext'; import CONST from '../../CONST'; import ChildrenProps from '../../types/utils/ChildrenProps'; -type AnimatedStepProviderProps = ChildrenProps; - -function AnimatedStepProvider({children}: AnimatedStepProviderProps): React.ReactNode { +function AnimatedStepProvider({children}: ChildrenProps): React.ReactNode { const [animationDirection, setAnimationDirection] = useState(CONST.ANIMATION_DIRECTION.IN); return {children}; diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx index 45078b193ed1..f843768ed630 100644 --- a/src/components/AnimatedStep/index.tsx +++ b/src/components/AnimatedStep/index.tsx @@ -29,7 +29,7 @@ function getAnimationStyle(direction: AnimationDirection) { return styles.makeSlideInTranslation('translateX', transitionValue); } -function AnimatedStep({onAnimationEnd, direction = 'in', style = [], children}: AnimatedStepProps) { +function AnimatedStep({onAnimationEnd, direction = CONST.ANIMATION_DIRECTION.IN, style = [], children}: AnimatedStepProps) { return ( { From 6bd4978a272f7581d92ea7b97d85cab817634450 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 20 Oct 2023 13:43:11 +0200 Subject: [PATCH 010/184] use AnimationDirection type --- src/components/AnimatedStep/AnimatedStepContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AnimatedStep/AnimatedStepContext.ts b/src/components/AnimatedStep/AnimatedStepContext.ts index eb68f67953c3..e1bf35dd31c9 100644 --- a/src/components/AnimatedStep/AnimatedStepContext.ts +++ b/src/components/AnimatedStep/AnimatedStepContext.ts @@ -6,7 +6,7 @@ type AnimationDirection = ValueOf; type StepContext = { animationDirection: AnimationDirection; - setAnimationDirection: React.Dispatch>>; + setAnimationDirection: React.Dispatch>; }; const AnimatedStepContext = createContext(null); From f89d89f9e7122106968c1eee7b301ef0e6f65bb3 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 30 Oct 2023 18:12:47 +0100 Subject: [PATCH 011/184] chore: init migration to flashlist for BaseAutoCompleteSuggestions --- .../BaseAutoCompleteSuggestions.js | 26 +++---------------- src/styles/StyleUtils.ts | 1 + 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js index c024b025c80e..a8e07e0255e6 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js @@ -1,6 +1,5 @@ +import {FlashList} from '@shopify/flash-list'; import React, {useEffect, useRef} from 'react'; -// We take FlatList from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another -import {FlatList} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import styles from '@styles/styles'; @@ -51,24 +50,6 @@ function BaseAutoCompleteSuggestions(props) { ); - /** - * This function is used to compute the layout of any given item in our list. Since we know that each item will have the exact same height, this is a performance optimization - * so that the heights can be determined before the options are rendered. Otherwise, the heights are determined when each option is rendering and it causes a lot of overhead on large - * lists. - * - * Also, `scrollToIndex` should be used in conjunction with `getItemLayout`, otherwise there is no way to know the location of offscreen indices or handle failures. - * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} index the current item's index in the set of data - * - * @returns {Object} - */ - const getItemLayout = (data, index) => ({ - length: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, - offset: index * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, - index, - }); - const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * props.suggestions.length; const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); @@ -92,7 +73,8 @@ function BaseAutoCompleteSuggestions(props) { style={[styles.autoCompleteSuggestionsContainer, animatedStyles]} exiting={FadeOutDown.duration(100).easing(Easing.inOut(Easing.ease))} > - rowHeight.value} - style={{flex: 1}} - getItemLayout={getItemLayout} /> ); diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index f58d2c9a236d..76033d4092b4 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1006,6 +1006,7 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle overflow: 'hidden', top: -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth), height, + minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; } From d921edb32f8939ed463fff56a351266813cfbebe Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 26 Oct 2023 09:08:50 +0200 Subject: [PATCH 012/184] Refactor save method in PersistedRequests --- src/libs/actions/PersistedRequests.ts | 43 +++++++++++++++++++++++++-- src/libs/actions/Report.js | 1 + src/types/onyx/Request.ts | 1 + 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index c35de9ee94c4..cf714271263b 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,5 +1,5 @@ +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import isEqual from 'lodash/isEqual'; -import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; @@ -17,10 +17,49 @@ function clear() { return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } +function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = []): OnyxUpdate[] { + const mergedData = [...newData]; + + oldData.forEach((oldUpdate) => { + const hasSameKey = newData.some((newUpdate) => newUpdate.key === oldUpdate.key); + + if (!hasSameKey) { + mergedData.push(oldUpdate); + } + }); + + return mergedData; +} + +function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { + const updatedRequest = { + failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), + successData: mergeOnyxUpdateData(oldRequest.successData, newRequest.successData), + ...newRequest, + }; + + const updatedOptimisticData = mergeOnyxUpdateData(oldRequest.optimisticData, newRequest.optimisticData); + + if (updatedOptimisticData.length > 0) { + updatedRequest.optimisticData = updatedOptimisticData; + } + + return updatedRequest; +} + function save(requestsToPersist: Request[]) { let requests: Request[] = []; + if (persistedRequests.length) { - requests = persistedRequests.concat(requestsToPersist); + requests = [...persistedRequests]; + requestsToPersist.forEach((requestToPersist) => { + const index = persistedRequests.findIndex((persistedRequest) => !!requestToPersist.idempotencyKey && requestToPersist.idempotencyKey === persistedRequest.idempotencyKey); + if (index > -1) { + requests[index] = createUpdatedRequest(requests[index], requestToPersist); + } else { + requests.push(requestToPersist); + } + }); } else { requests = requestsToPersist; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 3f7dc76b174d..afbe96273ca2 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -521,6 +521,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p optimisticData: optimisticReportData, successData: reportSuccessData, failureData: reportFailureData, + idempotencyKey: `OpenReport_${reportID}`, }; const params = { diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 836138ca99ba..8f0121a31fcb 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -5,6 +5,7 @@ type OnyxData = { successData?: OnyxUpdate[]; failureData?: OnyxUpdate[]; optimisticData?: OnyxUpdate[]; + idempotencyKey?: string; }; type RequestData = { From b37e1bad02ee4ae474702f01266d873e260ae837 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 30 Oct 2023 10:19:33 +0100 Subject: [PATCH 013/184] Run prettier --- src/libs/actions/PersistedRequests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index cf714271263b..ea1f4e9b852d 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,5 +1,5 @@ -import Onyx, {OnyxUpdate} from 'react-native-onyx'; import isEqual from 'lodash/isEqual'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; From f2141610cab49d0fd454c080016bff94c36a413c Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 31 Oct 2023 10:08:43 +0100 Subject: [PATCH 014/184] Merge requests params --- src/libs/actions/PersistedRequests.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index ea1f4e9b852d..27e52a13c743 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,3 +1,4 @@ +import {merge} from 'lodash'; import isEqual from 'lodash/isEqual'; import Onyx, {OnyxUpdate} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -33,6 +34,7 @@ function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { const updatedRequest = { + data: merge({...oldRequest.data}, newRequest.data), failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), successData: mergeOnyxUpdateData(oldRequest.successData, newRequest.successData), ...newRequest, From 059306ca76764c86aebbed608234ff3e5f1d0e45 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 31 Oct 2023 11:20:46 +0100 Subject: [PATCH 015/184] Add comments --- src/libs/actions/PersistedRequests.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 27e52a13c743..59959318d0eb 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -18,6 +18,10 @@ function clear() { return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } +/** + * Method to merge two arrays of OnyxUpdate elements. + * Elements from the old array which keys are not present in the new array are merged with the data of the new array. + */ function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = []): OnyxUpdate[] { const mergedData = [...newData]; @@ -33,6 +37,9 @@ function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = } function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { + /** + * In order to create updated request, properties: data, failureData, successData and optimisticData have to be merged + */ const updatedRequest = { data: merge({...oldRequest.data}, newRequest.data), failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), @@ -54,6 +61,10 @@ function save(requestsToPersist: Request[]) { if (persistedRequests.length) { requests = [...persistedRequests]; + /** + * When we add a new request to the persistedRequests array, firstly we should check if the array already contains a request with the same idempotency key as the new request. + * If we find a matching request, we should update it, otherwise the new request will be added to the array. + */ requestsToPersist.forEach((requestToPersist) => { const index = persistedRequests.findIndex((persistedRequest) => !!requestToPersist.idempotencyKey && requestToPersist.idempotencyKey === persistedRequest.idempotencyKey); if (index > -1) { From 6b0cd8f3f3cc4343f25a2a3d711a5820cc579a39 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 31 Oct 2023 14:56:56 +0100 Subject: [PATCH 016/184] fix linting --- src/components/AnimatedStep/AnimatedStepProvider.tsx | 2 +- src/components/AnimatedStep/index.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AnimatedStep/AnimatedStepProvider.tsx b/src/components/AnimatedStep/AnimatedStepProvider.tsx index 830876f28c56..0e9e61514810 100644 --- a/src/components/AnimatedStep/AnimatedStepProvider.tsx +++ b/src/components/AnimatedStep/AnimatedStepProvider.tsx @@ -1,7 +1,7 @@ import React, {useMemo, useState} from 'react'; -import AnimatedStepContext, {AnimationDirection} from './AnimatedStepContext'; import CONST from '../../CONST'; import ChildrenProps from '../../types/utils/ChildrenProps'; +import AnimatedStepContext, {AnimationDirection} from './AnimatedStepContext'; function AnimatedStepProvider({children}: ChildrenProps): React.ReactNode { const [animationDirection, setAnimationDirection] = useState(CONST.ANIMATION_DIRECTION.IN); diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx index f843768ed630..d5b3ee2d5fec 100644 --- a/src/components/AnimatedStep/index.tsx +++ b/src/components/AnimatedStep/index.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import * as Animatable from 'react-native-animatable'; import {StyleProp, ViewStyle} from 'react-native'; +import * as Animatable from 'react-native-animatable'; import CONST from '../../CONST'; -import styles from '../../styles/styles'; import useNativeDriver from '../../libs/useNativeDriver'; -import {AnimationDirection} from './AnimatedStepContext'; +import styles from '../../styles/styles'; import ChildrenProps from '../../types/utils/ChildrenProps'; +import {AnimationDirection} from './AnimatedStepContext'; type AnimatedStepProps = ChildrenProps & { /** Styles to be assigned to Container */ From a12e7a77a152ae81d18861f8ccd6606461518e18 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 31 Oct 2023 15:23:09 +0100 Subject: [PATCH 017/184] fix imports --- src/components/AnimatedStep/AnimatedStepContext.ts | 2 +- src/components/AnimatedStep/index.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AnimatedStep/AnimatedStepContext.ts b/src/components/AnimatedStep/AnimatedStepContext.ts index e1bf35dd31c9..3b4c5f79a34f 100644 --- a/src/components/AnimatedStep/AnimatedStepContext.ts +++ b/src/components/AnimatedStep/AnimatedStepContext.ts @@ -1,6 +1,6 @@ import React, {createContext} from 'react'; import {ValueOf} from 'type-fest'; -import CONST from '../../CONST'; +import CONST from '@src/CONST'; type AnimationDirection = ValueOf; diff --git a/src/components/AnimatedStep/index.tsx b/src/components/AnimatedStep/index.tsx index d5b3ee2d5fec..607f4f0a4b11 100644 --- a/src/components/AnimatedStep/index.tsx +++ b/src/components/AnimatedStep/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; import {StyleProp, ViewStyle} from 'react-native'; import * as Animatable from 'react-native-animatable'; -import CONST from '../../CONST'; -import useNativeDriver from '../../libs/useNativeDriver'; -import styles from '../../styles/styles'; -import ChildrenProps from '../../types/utils/ChildrenProps'; +import useNativeDriver from '@libs/useNativeDriver'; +import styles from '@styles/styles'; +import CONST from '@src/CONST'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import {AnimationDirection} from './AnimatedStepContext'; type AnimatedStepProps = ChildrenProps & { From b721afce53950b60c29edd91fa593ef8add687e0 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 31 Oct 2023 15:24:36 +0100 Subject: [PATCH 018/184] fix imports in AnimatedStepProvider --- src/components/AnimatedStep/AnimatedStepProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AnimatedStep/AnimatedStepProvider.tsx b/src/components/AnimatedStep/AnimatedStepProvider.tsx index 0e9e61514810..53b3a0e0a53d 100644 --- a/src/components/AnimatedStep/AnimatedStepProvider.tsx +++ b/src/components/AnimatedStep/AnimatedStepProvider.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useState} from 'react'; -import CONST from '../../CONST'; -import ChildrenProps from '../../types/utils/ChildrenProps'; +import CONST from '@src/CONST'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import AnimatedStepContext, {AnimationDirection} from './AnimatedStepContext'; function AnimatedStepProvider({children}: ChildrenProps): React.ReactNode { From d2122bc79b62a401d7d8499a430854b8973e1ec1 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 31 Oct 2023 15:57:11 +0100 Subject: [PATCH 019/184] Refactor import of lodash merge function --- src/libs/actions/PersistedRequests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 59959318d0eb..87881d610d14 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,5 +1,5 @@ -import {merge} from 'lodash'; import isEqual from 'lodash/isEqual'; +import lodashMerge from 'lodash/merge'; import Onyx, {OnyxUpdate} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; @@ -41,7 +41,7 @@ function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request * In order to create updated request, properties: data, failureData, successData and optimisticData have to be merged */ const updatedRequest = { - data: merge({...oldRequest.data}, newRequest.data), + data: lodashMerge({...oldRequest.data}, newRequest.data), failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), successData: mergeOnyxUpdateData(oldRequest.successData, newRequest.successData), ...newRequest, From ee61cff28ec73396f60d31e62257c8035fd21833 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 31 Oct 2023 17:05:43 +0100 Subject: [PATCH 020/184] fix: use memoised values on mentionSuggestions --- src/pages/home/report/ReportActionCompose/SuggestionMention.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index baf93da6ccc4..23a58407aa0e 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -290,7 +290,7 @@ function SuggestionMention({ highlightedMentionIndex={highlightedMentionIndex} mentions={suggestionValues.suggestedMentions} comment={value} - updateComment={(newComment) => setValue(newComment)} + updateComment={setValue} colonIndex={suggestionValues.colonIndex} prefix={suggestionValues.mentionPrefix} onSelect={insertSelectedMention} From 0fd026394cf0a1e6e0aee33a63e6884dfb2a534c Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 31 Oct 2023 17:05:22 +0100 Subject: [PATCH 021/184] fix: use memoised values on emojiSuggestions --- .../home/report/ReportActionCompose/SuggestionEmoji.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js index dc84f77b6311..88f0d0a68c67 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js @@ -201,6 +201,10 @@ function SuggestionEmoji({ const getSuggestions = useCallback(() => suggestionValues.suggestedEmojis, [suggestionValues]); + const resetEmojiSuggestions = useCallback(() => { + setSuggestionValues((prevState) => ({...prevState, suggestedEmojis: []})); + }, []); + useImperativeHandle( forwardedRef, () => ({ @@ -220,11 +224,11 @@ function SuggestionEmoji({ return ( setSuggestionValues((prevState) => ({...prevState, suggestedEmojis: []}))} + onClose={resetEmojiSuggestions} highlightedEmojiIndex={highlightedEmojiIndex} emojis={suggestionValues.suggestedEmojis} comment={value} - updateComment={(newComment) => setValue(newComment)} + updateComment={setValue} colonIndex={suggestionValues.colonIndex} prefix={value.slice(suggestionValues.colonIndex + 1, selection.start)} onSelect={insertSelectedEmoji} From 9747b20d05885ed4d64612ddcf74c3f3e82781d0 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:21:27 +0100 Subject: [PATCH 022/184] prettier --- src/components/FocusTrapView/index.js | 4 ++-- src/components/ScreenWrapper/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js index 38d7ac5730f8..647f037af805 100644 --- a/src/components/FocusTrapView/index.js +++ b/src/components/FocusTrapView/index.js @@ -1,10 +1,10 @@ /* * The FocusTrap is only used on web and desktop */ -import React, {useEffect, useRef} from 'react'; import FocusTrap from 'focus-trap-react'; -import {View} from 'react-native'; import {PropTypes} from 'prop-types'; +import React, {useEffect, useRef} from 'react'; +import {View} from 'react-native'; const propTypes = { /** Children to wrap with FocusTrap */ diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 11669cdf001e..889c898a6bf6 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -5,6 +5,7 @@ import {Keyboard, PanResponder, View} from 'react-native'; import {PickerAvoidingView} from 'react-native-picker-select'; import _ from 'underscore'; import CustomDevMenu from '@components/CustomDevMenu'; +import FocusTrapView from '@components/FocusTrapView'; import HeaderGap from '@components/HeaderGap'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; @@ -19,7 +20,6 @@ import * as Browser from '@libs/Browser'; import styles from '@styles/styles'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; -import FocusTrapView from '../FocusTrapView'; import {defaultProps, propTypes} from './propTypes'; function ScreenWrapper({ From 0ff0eee319b258bb81565cd9ea11ce3cabc07a05 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 3 Nov 2023 11:14:40 +0700 Subject: [PATCH 023/184] fix account number regex condition --- src/CONST.ts | 1 + src/pages/ReimbursementAccount/BankAccountManualStep.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 29bb0b83aaee..a0839cf878a1 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -218,6 +218,7 @@ const CONST = { // If the account number length is from 4 to 13 digits, we show the last 4 digits and hide the rest with X // If the length is longer than 13 digits, we show the first 6 and last 4 digits, hiding the rest with X MASKED_US_ACCOUNT_NUMBER: /^[X]{0,9}[0-9]{4}$|^[0-9]{6}[X]{4,7}[0-9]{4}$/, + MASKED_US_ACCOUNT_NUMBER_MATCH_BE: /^[X]{0,13}[0-9]{4}$/, // we can change the name later SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/, }, VERIFICATION_MAX_ATTEMPTS: 7, diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index 13155d286a5e..9c7b64efce90 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -40,7 +40,7 @@ function BankAccountManualStep(props) { 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()) + !(shouldDisableInputs && CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER_MATCH_BE.test(values.accountNumber.trim())) ) { errors.accountNumber = 'bankAccount.error.accountNumber'; } else if (values.accountNumber && values.accountNumber === routingNumber) { From 87b0d782b24a86b5390f4570d4b3ef84701d3e0b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 3 Nov 2023 11:28:48 +0700 Subject: [PATCH 024/184] fix lint error --- src/pages/ReimbursementAccount/BankAccountManualStep.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index 9c7b64efce90..dbd169dfacb0 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -27,6 +27,9 @@ const propTypes = { function BankAccountManualStep(props) { const {translate, preferredLocale} = useLocalize(); const {reimbursementAccount, reimbursementAccountDraft} = props; + + const shouldDisableInputs = Boolean(lodashGet(reimbursementAccount, 'achData.bankAccountID')); + /** * @param {Object} values - form input values passed by the Form component * @returns {Object} @@ -55,7 +58,7 @@ function BankAccountManualStep(props) { return errors; }, - [translate], + [translate, shouldDisableInputs], ); const submit = useCallback( @@ -70,8 +73,6 @@ function BankAccountManualStep(props) { [reimbursementAccount, reimbursementAccountDraft], ); - const shouldDisableInputs = Boolean(lodashGet(reimbursementAccount, 'achData.bankAccountID')); - return ( Date: Fri, 3 Nov 2023 10:29:07 +0100 Subject: [PATCH 025/184] Refactor methods to send requests --- src/libs/Network/SequentialQueue.ts | 2 +- src/libs/Network/enhanceParameters.ts | 3 ++ src/libs/actions/PersistedRequests.ts | 71 +++++---------------------- src/libs/actions/Report.js | 7 +-- src/types/onyx/Request.ts | 2 +- 5 files changed, 20 insertions(+), 65 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index d4aee4a221e5..5da032baaf45 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -160,7 +160,7 @@ NetworkStore.onReconnection(flush); function push(request: OnyxRequest) { // Add request to Persisted Requests so that it can be retried if it fails - PersistedRequests.save([request]); + PersistedRequests.save(request); // If we are offline we don't need to trigger the queue to empty as it will happen when we come back online if (NetworkStore.isOffline()) { diff --git a/src/libs/Network/enhanceParameters.ts b/src/libs/Network/enhanceParameters.ts index 6ff54f94bc88..3fadeea7447c 100644 --- a/src/libs/Network/enhanceParameters.ts +++ b/src/libs/Network/enhanceParameters.ts @@ -37,5 +37,8 @@ export default function enhanceParameters(command: string, parameters: Record { - const hasSameKey = newData.some((newUpdate) => newUpdate.key === oldUpdate.key); - - if (!hasSameKey) { - mergedData.push(oldUpdate); - } - }); - - return mergedData; -} - -function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { - /** - * In order to create updated request, properties: data, failureData, successData and optimisticData have to be merged - */ - const updatedRequest = { - data: lodashMerge({...oldRequest.data}, newRequest.data), - failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), - successData: mergeOnyxUpdateData(oldRequest.successData, newRequest.successData), - ...newRequest, - }; - - const updatedOptimisticData = mergeOnyxUpdateData(oldRequest.optimisticData, newRequest.optimisticData); - - if (updatedOptimisticData.length > 0) { - updatedRequest.optimisticData = updatedOptimisticData; - } - - return updatedRequest; -} - -function save(requestsToPersist: Request[]) { - let requests: Request[] = []; - - if (persistedRequests.length) { - requests = [...persistedRequests]; - /** - * When we add a new request to the persistedRequests array, firstly we should check if the array already contains a request with the same idempotency key as the new request. - * If we find a matching request, we should update it, otherwise the new request will be added to the array. - */ - requestsToPersist.forEach((requestToPersist) => { - const index = persistedRequests.findIndex((persistedRequest) => !!requestToPersist.idempotencyKey && requestToPersist.idempotencyKey === persistedRequest.idempotencyKey); - if (index > -1) { - requests[index] = createUpdatedRequest(requests[index], requestToPersist); - } else { - requests.push(requestToPersist); - } - }); +function save(requestToPersist: Request) { + // Check for a request w/ matching idempotencyKey in the queue + const existingRequestIndex = persistedRequests.findIndex((request) => request.data?.idempotencyKey && request.data?.idempotencyKey === requestToPersist.data?.idempotencyKey); + if (existingRequestIndex > -1) { + // Merge the new request into the existing one, keeping its place in the queue + persistedRequests.splice(existingRequestIndex, 1, merge(persistedRequests[existingRequestIndex], requestToPersist)); } else { - requests = requestsToPersist; + // If not, push the new request to the end of the queue + persistedRequests.push(requestToPersist); } - persistedRequests = requests; - Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } function remove(requestToRemove: Request) { diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index afbe96273ca2..87a14aac89cb 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -463,6 +463,7 @@ function reportActionsExist(reportID) { * @param {Array} participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it */ function openReport(reportID, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { + const commandName = 'OpenReport'; const optimisticReportData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -521,7 +522,6 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p optimisticData: optimisticReportData, successData: reportSuccessData, failureData: reportFailureData, - idempotencyKey: `OpenReport_${reportID}`, }; const params = { @@ -529,6 +529,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', parentReportActionID, + idempotencyKey: `${commandName}_${reportID}`, }; if (isFromDeepLink) { @@ -626,12 +627,12 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p if (isFromDeepLink) { // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('OpenReport', params, onyxData).finally(() => { + API.makeRequestWithSideEffects(commandName, params, onyxData).finally(() => { Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); }); } else { // eslint-disable-next-line rulesdir/no-multiple-api-calls - API.write('OpenReport', params, onyxData); + API.write(commandName, params, onyxData); } } diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 8f0121a31fcb..c97a5a21f488 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -5,7 +5,6 @@ type OnyxData = { successData?: OnyxUpdate[]; failureData?: OnyxUpdate[]; optimisticData?: OnyxUpdate[]; - idempotencyKey?: string; }; type RequestData = { @@ -16,6 +15,7 @@ type RequestData = { shouldUseSecure?: boolean; successData?: OnyxUpdate[]; failureData?: OnyxUpdate[]; + idempotencyKey?: string; resolve?: (value: Response) => void; reject?: (value?: unknown) => void; From 8616bd2293d096636449dc7303aa27999c3f9a5e Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 3 Nov 2023 13:41:51 +0100 Subject: [PATCH 026/184] Add some basic tests for PersistedRequests methods --- src/libs/actions/PersistedRequests.ts | 42 ++++++++++++++++- tests/unit/PersistedRequestsTest.ts | 65 +++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 tests/unit/PersistedRequestsTest.ts diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index dbe1c6e8bef2..459b24a9a1b6 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,6 +1,6 @@ import isEqual from 'lodash/isEqual'; import merge from 'lodash/merge'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; @@ -18,12 +18,50 @@ function clear() { return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } +/** + * Method to merge two arrays of OnyxUpdate elements. + * Elements from the old array which keys are not present in the new array are merged with the data of the new array. + */ +function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = []): OnyxUpdate[] { + const mergedData = newData; + + oldData.forEach((oldUpdate) => { + const hasSameKey = newData.some((newUpdate) => newUpdate.key === oldUpdate.key); + + if (!hasSameKey) { + mergedData.push(oldUpdate); + } + }); + + return mergedData; +} + +function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { + /** + * In order to create updated request, properties: data, failureData, successData and optimisticData have to be merged + */ + const updatedRequest = { + data: merge(oldRequest.data, newRequest.data), + failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), + successData: mergeOnyxUpdateData(oldRequest.successData, newRequest.successData), + ...newRequest, + }; + + const updatedOptimisticData = mergeOnyxUpdateData(oldRequest.optimisticData, newRequest.optimisticData); + + if (updatedOptimisticData.length > 0) { + updatedRequest.optimisticData = updatedOptimisticData; + } + + return updatedRequest; +} + function save(requestToPersist: Request) { // Check for a request w/ matching idempotencyKey in the queue const existingRequestIndex = persistedRequests.findIndex((request) => request.data?.idempotencyKey && request.data?.idempotencyKey === requestToPersist.data?.idempotencyKey); if (existingRequestIndex > -1) { // Merge the new request into the existing one, keeping its place in the queue - persistedRequests.splice(existingRequestIndex, 1, merge(persistedRequests[existingRequestIndex], requestToPersist)); + persistedRequests.splice(existingRequestIndex, 1, createUpdatedRequest(persistedRequests[existingRequestIndex], requestToPersist)); } else { // If not, push the new request to the end of the queue persistedRequests.push(requestToPersist); diff --git a/tests/unit/PersistedRequestsTest.ts b/tests/unit/PersistedRequestsTest.ts new file mode 100644 index 000000000000..18b9b4536772 --- /dev/null +++ b/tests/unit/PersistedRequestsTest.ts @@ -0,0 +1,65 @@ +import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; +import Request from '../../src/types/onyx/Request'; + +const request: Request = { + command: 'OpenReport', + data: { + idempotencyKey: 'OpenReport_1', + }, + successData: [{key: 'reportMetadata_1', onyxMethod: 'merge', value: {}}], + failureData: [{key: 'reportMetadata_2', onyxMethod: 'merge', value: {}}], +}; + +beforeEach(() => { + PersistedRequests.clear(); + PersistedRequests.save(request); +}); + +afterEach(() => { + PersistedRequests.clear(); +}); + +describe('PersistedRequests', () => { + it('save a new request with an idempotency key which currently exists in PersistedRequests', () => { + PersistedRequests.save(request); + expect(PersistedRequests.getAll().length).toBe(1); + }); + + it('save a new request with a new idempotency key', () => { + const newRequest = { + command: 'OpenReport', + data: { + idempotencyKey: 'OpenReport_2', + }, + }; + PersistedRequests.save(newRequest); + expect(PersistedRequests.getAll().length).toBe(2); + }); + + it('merge a new request with one existing in PersistedRequests array', () => { + const newRequest: Request = { + command: 'OpenReport', + data: { + idempotencyKey: 'OpenReport_1', + }, + successData: [{key: 'reportMetadata_3', onyxMethod: 'merge', value: {}}], + failureData: [{key: 'reportMetadata_4', onyxMethod: 'merge', value: {}}], + }; + + PersistedRequests.save(newRequest); + + const persistedRequests = PersistedRequests.getAll(); + + expect(persistedRequests.length).toBe(1); + + const mergedRequest = persistedRequests[0]; + + expect(mergedRequest.successData?.length).toBe(2); + expect(mergedRequest.failureData?.length).toBe(2); + }); + + it('remove a request from the PersistedRequests array', () => { + PersistedRequests.remove(request); + expect(PersistedRequests.getAll().length).toBe(0); + }); +}); From f0b46b6bd30b7e3a971f46c6e1e0f89a4ba82b96 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 4 Nov 2023 22:54:25 +0530 Subject: [PATCH 027/184] Restrict room rename for Task --- src/libs/ReportUtils.js | 13 ++++++++++++- src/pages/settings/Report/ReportSettingsPage.js | 2 +- src/pages/settings/Report/WriteCapabilityPage.js | 3 +-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1e3fc5297193..85c74fc1bb9c 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -19,6 +19,7 @@ import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; +import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; @@ -3912,7 +3913,7 @@ function getWorkspaceChats(policyID, accountIDs) { * @returns {Boolean} */ function shouldDisableRename(report, policy) { - if (isDefaultRoom(report) || isArchivedRoom(report) || isChatThread(report) || isMoneyRequestReport(report) || isPolicyExpenseChat(report)) { + if (isDefaultRoom(report) || isArchivedRoom(report) || isThread(report) || isMoneyRequestReport(report) || isPolicyExpenseChat(report)) { return true; } @@ -3927,6 +3928,15 @@ function shouldDisableRename(report, policy) { return !_.keys(loginList).includes(policy.owner) && policy.role !== CONST.POLICY.ROLE.ADMIN; } +/** + * @param {Object|null} report + * @param {Object|null} policy - the workspace the report is on, null if the user isn't a member of the workspace + * @returns {Boolean} + */ +function canEditWriteCapability(report, policy) { + return PolicyUtils.isPolicyAdmin(policy) && !isAdminRoom(report) && !isArchivedRoom(report) && !isThread(report); +} + /** * Returns the onyx data needed for the task assignee chat * @param {Number} accountID @@ -4315,4 +4325,5 @@ export { parseReportRouteParams, getReimbursementQueuedActionMessage, getPersonalDetailsForAccountID, + canEditWriteCapability, }; diff --git a/src/pages/settings/Report/ReportSettingsPage.js b/src/pages/settings/Report/ReportSettingsPage.js index df23e16e80cd..fc1dfc2033f2 100644 --- a/src/pages/settings/Report/ReportSettingsPage.js +++ b/src/pages/settings/Report/ReportSettingsPage.js @@ -74,7 +74,7 @@ function ReportSettingsPage(props) { const writeCapability = ReportUtils.isAdminRoom(report) ? CONST.REPORT.WRITE_CAPABILITIES.ADMINS : report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL; const writeCapabilityText = translate(`writeCapabilityPage.writeCapability.${writeCapability}`); - const shouldAllowWriteCapabilityEditing = lodashGet(linkedWorkspace, 'role', '') === CONST.POLICY.ROLE.ADMIN && !ReportUtils.isAdminRoom(report) && !isMoneyRequestReport; + const shouldAllowWriteCapabilityEditing = useMemo(() => ReportUtils.canEditWriteCapability(report, linkedWorkspace), [report, linkedWorkspace]); const shouldShowNotificationPref = !isMoneyRequestReport && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const roomNameLabel = translate(isMoneyRequestReport ? 'workspace.editor.nameInputLabel' : 'newRoomPage.roomName'); diff --git a/src/pages/settings/Report/WriteCapabilityPage.js b/src/pages/settings/Report/WriteCapabilityPage.js index c1b417bc28bd..fc587b028f7d 100644 --- a/src/pages/settings/Report/WriteCapabilityPage.js +++ b/src/pages/settings/Report/WriteCapabilityPage.js @@ -8,7 +8,6 @@ import SelectionList from '@components/SelectionList'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; import reportPropTypes from '@pages/reportPropTypes'; @@ -38,7 +37,7 @@ function WriteCapabilityPage(props) { isSelected: value === (props.report.writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL), })); - const isAbleToEdit = !ReportUtils.isAdminRoom(props.report) && PolicyUtils.isPolicyAdmin(props.policy) && !ReportUtils.isArchivedRoom(props.report); + const isAbleToEdit = ReportUtils.canEditWriteCapability(props.report, props.policy); return ( Date: Mon, 6 Nov 2023 10:33:38 +0100 Subject: [PATCH 028/184] Refactor logic of updating requests --- src/libs/actions/PersistedRequests.ts | 42 ++++++--------------------- 1 file changed, 9 insertions(+), 33 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 459b24a9a1b6..2df9fcbf8c3c 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,5 +1,5 @@ import isEqual from 'lodash/isEqual'; -import merge from 'lodash/merge'; +import mergeWith from 'lodash/mergeWith'; import Onyx, {OnyxUpdate} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; @@ -18,42 +18,18 @@ function clear() { return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } -/** - * Method to merge two arrays of OnyxUpdate elements. - * Elements from the old array which keys are not present in the new array are merged with the data of the new array. - */ function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = []): OnyxUpdate[] { - const mergedData = newData; - - oldData.forEach((oldUpdate) => { - const hasSameKey = newData.some((newUpdate) => newUpdate.key === oldUpdate.key); - - if (!hasSameKey) { - mergedData.push(oldUpdate); - } - }); - - return mergedData; + return oldData.concat(newData); } function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { - /** - * In order to create updated request, properties: data, failureData, successData and optimisticData have to be merged - */ - const updatedRequest = { - data: merge(oldRequest.data, newRequest.data), - failureData: mergeOnyxUpdateData(oldRequest.failureData, newRequest.failureData), - successData: mergeOnyxUpdateData(oldRequest.successData, newRequest.successData), - ...newRequest, - }; - - const updatedOptimisticData = mergeOnyxUpdateData(oldRequest.optimisticData, newRequest.optimisticData); - - if (updatedOptimisticData.length > 0) { - updatedRequest.optimisticData = updatedOptimisticData; - } - - return updatedRequest; + // Merge the requests together, but concat Onyx update arrays together + return mergeWith(oldRequest, newRequest, (objValue, srcValue) => { + if (!Array.isArray(objValue) || !objValue.some((obj) => 'onyxMethod' in obj)) { + return; + } + return mergeOnyxUpdateData(objValue, srcValue); + }); } function save(requestToPersist: Request) { From fb2c8a5596b8480e0833d4512c263c9da6b56b22 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 6 Nov 2023 11:00:09 +0100 Subject: [PATCH 029/184] Refactor createUpdatedRequest method --- src/libs/actions/PersistedRequests.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 2df9fcbf8c3c..1683d850dfc6 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,6 +1,5 @@ import isEqual from 'lodash/isEqual'; -import mergeWith from 'lodash/mergeWith'; -import Onyx, {OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; @@ -18,18 +17,14 @@ function clear() { return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } -function mergeOnyxUpdateData(oldData: OnyxUpdate[] = [], newData: OnyxUpdate[] = []): OnyxUpdate[] { - return oldData.concat(newData); -} - function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { // Merge the requests together, but concat Onyx update arrays together - return mergeWith(oldRequest, newRequest, (objValue, srcValue) => { - if (!Array.isArray(objValue) || !objValue.some((obj) => 'onyxMethod' in obj)) { - return; - } - return mergeOnyxUpdateData(objValue, srcValue); - }); + return { + ...newRequest, + successData: [...(oldRequest.successData ?? []), ...(newRequest.successData ?? [])], + failureData: [...(oldRequest.failureData ?? []), ...(newRequest.failureData ?? [])], + optimisticData: [...(oldRequest.optimisticData ?? []), ...(newRequest.optimisticData ?? [])], + }; } function save(requestToPersist: Request) { From 18a0aea2d13dabaf2b94ec941c2376bc93cc3b51 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:48:35 +0100 Subject: [PATCH 030/184] refactor --- src/components/FocusTrapView/index.js | 14 +++++++------- src/components/Modal/index.web.js | 8 +------- src/components/ScreenWrapper/index.js | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js index 647f037af805..384f531aa6ed 100644 --- a/src/components/FocusTrapView/index.js +++ b/src/components/FocusTrapView/index.js @@ -11,7 +11,7 @@ const propTypes = { children: PropTypes.node.isRequired, /** Whether to enable the FocusTrap */ - enabled: PropTypes.bool, + isEnabled: PropTypes.bool, /** * Whether to disable auto focus @@ -20,16 +20,16 @@ const propTypes = { shouldEnableAutoFocus: PropTypes.bool, /** Whether the FocusTrap is active */ - active: PropTypes.bool, + isActive: PropTypes.bool, }; const defaultProps = { - enabled: true, + isEnabled: true, shouldEnableAutoFocus: false, - active: false, + isActive: false, }; -function FocusTrapView({enabled = true, active = true, shouldEnableAutoFocus, ...props}) { +function FocusTrapView({isEnabled = true, isActive = true, shouldEnableAutoFocus, ...props}) { /** * Focus trap always needs a focusable element. * In case that we don't have any focusable elements in the modal, @@ -49,9 +49,9 @@ function FocusTrapView({enabled = true, active = true, shouldEnableAutoFocus, .. ref.current.setAttribute('tabindex', '0'); }, []); - return enabled ? ( + return isEnabled ? ( shouldEnableAutoFocus && ref.current, fallbackFocus: () => ref.current, diff --git a/src/components/Modal/index.web.js b/src/components/Modal/index.web.js index 1383d5488964..1344bc994ff4 100644 --- a/src/components/Modal/index.web.js +++ b/src/components/Modal/index.web.js @@ -43,13 +43,7 @@ function Modal(props) { onModalShow={showModal} avoidKeyboard={false} > - - {props.children} - + {props.children} ); } diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 889c898a6bf6..4df3eba05551 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -143,9 +143,9 @@ function ScreenWrapper({ > {isDevelopment && } From 68905314d4b1d547d1071908510748e6c6c1684e Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:56:11 +0100 Subject: [PATCH 031/184] refactor TwoFactorAuthSteps --- src/components/FocusTrapView/index.js | 7 +++++-- .../Security/TwoFactorAuth/TwoFactorAuthSteps.js | 12 ++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js index 384f531aa6ed..72efb396cf20 100644 --- a/src/components/FocusTrapView/index.js +++ b/src/components/FocusTrapView/index.js @@ -10,7 +10,10 @@ const propTypes = { /** Children to wrap with FocusTrap */ children: PropTypes.node.isRequired, - /** Whether to enable the FocusTrap */ + /** + * Whether to enable the FocusTrap. + * If the FocusTrap is disabled, we just pass the children through. + */ isEnabled: PropTypes.bool, /** @@ -19,7 +22,7 @@ const propTypes = { */ shouldEnableAutoFocus: PropTypes.bool, - /** Whether the FocusTrap is active */ + /** Whether the FocusTrap is active (listening for events) */ isActive: PropTypes.bool, }; diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js index 116d44e9e334..9a9e42f75576 100644 --- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js +++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthSteps.js @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import useAnimatedStepContext from '@components/AnimatedStep/useAnimatedStepContext'; import * as TwoFactorAuthActions from '@userActions/TwoFactorAuthActions'; @@ -13,28 +13,20 @@ import TwoFactorAuthContext from './TwoFactorAuthContext'; import {defaultAccount, TwoFactorAuthPropTypes} from './TwoFactorAuthPropTypes'; function TwoFactorAuthSteps({account = defaultAccount}) { - const calculateCurrentStep = useMemo(() => { + const currentStep = useMemo(() => { if (account.twoFactorAuthStep) { return account.twoFactorAuthStep; } return account.requiresTwoFactorAuth ? CONST.TWO_FACTOR_AUTH_STEPS.ENABLED : CONST.TWO_FACTOR_AUTH_STEPS.CODES; }, [account.requiresTwoFactorAuth, account.twoFactorAuthStep]); - const [currentStep, setCurrentStep] = useState(calculateCurrentStep); - const {setAnimationDirection} = useAnimatedStepContext(); useEffect(() => () => TwoFactorAuthActions.clearTwoFactorAuthData(), []); - - useEffect(() => { - setCurrentStep(calculateCurrentStep); - }, [calculateCurrentStep]); - const handleSetStep = useCallback( (step, animationDirection = CONST.ANIMATION_DIRECTION.IN) => { setAnimationDirection(animationDirection); TwoFactorAuthActions.setTwoFactorAuthStep(step); - setCurrentStep(step); }, [setAnimationDirection], ); From f0ec2ca0e945e81f6592f02ce905038c96280ee9 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:01:00 +0100 Subject: [PATCH 032/184] fix confirmation modal issue --- src/components/Modal/index.web.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Modal/index.web.js b/src/components/Modal/index.web.js index 1344bc994ff4..1383d5488964 100644 --- a/src/components/Modal/index.web.js +++ b/src/components/Modal/index.web.js @@ -43,7 +43,13 @@ function Modal(props) { onModalShow={showModal} avoidKeyboard={false} > - {props.children} + + {props.children} + ); } From 3f51f98720cbbc2abf91189be36a0659910f593c Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:07:03 +0100 Subject: [PATCH 033/184] fix --- src/components/Modal/index.web.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Modal/index.web.js b/src/components/Modal/index.web.js index 1383d5488964..0627cb4d5c0b 100644 --- a/src/components/Modal/index.web.js +++ b/src/components/Modal/index.web.js @@ -44,9 +44,9 @@ function Modal(props) { avoidKeyboard={false} > {props.children} From fe25602706ab208aa029785843f738d781afe60a Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 10:52:19 +0700 Subject: [PATCH 034/184] fix: 30252 Animation is not visible on desktop but visible on Mobile in sign in Modal --- src/pages/signin/SignInPage.js | 2 +- .../SignInPageLayout/SignInPageContent.js | 20 +++++++++++-------- src/pages/signin/SignInPageLayout/index.js | 15 +++++++------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 30eadf952042..bbbefec3e4b2 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -240,7 +240,7 @@ function SignInPage({credentials, account, isInModal, activeClients, preferredLo shouldShowWelcomeHeader={shouldShowWelcomeHeader || !isSmallScreenWidth || !isInModal} shouldShowWelcomeText={shouldShowWelcomeText} ref={signInPageLayoutRef} - isInModal={isInModal} + shouldShowSmallScreen={shouldShowSmallScreen} > {/* LoginForm must use the isVisible prop. This keeps it mounted, but visually hidden so that password managers can access the values. Conditionally rendering this component will break this feature. */} diff --git a/src/pages/signin/SignInPageLayout/SignInPageContent.js b/src/pages/signin/SignInPageLayout/SignInPageContent.js index 14b7b7e004a6..91e83da4a388 100755 --- a/src/pages/signin/SignInPageLayout/SignInPageContent.js +++ b/src/pages/signin/SignInPageLayout/SignInPageContent.js @@ -7,7 +7,7 @@ import OfflineIndicator from '@components/OfflineIndicator'; import SignInPageForm from '@components/SignInPageForm'; import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import SignInHeroImage from '@pages/signin/SignInHeroImage'; import styles from '@styles/styles'; @@ -32,19 +32,23 @@ const propTypes = { /** Whether to show welcome header on a particular page */ shouldShowWelcomeHeader: PropTypes.bool.isRequired, + /** Whether to show signIn hero image on a particular page */ + shouldShowSmallScreen: PropTypes.bool.isRequired, + ...withLocalizePropTypes, - ...windowDimensionsPropTypes, }; function SignInPageContent(props) { + const {isSmallScreenWidth} = useWindowDimensions(); + return ( {/* This empty view creates margin on the top of the sign in form which will shrink and grow depending on if the keyboard is open or not */} - + - + @@ -55,7 +59,7 @@ function SignInPageContent(props) { StyleUtils.getLineHeightStyle(variables.lineHeightSignInHeroXSmall), StyleUtils.getFontSizeStyle(variables.fontSizeSignInHeroXSmall), !props.welcomeText ? styles.mb5 : {}, - !props.isSmallScreenWidth ? styles.textAlignLeft : {}, + !isSmallScreenWidth ? styles.textAlignLeft : {}, styles.mb5, ]} > @@ -63,7 +67,7 @@ function SignInPageContent(props) { ) : null} {props.shouldShowWelcomeText && props.welcomeText ? ( - {props.welcomeText} + {props.welcomeText} ) : null} {props.children} @@ -71,7 +75,7 @@ function SignInPageContent(props) { - {props.isSmallScreenWidth ? ( + {props.shouldShowSmallScreen ? ( @@ -85,4 +89,4 @@ function SignInPageContent(props) { SignInPageContent.propTypes = propTypes; SignInPageContent.displayName = 'SignInPageContent'; -export default compose(withWindowDimensions, withLocalize, withSafeAreaInsets)(SignInPageContent); +export default compose(withLocalize, withSafeAreaInsets)(SignInPageContent); diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index 627fdd0eaa37..b887dc114c06 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -4,7 +4,6 @@ import {ScrollView, View} from 'react-native'; import {withSafeAreaInsets} from 'react-native-safe-area-context'; import SignInGradient from '@assets/images/home-fade-gradient.svg'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import SignInPageHero from '@pages/signin/SignInPageHero'; @@ -39,7 +38,7 @@ const propTypes = { innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** Whether or not the sign in page is being rendered in the RHP modal */ - isInModal: PropTypes.bool, + shouldShowSmallScreen: PropTypes.bool, /** Override the green headline copy */ customHeadline: PropTypes.string, @@ -47,13 +46,12 @@ const propTypes = { /** Override the smaller hero body copy below the headline */ customHeroBody: PropTypes.string, - ...windowDimensionsPropTypes, ...withLocalizePropTypes, }; const defaultProps = { innerRef: () => {}, - isInModal: false, + shouldShowSmallScreen: false, customHeadline: '', customHeroBody: '', }; @@ -63,12 +61,11 @@ function SignInPageLayout(props) { const prevPreferredLocale = usePrevious(props.preferredLocale); let containerStyles = [styles.flex1, styles.signInPageInner]; let contentContainerStyles = [styles.flex1, styles.flexRow]; - const shouldShowSmallScreen = props.isSmallScreenWidth || props.isInModal; // To scroll on both mobile and web, we need to set the container height manually const containerHeight = props.windowHeight - props.insets.top - props.insets.bottom; - if (shouldShowSmallScreen) { + if (props.shouldShowSmallScreen) { containerStyles = [styles.flex1]; contentContainerStyles = [styles.flex1, styles.flexColumn]; } @@ -94,7 +91,7 @@ function SignInPageLayout(props) { return ( - {!shouldShowSmallScreen ? ( + {!props.shouldShowSmallScreen ? ( {props.children} @@ -165,6 +163,7 @@ function SignInPageLayout(props) { welcomeText={props.welcomeText} shouldShowWelcomeText={props.shouldShowWelcomeText} shouldShowWelcomeHeader={props.shouldShowWelcomeHeader} + shouldShowSmallScreen={props.shouldShowSmallScreen} > {props.children} @@ -195,4 +194,4 @@ const SignInPageLayoutWithRef = forwardRef((props, ref) => ( SignInPageLayoutWithRef.displayName = 'SignInPageLayoutWithRef'; -export default compose(withWindowDimensions, withSafeAreaInsets, withLocalize)(SignInPageLayoutWithRef); +export default compose(withSafeAreaInsets, withLocalize)(SignInPageLayoutWithRef); From 49c8699f433ecbad212a20dd65b43fe5d809cd8e Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:19:53 +0100 Subject: [PATCH 035/184] use tabIndex property --- src/components/FocusTrapView/index.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/FocusTrapView/index.js b/src/components/FocusTrapView/index.js index 72efb396cf20..361fb60f3f9e 100644 --- a/src/components/FocusTrapView/index.js +++ b/src/components/FocusTrapView/index.js @@ -3,7 +3,7 @@ */ import FocusTrap from 'focus-trap-react'; import {PropTypes} from 'prop-types'; -import React, {useEffect, useRef} from 'react'; +import React, {useRef} from 'react'; import {View} from 'react-native'; const propTypes = { @@ -40,18 +40,6 @@ function FocusTrapView({isEnabled = true, isActive = true, shouldEnableAutoFocus */ const ref = useRef(null); - /** - * We have to set the 'tabindex' attribute to 0 to make the View focusable. - * Currently, it is not possible to set this through props. - * After the upgrade of 'react-native-web' to version 0.19 we can use 'tabIndex={0}' prop instead. - */ - useEffect(() => { - if (!ref.current) { - return; - } - ref.current.setAttribute('tabindex', '0'); - }, []); - return isEnabled ? ( From 71f999b6927ae0ea3a94817715d630e29f501e46 Mon Sep 17 00:00:00 2001 From: c3024 Date: Wed, 8 Nov 2023 13:28:37 +0530 Subject: [PATCH 036/184] remove code from invite page phone numbers --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 54d09b75eff2..574c39d0140b 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -558,7 +558,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.phoneNumber = personalDetail.phoneNumber; } - result.text = reportName; + result.text = reportName || LocalePhoneNumber.formatPhoneNumber(personalDetail.login); result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), personalDetail.login, personalDetail.accountID); result.subtitle = subtitle; From d9df77aae18b544edcf25b5a76001d753d14c40b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 8 Nov 2023 09:25:59 +0100 Subject: [PATCH 037/184] Refactor createUpdatedRequest --- src/libs/actions/PersistedRequests.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 1683d850dfc6..0cc3c334aa9e 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,5 +1,6 @@ import isEqual from 'lodash/isEqual'; -import Onyx from 'react-native-onyx'; +import mergeWith from 'lodash/mergeWith'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import {Request} from '@src/types/onyx'; @@ -19,12 +20,12 @@ function clear() { function createUpdatedRequest(oldRequest: Request, newRequest: Request): Request { // Merge the requests together, but concat Onyx update arrays together - return { - ...newRequest, - successData: [...(oldRequest.successData ?? []), ...(newRequest.successData ?? [])], - failureData: [...(oldRequest.failureData ?? []), ...(newRequest.failureData ?? [])], - optimisticData: [...(oldRequest.optimisticData ?? []), ...(newRequest.optimisticData ?? [])], - }; + return mergeWith(oldRequest, newRequest, (objValue, srcValue) => { + if (!Array.isArray(objValue) || !objValue.some((obj) => 'onyxMethod' in obj)) { + return; + } + return (objValue as OnyxUpdate[]).concat(srcValue); + }); } function save(requestToPersist: Request) { From 7135f065a1254b0f98a9788e45767f980ad2c91b Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 9 Nov 2023 12:15:11 +0100 Subject: [PATCH 038/184] prettier --- src/components/ScreenWrapper/index.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index 386fb01911f5..01c28b3b8463 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -40,8 +40,8 @@ const ScreenWrapper = React.forwardRef( shouldDismissKeyboardBeforeClose, onEntryTransitionEnd, testID, - shouldDisableFocusTrap, - shouldEnableAutoFocus, + shouldDisableFocusTrap, + shouldEnableAutoFocus, }, ref, ) => { @@ -52,7 +52,7 @@ const ScreenWrapper = React.forwardRef( const {isOffline} = useNetwork(); const navigation = useNavigation(); const isFocused = useIsFocused(); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight ? initialHeight : undefined; const isKeyboardShown = lodashGet(keyboardState, 'isKeyboardShown', false); @@ -150,12 +150,12 @@ const ScreenWrapper = React.forwardRef( style={styles.flex1} enabled={shouldEnablePickerAvoiding} > - + {isDevelopment && } {isDevelopment && } @@ -171,7 +171,7 @@ const ScreenWrapper = React.forwardRef( } {isSmallScreenWidth && shouldShowOfflineIndicator && } - + From 3baf0852028c58de0f246938fb4352360725cce1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 10 Nov 2023 14:56:11 +0700 Subject: [PATCH 039/184] update front-end masked account number --- src/CONST.ts | 6 ++---- src/pages/ReimbursementAccount/BankAccountManualStep.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3cef18c59b76..6b0c5b2214ac 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -218,10 +218,8 @@ const CONST = { REGEX: { US_ACCOUNT_NUMBER: /^[0-9]{4,17}$/, - // If the account number length is from 4 to 13 digits, we show the last 4 digits and hide the rest with X - // If the length is longer than 13 digits, we show the first 6 and last 4 digits, hiding the rest with X - MASKED_US_ACCOUNT_NUMBER: /^[X]{0,9}[0-9]{4}$|^[0-9]{6}[X]{4,7}[0-9]{4}$/, - MASKED_US_ACCOUNT_NUMBER_MATCH_BE: /^[X]{0,13}[0-9]{4}$/, // we can change the name later + // The back-end is always returning account number with 4 last digits and mask the rest with X + MASKED_US_ACCOUNT_NUMBER: /^[X]{0,13}[0-9]{4}$/, SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/, }, VERIFICATION_MAX_ATTEMPTS: 7, diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index c72be8b62568..d15fcb2a7adf 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -43,7 +43,7 @@ function BankAccountManualStep(props) { if ( values.accountNumber && !CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) && - !(shouldDisableInputs && CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER_MATCH_BE.test(values.accountNumber.trim())) + !(shouldDisableInputs && CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim())) ) { errors.accountNumber = 'bankAccount.error.accountNumber'; } else if (values.accountNumber && values.accountNumber === routingNumber) { From aedebbea7891388e4954641a8e162e2b1fff3f59 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 13 Nov 2023 10:55:52 +0700 Subject: [PATCH 040/184] add shouldShowSmallScreen --- src/pages/signin/SignInHeroImage.js | 10 +++++++++- src/pages/signin/SignInPageLayout/SignInPageContent.js | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/signin/SignInHeroImage.js b/src/pages/signin/SignInHeroImage.js index b905d62195d8..3bcf84bedef5 100644 --- a/src/pages/signin/SignInHeroImage.js +++ b/src/pages/signin/SignInHeroImage.js @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import React from 'react'; import Lottie from '@components/Lottie'; import LottieAnimations from '@components/LottieAnimations'; @@ -7,11 +8,17 @@ import variables from '@styles/variables'; const propTypes = { ...windowDimensionsPropTypes, + + shouldShowSmallScreen: PropTypes.bool, +}; + +const defaultProps = { + shouldShowSmallScreen: false, }; function SignInHeroImage(props) { let imageSize; - if (props.isSmallScreenWidth) { + if (props.isSmallScreenWidth || props.shouldShowSmallScreen) { imageSize = { height: variables.signInHeroImageMobileHeight, width: variables.signInHeroImageMobileWidth, @@ -41,5 +48,6 @@ function SignInHeroImage(props) { SignInHeroImage.displayName = 'SignInHeroImage'; SignInHeroImage.propTypes = propTypes; +SignInHeroImage.defaultProps = defaultProps; export default withWindowDimensions(SignInHeroImage); diff --git a/src/pages/signin/SignInPageLayout/SignInPageContent.js b/src/pages/signin/SignInPageLayout/SignInPageContent.js index 91e83da4a388..9f4ffe62e913 100755 --- a/src/pages/signin/SignInPageLayout/SignInPageContent.js +++ b/src/pages/signin/SignInPageLayout/SignInPageContent.js @@ -77,7 +77,7 @@ function SignInPageContent(props) { {props.shouldShowSmallScreen ? ( - + ) : null} From 13bf2beab118ac8ba8986ecaab4a27418ccd3184 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 13 Nov 2023 11:37:23 +0100 Subject: [PATCH 041/184] fix: nested scrollviews on android --- .../AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js index a8e07e0255e6..fec8d1b6730d 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js @@ -1,5 +1,7 @@ -import {FlashList} from '@shopify/flash-list'; import React, {useEffect, useRef} from 'react'; +import {FlashList} from '@shopify/flash-list'; +// We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another +import { ScrollView } from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import styles from '@styles/styles'; @@ -79,6 +81,7 @@ function BaseAutoCompleteSuggestions(props) { keyboardShouldPersistTaps="handled" data={props.suggestions} renderItem={renderSuggestionMenuItem} + renderScrollComponent={ScrollView} keyExtractor={props.keyExtractor} removeClippedSubviews={false} showsVerticalScrollIndicator={innerHeight > rowHeight.value} From 9600c527513ef9fc5abc0744aff098476dfa7025 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 13 Nov 2023 11:43:23 +0100 Subject: [PATCH 042/184] chore: run prettier --- .../AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js index fec8d1b6730d..0ff6ebb90c0e 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.js @@ -1,7 +1,7 @@ -import React, {useEffect, useRef} from 'react'; import {FlashList} from '@shopify/flash-list'; +import React, {useEffect, useRef} from 'react'; // We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another -import { ScrollView } from 'react-native-gesture-handler'; +import {ScrollView} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import styles from '@styles/styles'; From 7af333f464e95176e825a7841639269dec4786b6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 13 Nov 2023 14:57:29 +0100 Subject: [PATCH 043/184] extend a condition to pin a workspace chat --- src/libs/ReportUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 9f8e138afccd..7942249382d9 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2901,12 +2901,13 @@ function buildOptimisticChatReport( welcomeMessage = '', ) { const currentTime = DateUtils.getDBTime(); + const isNewlyCreatedWorkspaceChat = chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT && isOwnPolicyExpenseChat; return { type: CONST.REPORT.TYPE.CHAT, chatType, hasOutstandingIOU: false, isOwnPolicyExpenseChat, - isPinned: reportName === CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, + isPinned: reportName === CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS || (reportName !== CONST.TEACHERS_UNITE.POLICY_NAME && isNewlyCreatedWorkspaceChat), lastActorAccountID: 0, lastMessageTranslationKey: '', lastMessageHtml: '', From 4e2c9bb13d3536d82d900fec707430e881575b91 Mon Sep 17 00:00:00 2001 From: Heri Setiawan Date: Tue, 14 Nov 2023 14:55:32 +0700 Subject: [PATCH 044/184] 31208 - Fix app error on chat bubble click from OD --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9af53a675882..729d5bf322d4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -460,7 +460,7 @@ function getLastClosedReportAction(reportActions: ReportActions | null): OnyxEnt * 4. We will get the second last action from filtered actions because the last * action is always the created action */ -function getFirstVisibleReportActionID(sortedReportActions: ReportAction[], isOffline: boolean): string { +function getFirstVisibleReportActionID(sortedReportActions: ReportAction[] = [], isOffline = false): string { const sortedFilterReportActions = sortedReportActions.filter((action) => !isDeletedAction(action) || (action?.childVisibleActionCount ?? 0) > 0 || isOffline); return sortedFilterReportActions.length > 1 ? sortedFilterReportActions[sortedFilterReportActions.length - 2].reportActionID : ''; } From a147db20cb130bb15f422ce41b9819bafcf979fc Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 29 Sep 2023 10:28:10 +0100 Subject: [PATCH 045/184] feat(expensify-card): add get physical card button and routes --- src/ONYXKEYS.ts | 1 + src/ROUTES.ts | 15 ++ src/SCREENS.ts | 6 + src/languages/en.ts | 22 +++ src/languages/es.ts | 24 ++++ .../AppNavigator/ModalStackNavigators.js | 4 + src/libs/Navigation/Navigation.js | 26 ++++ src/libs/Navigation/linkingConfig.js | 16 +++ src/libs/actions/Wallet.js | 26 ++++ .../Wallet/Cards/BaseGetPhysicalCard.js | 68 ++++++++++ .../Wallet/Cards/GetPhysicalCardAddress.js | 128 ++++++++++++++++++ .../Wallet/Cards/GetPhysicalCardConfirm.js | 96 +++++++++++++ .../Wallet/Cards/GetPhysicalCardName.js | 70 ++++++++++ .../Wallet/Cards/GetPhysicalCardPhone.js | 53 ++++++++ .../settings/Wallet/ExpensifyCardPage.js | 9 ++ src/styles/utilities/sizing.ts | 4 + 16 files changed, 568 insertions(+) create mode 100644 src/pages/settings/Wallet/Cards/BaseGetPhysicalCard.js create mode 100644 src/pages/settings/Wallet/Cards/GetPhysicalCardAddress.js create mode 100644 src/pages/settings/Wallet/Cards/GetPhysicalCardConfirm.js create mode 100644 src/pages/settings/Wallet/Cards/GetPhysicalCardName.js create mode 100644 src/pages/settings/Wallet/Cards/GetPhysicalCardPhone.js diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index fa72a99b5fa2..4a79dca45b13 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -334,6 +334,7 @@ const ONYXKEYS = { REPORT_PHYSICAL_CARD_FORM_DRAFT: 'requestPhysicalCardFormDraft', REPORT_VIRTUAL_CARD_FRAUD: 'reportVirtualCardFraudForm', REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft', + GET_PHYSICAL_CARD_FORM: 'getPhysicalCardForm', }, } as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ed9cc6ae987c..9e304be85a2f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -82,6 +82,21 @@ export default { SETTINGS_REPORT_FRAUD: { route: '/settings/wallet/card/:domain/report-virtual-fraud', getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud`, + SETTINGS_WALLET_CARDS_GET_PHYSICAL_NAME: { + route: '/settings/wallet/cards/:domain/get-physical/name', + getRoute: (domain: string) => `/settings/wallet/cards/${domain}/get-physical/name`, + }, + SETTINGS_WALLET_CARDS_GET_PHYSICAL_PHONE: { + route: '/settings/wallet/cards/:domain/get-physical/phone', + getRoute: (domain: string) => `/settings/wallet/cards/${domain}/get-physical/phone`, + }, + SETTINGS_WALLET_CARDS_GET_PHYSICAL_ADDRESS: { + route: '/settings/wallet/cards/:domain/get-physical/address', + getRoute: (domain: string) => `/settings/wallet/cards/${domain}/get-physical/address`, + }, + SETTINGS_WALLET_CARDS_GET_PHYSICAL_CONFIRM: { + route: '/settings/wallet/cards/:domain/get-physical/confirm', + getRoute: (domain: string) => `/settings/wallet/cards/${domain}/get-physical/confirm`, }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f7de8cfab4b6..1d86d66368e1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -19,6 +19,12 @@ export default { STATUS: 'Settings_Status', WALLET: 'Settings_Wallet', WALLET_DOMAIN_CARDS: 'Settings_Wallet_DomainCards', + WALLET_CARDS_GET_PHYSICAL: { + NAME: 'Settings_Cards_Get_Physical_Name', + PHONE: 'Settings_Cards_Get_Physical_Phone', + ADDRESS: 'Settings_Cards_Get_Physical_Address', + CONFIRM: 'Settings_Cards_Get_Physical_Confirm', + }, }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', diff --git a/src/languages/en.ts b/src/languages/en.ts index fe867efc27c0..66635124639b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -872,6 +872,7 @@ export default { availableSpend: 'Remaining limit', virtualCardNumber: 'Virtual card number', physicalCardNumber: 'Physical card number', + getPhysicalCard: 'Get physical card', reportFraud: 'Report virtual card fraud', reviewTransaction: 'Review transaction', suspiciousBannerTitle: 'Suspicious transaction', @@ -902,6 +903,27 @@ export default { thatDidntMatch: "That didn't match the last 4 digits on your card. Please try again.", }, }, + getPhysicalCard: { + header: 'Get physical card', + nameMessage: 'Enter your first and last name, as this will be shown on your card.', + legalName: 'Legal name', + firstName: 'Legal first name', + lastName: 'Legal last name', + phoneMessage: 'Enter your phone number.', + phoneNumber: 'Phone number', + address: 'Address', + addressMessage: 'Enter your shipping address.', + streetAddress: 'Street Address', + city: 'City', + state: 'State', + zipPostcode: 'Zip/Postcode', + country: 'Country', + confirmMessage: 'Please confirm your details below.', + estimatedDeliveryMessage: 'Your physical card will arrive in 2-3 business days.', + next: 'Next', + getPhysicalCard: 'Get physical card', + shipCard: 'Ship card', + }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, instant: 'Instant (Debit card)', diff --git a/src/languages/es.ts b/src/languages/es.ts index d86b712104fd..4b576a2ac03a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -867,6 +867,8 @@ export default { availableSpend: 'Límite restante', virtualCardNumber: 'Número de la tarjeta virtual', physicalCardNumber: 'Número de la tarjeta física', + // TODO: add translation + getPhysicalCard: '', reportFraud: 'Reportar fraude con la tarjeta virtual', reviewTransaction: 'Revisar transacción', suspiciousBannerTitle: 'Transacción sospechosa', @@ -898,6 +900,28 @@ export default { thatDidntMatch: 'Los 4 últimos dígitos de tu tarjeta no coinciden. Por favor, inténtalo de nuevo.', }, }, + // TODO: add translation + getPhysicalCard: { + header: '', + nameMessage: '', + legalName: '', + firstName: '', + lastName: '', + phoneMessage: '', + phoneNumber: '', + address: '', + addressMessage: '', + streetAddress: '', + city: '', + state: '', + zipPostcode: '', + country: '', + confirmMessage: '', + estimatedDeliveryMessage: '', + next: '', + getPhysicalCard: '', + shipCard: '', + }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, instant: 'Instante', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 2f0a75a02cc3..81a011210630 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -162,6 +162,10 @@ const SettingsModalStackNavigator = createModalStackNavigator({ Settings_Wallet_DomainCards: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default, Settings_Wallet_ReportVirtualCardFraud: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, Settings_Wallet_Card_Activate: () => require('../../../pages/settings/Wallet/ActivatePhysicalCardPage').default, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.NAME]: () => require('../../../pages/settings/Wallet/Cards/GetPhysicalCardName').default, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.PHONE]: () => require('../../../pages/settings/Wallet/Cards/GetPhysicalCardPhone').default, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.ADDRESS]: () => require('../../../pages/settings/Wallet/Cards/GetPhysicalCardAddress').default, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.CONFIRM]: () => require('../../../pages/settings/Wallet/Cards/GetPhysicalCardConfirm').default, Settings_Wallet_Transfer_Balance: () => require('../../../pages/settings/Wallet/TransferBalancePage').default, Settings_Wallet_Choose_Transfer_Account: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default, Settings_Wallet_EnablePayments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index e12cb5545240..58b191aa1c7b 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -305,6 +305,31 @@ function setIsNavigationReady() { resolveNavigationIsReadyPromise(); } +/** + * @param {{address: String, firstName: String, lastName: String, phoneNumber: String}} formData + * @param {Array} loginList + */ +function goToNextPhysicalCardRoute(formData, loginList) { + const {address, firstName, lastName, phoneNumber} = formData; + const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); + const {domain} = (currentRoute && currentRoute.params) || {domain: ''}; + + if (!firstName && !lastName) { + navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_NAME.getRoute(domain)); + return; + } + if (!phoneNumber && !getSecondaryPhoneLogin(loginList)) { + navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_PHONE.getRoute(domain)); + return; + } + if (!address) { + navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_ADDRESS.getRoute(domain)); + return; + } + + navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_CONFIRM.getRoute(domain)); +} + export default { setShouldPopAllStateOnUP, canNavigate, @@ -320,6 +345,7 @@ export default { getTopmostReportId, getRouteNameFromStateEvent, getTopmostReportActionId, + goToNextPhysicalCardRoute, }; export {navigationRef}; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index c017e6c7664e..26546dcc7395 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -79,6 +79,22 @@ export default { }, Settings_Wallet_ReportVirtualCardFraud: { path: ROUTES.SETTINGS_REPORT_FRAUD.route, + exact: true, + }, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.NAME]: { + path: ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_NAME.route, + exact: true, + }, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.PHONE]: { + path: ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_PHONE.route, + exact: true, + }, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.ADDRESS]: { + path: ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_ADDRESS.route, + exact: true, + }, + [SCREENS.SETTINGS.WALLET.CARDS.GET_PHYSICAL.CONFIRM]: { + path: ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_CONFIRM.route, exact: true, }, Settings_Wallet_EnablePayments: { diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index bfc2a7306434..b6ceed79308c 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -330,6 +330,31 @@ function answerQuestionsForWallet(answers, idNumber) { ); } +function requestPhysicalExpensifyCard(cardID, updatedPersonalDetails) { + // TODO: confirm required parameters + const parameters = {}; + const onyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + state: 4, // NOT_ACTIVATED + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: updatedPersonalDetails, + }, + ], + }; + + API.write('RequestPhysicalExpensifyCard', parameters, onyxData); +} + export { openOnfidoFlow, openInitialSettingsPage, @@ -343,4 +368,5 @@ export { verifyIdentity, acceptWalletTerms, setKYCWallSource, + requestPhysicalExpensifyCard, }; diff --git a/src/pages/settings/Wallet/Cards/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Cards/BaseGetPhysicalCard.js new file mode 100644 index 000000000000..11e5416c1057 --- /dev/null +++ b/src/pages/settings/Wallet/Cards/BaseGetPhysicalCard.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {Text} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import Form from '../../../../components/Form'; +import HeaderWithBackButton from '../../../../components/HeaderWithBackButton'; +import ScreenWrapper from '../../../../components/ScreenWrapper'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import styles from '../../../../styles/styles'; +import * as Wallet from '../../../../libs/actions/Wallet'; + +const propTypes = { + children: PropTypes.node.isRequired, + headline: PropTypes.string.isRequired, + isConfirmation: PropTypes.bool, + // TODO: Confirm what is the correct type of loginList + loginList: PropTypes.shape({}).isRequired, + submitButtonText: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, +}; + +const defaultProps = { + isConfirmation: false, +}; + +function BaseGetPhysicalCard({children, headline, isConfirmation, loginList, submitButtonText, title}) { + const onSubmit = (formData) => { + if (isConfirmation) { + // TODO: Use cardID and pass formatted formData here + Wallet.requestPhysicalExpensifyCard('', formData); + // TODO: Redirect user to the domain card page (?) + return; + } + Navigation.goToNextPhysicalCardRoute(formData, loginList); + }; + return ( + + +
+ {headline} + {children} +
+
+ ); +} + +BaseGetPhysicalCard.defaultProps = defaultProps; +BaseGetPhysicalCard.displayName = 'BaseGetPhysicalCard'; +BaseGetPhysicalCard.propTypes = propTypes; + +export default withOnyx({ + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, +})(BaseGetPhysicalCard); diff --git a/src/pages/settings/Wallet/Cards/GetPhysicalCardAddress.js b/src/pages/settings/Wallet/Cards/GetPhysicalCardAddress.js new file mode 100644 index 000000000000..05b9c990c2ad --- /dev/null +++ b/src/pages/settings/Wallet/Cards/GetPhysicalCardAddress.js @@ -0,0 +1,128 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import CONST from '../../../../CONST'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import TextInput from '../../../../components/TextInput'; +import useLocalize from '../../../../hooks/useLocalize'; +import styles from '../../../../styles/styles'; +import BaseGetPhysicalCard from './BaseGetPhysicalCard'; +import AddressSearch from '../../../../components/AddressSearch'; + +const propTypes = { + personalDetails: PropTypes.shape({ + address: PropTypes.string, + }), +}; + +const defaultProps = { + personalDetails: { + address: '', + }, +}; + +function GetPhysicalCardAddress({personalDetails: {address}}) { + const {translate} = useLocalize(); + return ( + + + + + props.onFieldChange({city: value})} + // errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} + containerStyles={[styles.mt4]} + /> + + + + props.onFieldChange({zipCode: value})} + // errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + // hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + /> + + + props.onFieldChange({zipCode: value})} + // errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + // hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + /> + + + props.onFieldChange({zipCode: value})} + // errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + // hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + containerStyles={[styles.mt4]} + /> + + ); +} + +GetPhysicalCardAddress.defaultProps = defaultProps; +GetPhysicalCardAddress.displayName = 'GetPhysicalCardAddress'; +GetPhysicalCardAddress.propTypes = propTypes; + +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, +})(GetPhysicalCardAddress); diff --git a/src/pages/settings/Wallet/Cards/GetPhysicalCardConfirm.js b/src/pages/settings/Wallet/Cards/GetPhysicalCardConfirm.js new file mode 100644 index 000000000000..0d39e2525ce7 --- /dev/null +++ b/src/pages/settings/Wallet/Cards/GetPhysicalCardConfirm.js @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import useLocalize from '../../../../hooks/useLocalize'; +import BaseGetPhysicalCard from './BaseGetPhysicalCard'; +import styles from '../../../../styles/styles'; +import Text from '../../../../components/Text'; +import MenuItemWithTopDescription from '../../../../components/MenuItemWithTopDescription'; +import * as Expensicons from '../../../../components/Icon/Expensicons'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import navigationRef from '../../../../libs/Navigation/navigationRef'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import ROUTES from '../../../../ROUTES'; + +const getDomainFromRoute = () => { + const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); + return (currentRoute && currentRoute.params && currentRoute.params.domain) || ''; +}; + +const goToGetPhysicalCardName = () => { + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_NAME.getRoute(getDomainFromRoute())); +}; + +const goToGetPhysicalCardPhone = () => { + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_PHONE.getRoute(getDomainFromRoute())); +}; + +const goToGetPhysicalCardAddress = () => { + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARDS_GET_PHYSICAL_ADDRESS.getRoute(getDomainFromRoute())); +}; + +const propTypes = { + draftValues: PropTypes.shape({ + address: PropTypes.string, + firstName: PropTypes.string, + lastName: PropTypes.string, + phoneNumber: PropTypes.string, + }), +}; + +const defaultProps = { + draftValues: { + address: '', + firstName: '', + lastName: '', + phoneNumber: '', + }, +}; + +function GetPhysicalCardConfirm({draftValues: {address, firstName, lastName, phoneNumber}}) { + const {translate} = useLocalize(); + + return ( + + {translate('getPhysicalCard.estimatedDeliveryMessage')} + + + + + ); +} + +GetPhysicalCardConfirm.defaultProps = defaultProps; +GetPhysicalCardConfirm.displayName = 'GetPhysicalCardConfirm'; +GetPhysicalCardConfirm.propTypes = propTypes; + +export default withOnyx({ + draftValues: { + key: `${ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM}Draft`, + }, +})(GetPhysicalCardConfirm); diff --git a/src/pages/settings/Wallet/Cards/GetPhysicalCardName.js b/src/pages/settings/Wallet/Cards/GetPhysicalCardName.js new file mode 100644 index 000000000000..8f16ca1c5143 --- /dev/null +++ b/src/pages/settings/Wallet/Cards/GetPhysicalCardName.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import CONST from '../../../../CONST'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import TextInput from '../../../../components/TextInput'; +import useLocalize from '../../../../hooks/useLocalize'; +import styles from '../../../../styles/styles'; +import BaseGetPhysicalCard from './BaseGetPhysicalCard'; + +const propTypes = { + personalDetails: PropTypes.shape({ + firstName: PropTypes.string, + lastName: PropTypes.string, + }), +}; + +const defaultProps = { + personalDetails: { + firstName: '', + lastName: '', + }, +}; + +function GetPhysicalCardName({personalDetails: {firstName, lastName}}) { + const {translate} = useLocalize(); + return ( + + + + + ); +} + +GetPhysicalCardName.defaultProps = defaultProps; +GetPhysicalCardName.displayName = 'GetPhysicalCardName'; +GetPhysicalCardName.propTypes = propTypes; + +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, +})(GetPhysicalCardName); diff --git a/src/pages/settings/Wallet/Cards/GetPhysicalCardPhone.js b/src/pages/settings/Wallet/Cards/GetPhysicalCardPhone.js new file mode 100644 index 000000000000..520e696bbb83 --- /dev/null +++ b/src/pages/settings/Wallet/Cards/GetPhysicalCardPhone.js @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import CONST from '../../../../CONST'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import TextInput from '../../../../components/TextInput'; +import useLocalize from '../../../../hooks/useLocalize'; +import styles from '../../../../styles/styles'; +import BaseGetPhysicalCard from './BaseGetPhysicalCard'; + +const propTypes = { + personalDetails: PropTypes.shape({ + phoneNumber: PropTypes.string, + }), +}; + +const defaultProps = { + personalDetails: { + phoneNumber: '', + }, +}; + +function GetPhysicalCardPhone({personalDetails: {phoneNumber}}) { + const {translate} = useLocalize(); + return ( + + + + ); +} + +GetPhysicalCardPhone.defaultProps = defaultProps; +GetPhysicalCardPhone.displayName = 'GetPhysicalCardPhone'; +GetPhysicalCardPhone.propTypes = propTypes; + +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, +})(GetPhysicalCardPhone); diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index d4a104d272dd..e0d0296faf6e 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -209,6 +209,15 @@ function ExpensifyCardPage({ text={translate('activateCardPage.activatePhysicalCard')} /> )} + {_.isEmpty(physicalCard) && ( +