Skip to content

Commit

Permalink
add validate code modal
Browse files Browse the repository at this point in the history
  • Loading branch information
hungvu193 committed Sep 5, 2024
1 parent fd16fdd commit f9a20e3
Show file tree
Hide file tree
Showing 10 changed files with 459 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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',
Expand Down Expand Up @@ -824,6 +827,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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
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 Session from '@userActions/Session';

Check failure on line 21 in src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

'Session' is defined but never used
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, LoginList, ValidateMagicCodeAction} from '@src/types/onyx';

Check failure on line 26 in src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

'LoginList' is defined but never used
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<Account>;
};

type ValidateCodeFormProps = {
/** The contact method being valdiated */
contactMethod: string;

Check failure on line 46 in src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

'contactMethod' PropType is defined but prop is never used

/** 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<ValidateCodeFormHandle>;

/** The contact that's going to be added after successful validation */
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<ValidateCodeFormError>({});
const [validateCode, setValidateCode] = useState('');
const inputValidateCodeRef = useRef<MagicCodeInputHandle>(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<NodeJS.Timeout | null>(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();
// contactMethod is not added as a dependency since it does not change between renders
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [clearError]);

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();
}
},
[validateError],

Check warning on line 171 in src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

React Hook useCallback has a missing dependency: 'clearError'. Either include it or remove the dependency array. If 'clearError' changes too often, find the parent component that defines it and wrap that definition in useCallback
);

/**
* 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 (
<>
<MagicCodeInput
autoComplete={autoComplete}
ref={inputValidateCodeRef}
name="validateCode"
value={validateCode}
onChangeText={onTextInput}
errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})}
hasError={!isEmptyObject(validateError)}
onFulfill={validateAndSubmitForm}
autoFocus={false}
/>
<OfflineWithFeedback
pendingAction={validateCodeAction?.pendingFields?.validateCodeSent}
errors={ErrorUtils.getLatestErrorField(validateCodeAction, 'actionVerified')}
errorRowStyles={[styles.mt2]}
onClose={clearError}
>
<View style={[styles.mt2, styles.dFlex, styles.flexColumn, styles.alignItemsStart]}>
<PressableWithFeedback
disabled={shouldDisableResendValidateCode}
style={[styles.mr1]}
onPress={resendValidateCode}
underlayColor={theme.componentBG}
hoverDimmingValue={1}
pressDimmingValue={0.2}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('validateCodeForm.magicCodeNotReceived')}
>
<Text style={[StyleUtils.getDisabledLinkStyles(shouldDisableResendValidateCode)]}>{translate('validateCodeForm.magicCodeNotReceived')}</Text>
</PressableWithFeedback>
{hasMagicCodeBeenSent && (
<DotIndicatorMessage
type="success"
style={[styles.mt6, styles.flex0]}
// eslint-disable-next-line @typescript-eslint/naming-convention
messages={{0: translate('validateCodeModal.successfulNewCodeRequest')}}
/>
)}
</View>
</OfflineWithFeedback>
<OfflineWithFeedback
pendingAction={validatePendingAction}
errors={validateError}
errorRowStyles={[styles.mt2]}
onClose={() => clearError()}
>
<Button
isDisabled={isOffline}
text={translate('common.verify')}
onPress={validateAndSubmitForm}
style={[styles.mt4]}
success
pressOnEnter
large
isLoading={account?.isLoading}
/>
</OfflineWithFeedback>
</>
);
}

BaseValidateCodeForm.displayName = 'BaseValidateCodeForm';

export type {ValidateCodeFormProps, ValidateCodeFormHandle};

export default withOnyx<BaseValidateCodeFormProps, BaseValidateCodeFormOnyxProps>({
account: {key: ONYXKEYS.ACCOUNT},
})(BaseValidateCodeForm);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, {forwardRef} from 'react';
import BaseValidateCodeForm from './BaseValidateCodeForm';
import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm';

const ValidateCodeForm = forwardRef<ValidateCodeFormHandle, ValidateCodeFormProps>((props, ref) => (
<BaseValidateCodeForm
autoComplete="sms-otp"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
));

export default ValidateCodeForm;

This comment has been minimized.

Copy link
@c3024

c3024 Dec 6, 2024

Contributor

This needs to be enclosed with gestureHandlerRootHOC for react-native-gesture-handler to work correctly. More details here. #51293

14 changes: 14 additions & 0 deletions src/components/ValidateCodeActionModal/ValidateCodeForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, {forwardRef} from 'react';
import BaseValidateCodeForm from './BaseValidateCodeForm';
import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm';

const ValidateCodeForm = forwardRef<ValidateCodeFormHandle, ValidateCodeFormProps>((props, ref) => (
<BaseValidateCodeForm
autoComplete="one-time-code"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
));

export default ValidateCodeForm;
74 changes: 74 additions & 0 deletions src/components/ValidateCodeActionModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, {useCallback, useEffect, useRef} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import * as User from '@libs/actions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ValidateCodeActionModalProps} from './type';
import ValidateCodeForm from './ValidateCodeForm';
import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';

function ValidateCodeActionModal({isVisible, title, description, onClose}: ValidateCodeActionModalProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const themeStyles = useThemeStyles();
const validateCodeFormRef = useRef<ValidateCodeFormHandle>(null);
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);

Check failure on line 21 in src/components/ValidateCodeActionModal/index.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint

'loginList' is assigned a value but never used

const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE);

const hide = useCallback(() => {
// clear data, error etc here.
onClose();
}, [onClose]);

useEffect(() => {
User.requestValidateCodeAction();
}, []);

return (
<Modal
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
isVisible={isVisible}
onClose={hide}
onModalHide={hide}
hideModalContentWhileAnimating
useNativeDriver
shouldUseModalPaddingStyle={false}
>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
testID={ValidateCodeActionModal.displayName}
offlineIndicatorStyle={themeStyles.mtAuto}
>
<HeaderWithBackButton
title={title}
onBackButtonPress={hide}
/>
{validateCodeAction?.isLoading ? (
<FullScreenLoadingIndicator />
) : (
<View style={[themeStyles.ph5, themeStyles.mt3, themeStyles.mb7]}>
<Text style={[themeStyles.mb3]}>{description}</Text>
<ValidateCodeForm
handleSubmitForm={() => {}}
clearError={() => {}}
ref={validateCodeFormRef}
contactMethod={account?.primaryLogin ?? ''}
/>
</View>
)}
</ScreenWrapper>
</Modal>
);
}

ValidateCodeActionModal.displayName = 'ValidateCodeActionModal';

export default ValidateCodeActionModal;
Loading

0 comments on commit f9a20e3

Please sign in to comment.