|
| 1 | +import {useFocusEffect} from '@react-navigation/native'; |
| 2 | +import type {ForwardedRef} from 'react'; |
| 3 | +import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; |
| 4 | +import {View} from 'react-native'; |
| 5 | +import type {OnyxEntry} from 'react-native-onyx'; |
| 6 | +import {withOnyx} from 'react-native-onyx'; |
| 7 | +import Button from '@components/Button'; |
| 8 | +import DotIndicatorMessage from '@components/DotIndicatorMessage'; |
| 9 | +import MagicCodeInput from '@components/MagicCodeInput'; |
| 10 | +import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput'; |
| 11 | +import OfflineWithFeedback from '@components/OfflineWithFeedback'; |
| 12 | +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; |
| 13 | +import Text from '@components/Text'; |
| 14 | +import useLocalize from '@hooks/useLocalize'; |
| 15 | +import useNetwork from '@hooks/useNetwork'; |
| 16 | +import useStyleUtils from '@hooks/useStyleUtils'; |
| 17 | +import useTheme from '@hooks/useTheme'; |
| 18 | +import useThemeStyles from '@hooks/useThemeStyles'; |
| 19 | +import * as ErrorUtils from '@libs/ErrorUtils'; |
| 20 | +import * as ValidationUtils from '@libs/ValidationUtils'; |
| 21 | +import * as Session from '@userActions/Session'; |
| 22 | +import * as User from '@userActions/User'; |
| 23 | +import CONST from '@src/CONST'; |
| 24 | +import type {TranslationPaths} from '@src/languages/types'; |
| 25 | +import ONYXKEYS from '@src/ONYXKEYS'; |
| 26 | +import type {Account, LoginList, ValidateMagicCodeAction} from '@src/types/onyx'; |
| 27 | +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; |
| 28 | +import {isEmptyObject} from '@src/types/utils/EmptyObject'; |
| 29 | + |
| 30 | +type ValidateCodeFormHandle = { |
| 31 | + focus: () => void; |
| 32 | + focusLastSelected: () => void; |
| 33 | +}; |
| 34 | + |
| 35 | +type ValidateCodeFormError = { |
| 36 | + validateCode?: TranslationPaths; |
| 37 | +}; |
| 38 | + |
| 39 | +type BaseValidateCodeFormOnyxProps = { |
| 40 | + /** The details about the account that the user is signing in with */ |
| 41 | + account: OnyxEntry<Account>; |
| 42 | +}; |
| 43 | + |
| 44 | +type ValidateCodeFormProps = { |
| 45 | + /** The contact method being valdiated */ |
| 46 | + contactMethod: string; |
| 47 | + |
| 48 | + /** If the magic code has been resent previously */ |
| 49 | + hasMagicCodeBeenSent?: boolean; |
| 50 | + |
| 51 | + /** Specifies autocomplete hints for the system, so it can provide autofill */ |
| 52 | + autoComplete?: AutoCompleteVariant; |
| 53 | + |
| 54 | + /** Forwarded inner ref */ |
| 55 | + innerRef?: ForwardedRef<ValidateCodeFormHandle>; |
| 56 | + |
| 57 | + /** The contact that's going to be added after successful validation */ |
| 58 | + validateCodeAction?: ValidateMagicCodeAction; |
| 59 | + |
| 60 | + /** The pending action for submitting form */ |
| 61 | + validatePendingAction?: PendingAction | null; |
| 62 | + |
| 63 | + /** The error of submitting */ |
| 64 | + validateError?: Errors; |
| 65 | + |
| 66 | + /** Function is called when submitting form */ |
| 67 | + handleSubmitForm: (validateCode: string) => void; |
| 68 | + |
| 69 | + /** Function to clear error of the form */ |
| 70 | + clearError: () => void; |
| 71 | +}; |
| 72 | + |
| 73 | +type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps; |
| 74 | + |
| 75 | +function BaseValidateCodeForm({ |
| 76 | + account = {}, |
| 77 | + hasMagicCodeBeenSent, |
| 78 | + autoComplete = 'one-time-code', |
| 79 | + innerRef = () => {}, |
| 80 | + validateCodeAction, |
| 81 | + validatePendingAction, |
| 82 | + validateError, |
| 83 | + handleSubmitForm, |
| 84 | + clearError, |
| 85 | +}: BaseValidateCodeFormProps) { |
| 86 | + const {translate} = useLocalize(); |
| 87 | + const {isOffline} = useNetwork(); |
| 88 | + const theme = useTheme(); |
| 89 | + const styles = useThemeStyles(); |
| 90 | + const StyleUtils = useStyleUtils(); |
| 91 | + const [formError, setFormError] = useState<ValidateCodeFormError>({}); |
| 92 | + const [validateCode, setValidateCode] = useState(''); |
| 93 | + const inputValidateCodeRef = useRef<MagicCodeInputHandle>(null); |
| 94 | + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case |
| 95 | + const shouldDisableResendValidateCode = !!isOffline || account?.isLoading; |
| 96 | + const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null); |
| 97 | + |
| 98 | + useImperativeHandle(innerRef, () => ({ |
| 99 | + focus() { |
| 100 | + inputValidateCodeRef.current?.focus(); |
| 101 | + }, |
| 102 | + focusLastSelected() { |
| 103 | + if (!inputValidateCodeRef.current) { |
| 104 | + return; |
| 105 | + } |
| 106 | + if (focusTimeoutRef.current) { |
| 107 | + clearTimeout(focusTimeoutRef.current); |
| 108 | + } |
| 109 | + focusTimeoutRef.current = setTimeout(() => { |
| 110 | + inputValidateCodeRef.current?.focusLastSelected(); |
| 111 | + }, CONST.ANIMATED_TRANSITION); |
| 112 | + }, |
| 113 | + })); |
| 114 | + |
| 115 | + useFocusEffect( |
| 116 | + useCallback(() => { |
| 117 | + if (!inputValidateCodeRef.current) { |
| 118 | + return; |
| 119 | + } |
| 120 | + if (focusTimeoutRef.current) { |
| 121 | + clearTimeout(focusTimeoutRef.current); |
| 122 | + } |
| 123 | + focusTimeoutRef.current = setTimeout(() => { |
| 124 | + inputValidateCodeRef.current?.focusLastSelected(); |
| 125 | + }, CONST.ANIMATED_TRANSITION); |
| 126 | + return () => { |
| 127 | + if (!focusTimeoutRef.current) { |
| 128 | + return; |
| 129 | + } |
| 130 | + clearTimeout(focusTimeoutRef.current); |
| 131 | + }; |
| 132 | + }, []), |
| 133 | + ); |
| 134 | + |
| 135 | + useEffect(() => { |
| 136 | + if (!validateError) { |
| 137 | + return; |
| 138 | + } |
| 139 | + clearError(); |
| 140 | + // contactMethod is not added as a dependency since it does not change between renders |
| 141 | + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps |
| 142 | + }, [clearError]); |
| 143 | + |
| 144 | + useEffect(() => { |
| 145 | + if (!hasMagicCodeBeenSent) { |
| 146 | + return; |
| 147 | + } |
| 148 | + inputValidateCodeRef.current?.clear(); |
| 149 | + }, [hasMagicCodeBeenSent]); |
| 150 | + |
| 151 | + /** |
| 152 | + * Request a validate code / magic code be sent to verify this contact method |
| 153 | + */ |
| 154 | + const resendValidateCode = () => { |
| 155 | + User.requestValidateCodeAction(); |
| 156 | + inputValidateCodeRef.current?.clear(); |
| 157 | + }; |
| 158 | + |
| 159 | + /** |
| 160 | + * Handle text input and clear formError upon text change |
| 161 | + */ |
| 162 | + const onTextInput = useCallback( |
| 163 | + (text: string) => { |
| 164 | + setValidateCode(text); |
| 165 | + setFormError({}); |
| 166 | + |
| 167 | + if (validateError) { |
| 168 | + clearError(); |
| 169 | + } |
| 170 | + }, |
| 171 | + [validateError], |
| 172 | + ); |
| 173 | + |
| 174 | + /** |
| 175 | + * Check that all the form fields are valid, then trigger the submit callback |
| 176 | + */ |
| 177 | + const validateAndSubmitForm = useCallback(() => { |
| 178 | + if (!validateCode.trim()) { |
| 179 | + setFormError({validateCode: 'validateCodeForm.error.pleaseFillMagicCode'}); |
| 180 | + return; |
| 181 | + } |
| 182 | + |
| 183 | + if (!ValidationUtils.isValidValidateCode(validateCode)) { |
| 184 | + setFormError({validateCode: 'validateCodeForm.error.incorrectMagicCode'}); |
| 185 | + return; |
| 186 | + } |
| 187 | + |
| 188 | + setFormError({}); |
| 189 | + handleSubmitForm(validateCode); |
| 190 | + }, [validateCode, handleSubmitForm]); |
| 191 | + |
| 192 | + return ( |
| 193 | + <> |
| 194 | + <MagicCodeInput |
| 195 | + autoComplete={autoComplete} |
| 196 | + ref={inputValidateCodeRef} |
| 197 | + name="validateCode" |
| 198 | + value={validateCode} |
| 199 | + onChangeText={onTextInput} |
| 200 | + errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})} |
| 201 | + hasError={!isEmptyObject(validateError)} |
| 202 | + onFulfill={validateAndSubmitForm} |
| 203 | + autoFocus={false} |
| 204 | + /> |
| 205 | + <OfflineWithFeedback |
| 206 | + pendingAction={validateCodeAction?.pendingFields?.validateCodeSent} |
| 207 | + errors={ErrorUtils.getLatestErrorField(validateCodeAction, 'actionVerified')} |
| 208 | + errorRowStyles={[styles.mt2]} |
| 209 | + onClose={clearError} |
| 210 | + > |
| 211 | + <View style={[styles.mt2, styles.dFlex, styles.flexColumn, styles.alignItemsStart]}> |
| 212 | + <PressableWithFeedback |
| 213 | + disabled={shouldDisableResendValidateCode} |
| 214 | + style={[styles.mr1]} |
| 215 | + onPress={resendValidateCode} |
| 216 | + underlayColor={theme.componentBG} |
| 217 | + hoverDimmingValue={1} |
| 218 | + pressDimmingValue={0.2} |
| 219 | + role={CONST.ROLE.BUTTON} |
| 220 | + accessibilityLabel={translate('validateCodeForm.magicCodeNotReceived')} |
| 221 | + > |
| 222 | + <Text style={[StyleUtils.getDisabledLinkStyles(shouldDisableResendValidateCode)]}>{translate('validateCodeForm.magicCodeNotReceived')}</Text> |
| 223 | + </PressableWithFeedback> |
| 224 | + {hasMagicCodeBeenSent && ( |
| 225 | + <DotIndicatorMessage |
| 226 | + type="success" |
| 227 | + style={[styles.mt6, styles.flex0]} |
| 228 | + // eslint-disable-next-line @typescript-eslint/naming-convention |
| 229 | + messages={{0: translate('validateCodeModal.successfulNewCodeRequest')}} |
| 230 | + /> |
| 231 | + )} |
| 232 | + </View> |
| 233 | + </OfflineWithFeedback> |
| 234 | + <OfflineWithFeedback |
| 235 | + pendingAction={validatePendingAction} |
| 236 | + errors={validateError} |
| 237 | + errorRowStyles={[styles.mt2]} |
| 238 | + onClose={() => clearError()} |
| 239 | + > |
| 240 | + <Button |
| 241 | + isDisabled={isOffline} |
| 242 | + text={translate('common.verify')} |
| 243 | + onPress={validateAndSubmitForm} |
| 244 | + style={[styles.mt4]} |
| 245 | + success |
| 246 | + pressOnEnter |
| 247 | + large |
| 248 | + isLoading={account?.isLoading} |
| 249 | + /> |
| 250 | + </OfflineWithFeedback> |
| 251 | + </> |
| 252 | + ); |
| 253 | +} |
| 254 | + |
| 255 | +BaseValidateCodeForm.displayName = 'BaseValidateCodeForm'; |
| 256 | + |
| 257 | +export type {ValidateCodeFormProps, ValidateCodeFormHandle}; |
| 258 | + |
| 259 | +export default withOnyx<BaseValidateCodeFormProps, BaseValidateCodeFormOnyxProps>({ |
| 260 | + account: {key: ONYXKEYS.ACCOUNT}, |
| 261 | +})(BaseValidateCodeForm); |
0 commit comments