diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b291eda984d..45b9a8c68bb 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -106,6 +106,9 @@ const ONYXKEYS = { /** Object containing contact method that's going to be added */ PENDING_CONTACT_ACTION: 'pendingContactAction', + /** Store the information of magic code */ + VALIDATE_ACTION_CODE: 'validate_action_code', + /** Information about the current session (authToken, accountID, email, loading, error) */ SESSION: 'session', STASHED_SESSION: 'stashedSession', @@ -840,6 +843,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction; + [ONYXKEYS.VALIDATE_ACTION_CODE]: OnyxTypes.ValidateMagicCodeAction; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx new file mode 100644 index 00000000000..247c0c60690 --- /dev/null +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -0,0 +1,257 @@ +import {useFocusEffect} from '@react-navigation/native'; +import type {ForwardedRef} from 'react'; +import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; +import MagicCodeInput from '@components/MagicCodeInput'; +import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import * as User from '@userActions/User'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Account, ValidateMagicCodeAction} from '@src/types/onyx'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ValidateCodeFormHandle = { + focus: () => void; + focusLastSelected: () => void; +}; + +type ValidateCodeFormError = { + validateCode?: TranslationPaths; +}; + +type BaseValidateCodeFormOnyxProps = { + /** The details about the account that the user is signing in with */ + account: OnyxEntry; +}; + +type ValidateCodeFormProps = { + /** If the magic code has been resent previously */ + hasMagicCodeBeenSent?: boolean; + + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete?: AutoCompleteVariant; + + /** Forwarded inner ref */ + innerRef?: ForwardedRef; + + /** The state of magic code that being sent */ + validateCodeAction?: ValidateMagicCodeAction; + + /** The pending action for submitting form */ + validatePendingAction?: PendingAction | null; + + /** The error of submitting */ + validateError?: Errors; + + /** Function is called when submitting form */ + handleSubmitForm: (validateCode: string) => void; + + /** Function to clear error of the form */ + clearError: () => void; +}; + +type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps; + +function BaseValidateCodeForm({ + account = {}, + hasMagicCodeBeenSent, + autoComplete = 'one-time-code', + innerRef = () => {}, + validateCodeAction, + validatePendingAction, + validateError, + handleSubmitForm, + clearError, +}: BaseValidateCodeFormProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const [formError, setFormError] = useState({}); + const [validateCode, setValidateCode] = useState(''); + const inputValidateCodeRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case + const shouldDisableResendValidateCode = !!isOffline || account?.isLoading; + const focusTimeoutRef = useRef(null); + + useImperativeHandle(innerRef, () => ({ + focus() { + inputValidateCodeRef.current?.focus(); + }, + focusLastSelected() { + if (!inputValidateCodeRef.current) { + return; + } + if (focusTimeoutRef.current) { + clearTimeout(focusTimeoutRef.current); + } + focusTimeoutRef.current = setTimeout(() => { + inputValidateCodeRef.current?.focusLastSelected(); + }, CONST.ANIMATED_TRANSITION); + }, + })); + + useFocusEffect( + useCallback(() => { + if (!inputValidateCodeRef.current) { + return; + } + if (focusTimeoutRef.current) { + clearTimeout(focusTimeoutRef.current); + } + focusTimeoutRef.current = setTimeout(() => { + inputValidateCodeRef.current?.focusLastSelected(); + }, CONST.ANIMATED_TRANSITION); + return () => { + if (!focusTimeoutRef.current) { + return; + } + clearTimeout(focusTimeoutRef.current); + }; + }, []), + ); + + useEffect(() => { + if (!validateError) { + return; + } + clearError(); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [clearError, validateError]); + + useEffect(() => { + if (!hasMagicCodeBeenSent) { + return; + } + inputValidateCodeRef.current?.clear(); + }, [hasMagicCodeBeenSent]); + + /** + * Request a validate code / magic code be sent to verify this contact method + */ + const resendValidateCode = () => { + User.requestValidateCodeAction(); + inputValidateCodeRef.current?.clear(); + }; + + /** + * Handle text input and clear formError upon text change + */ + const onTextInput = useCallback( + (text: string) => { + setValidateCode(text); + setFormError({}); + + if (validateError) { + clearError(); + User.clearValidateCodeActionError('actionVerified'); + } + }, + [validateError, clearError], + ); + + /** + * Check that all the form fields are valid, then trigger the submit callback + */ + const validateAndSubmitForm = useCallback(() => { + if (!validateCode.trim()) { + setFormError({validateCode: 'validateCodeForm.error.pleaseFillMagicCode'}); + return; + } + + if (!ValidationUtils.isValidValidateCode(validateCode)) { + setFormError({validateCode: 'validateCodeForm.error.incorrectMagicCode'}); + return; + } + + setFormError({}); + handleSubmitForm(validateCode); + }, [validateCode, handleSubmitForm]); + + return ( + <> + + User.clearValidateCodeActionError('actionVerified')} + > + + + {translate('validateCodeForm.magicCodeNotReceived')} + + {hasMagicCodeBeenSent && ( + + )} + + + clearError()} + > +