diff --git a/src/components/ValidateCodeActionForm/index.tsx b/src/components/ValidateCodeActionForm/index.tsx new file mode 100644 index 000000000000..93ea7554185c --- /dev/null +++ b/src/components/ValidateCodeActionForm/index.tsx @@ -0,0 +1,81 @@ +import React, {forwardRef, useEffect, useRef} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Text from '@components/Text'; +import ValidateCodeForm from '@components/ValidateCodeActionModal/ValidateCodeForm'; +import type {ValidateCodeFormHandle} from '@components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ValidateCodeActionFormProps} from './type'; + +function ValidateCodeActionForm({ + descriptionPrimary, + descriptionSecondary, + validatePendingAction, + validateError, + handleSubmitForm, + clearError, + sendValidateCode, + hasMagicCodeBeenSent, + isLoading, + forwardedRef, +}: ValidateCodeActionFormProps) { + const themeStyles = useThemeStyles(); + const isInitialized = useRef(false); + const isClosedRef = useRef(false); + + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + + useEffect( + () => () => { + isInitialized.current = false; + isClosedRef.current = true; + }, + [], + ); + + useEffect(() => { + if (!isInitialized.current) { + isInitialized.current = true; + sendValidateCode(); + } + // eslint-disable-next-line rulesdir/prefer-early-return + return () => { + // We need to run clearError in cleanup function to use as onClose function. + // As 'useEffect cleanup function' runs whenever a dependency is called, we need to put clearError() in the if condition. + // So clearError() will not run when the form is unmounted. + if (isClosedRef.current) { + clearError(); + } + }; + }, [sendValidateCode, clearError]); + + return ( + + {descriptionPrimary} + {!!descriptionSecondary && {descriptionSecondary}} + + + ); +} + +ValidateCodeActionForm.displayName = 'ValidateCodeActionForm'; + +export default forwardRef((props, ref) => ( + +)); diff --git a/src/components/ValidateCodeActionForm/type.ts b/src/components/ValidateCodeActionForm/type.ts new file mode 100644 index 000000000000..3d579e5fc4bc --- /dev/null +++ b/src/components/ValidateCodeActionForm/type.ts @@ -0,0 +1,38 @@ +import type {ForwardedRef} from 'react'; +import type {ValidateCodeFormHandle} from '@components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; + +type ValidateCodeActionFormProps = { + /** Primary description of the modal */ + descriptionPrimary: string; + + /** Secondary description of the modal */ + descriptionSecondary?: string | null; + + /** 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; + + /** Function is called when validate code modal is mounted and on magic code resend */ + sendValidateCode: () => void; + + /** If the magic code has been resent previously */ + hasMagicCodeBeenSent?: boolean; + + /** Whether the form is loading or not */ + isLoading?: boolean; + + /** Ref for validate code form */ + forwardedRef: ForwardedRef; +}; + +// eslint-disable-next-line import/prefer-default-export +export type {ValidateCodeActionFormProps}; diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 3f0cbdb27070..604793001390 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -13,12 +13,12 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; -import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; -import useBeforeRemove from '@hooks/useBeforeRemove'; +import ValidateCodeActionForm from '@components/ValidateCodeActionForm'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import { clearContactMethod, @@ -30,6 +30,7 @@ import { setContactMethodAsDefault, validateSecondaryLogin, } from '@libs/actions/User'; +import {isMobileSafari} from '@libs/Browser'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getEarliestErrorField, getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -54,13 +55,14 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const [myDomainSecurityGroups, myDomainSecurityGroupsResult] = useOnyx(ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS); const [securityGroups, securityGroupsResult] = useOnyx(ONYXKEYS.COLLECTION.SECURITY_GROUP); const [isLoadingReportData, isLoadingReportDataResult] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); - const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(true); + const [isValidateCodeFormVisible, setIsValidateCodeFormVisible] = useState(true); const isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult); const {formatPhoneNumber, translate} = useLocalize(); const theme = useTheme(); const themeStyles = useThemeStyles(); + const {windowWidth} = useWindowDimensions(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const validateCodeFormRef = useRef(null); @@ -160,10 +162,8 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod, backTo, loginData]); - useBeforeRemove(() => setIsValidateCodeActionModalVisible(false)); - useEffect(() => { - setIsValidateCodeActionModalVisible(!loginData?.validatedDate); + setIsValidateCodeFormVisible(!loginData?.validatedDate); }, [loginData?.validatedDate, loginData?.errorFields?.addedLogin]); useEffect(() => { @@ -175,7 +175,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const getThreeDotsMenuItems = useCallback(() => { const menuItems = []; - if (isValidateCodeActionModalVisible && !isDefaultContactMethod) { + if (isValidateCodeFormVisible && !isDefaultContactMethod) { menuItems.push({ icon: Trashcan, text: translate('common.remove'), @@ -183,7 +183,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { }); } return menuItems; - }, [isValidateCodeActionModalVisible, translate, toggleDeleteModal, isDefaultContactMethod]); + }, [isValidateCodeFormVisible, translate, toggleDeleteModal, isDefaultContactMethod]); if (isLoadingOnyxValues || (isLoadingReportData && isEmptyObject(loginList))) { return ; @@ -265,20 +265,55 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { /> )} - {getDeleteConfirmationModal()} ); return ( validateCodeFormRef.current?.focus?.()} + shouldEnableMaxHeight + onEntryTransitionEnd={() => { + InteractionManager.runAfterInteractions(() => { + validateCodeFormRef.current?.focus?.(); + }); + }} testID={ContactMethodDetailsPage.displayName} + focusTrapSettings={{ + focusTrapOptions: isMobileSafari() + ? undefined + : { + // It is added because input form's focusing bothers transition animation: + // https://github.com/Expensify/App/issues/53884#issuecomment-2594568960 + checkCanFocusTrap: (trapContainers: Array) => { + return new Promise((resolve) => { + const interval = setInterval(() => { + const trapContainer = trapContainers.at(0); + if (!trapContainer || (trapContainer && getComputedStyle(trapContainer).visibility !== 'hidden')) { + resolve(); + clearInterval(interval); + } + }, 5); + }); + }, + }, + }} > Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo))} + threeDotsMenuItems={getThreeDotsMenuItems()} + shouldShowThreeDotsButton={getThreeDotsMenuItems().length > 0} + shouldOverlayDots + threeDotsAnchorPosition={themeStyles.threeDotsPopoverOffset(windowWidth)} + onThreeDotsButtonPress={() => { + // Hide the keyboard when the user clicks the three-dot menu. + // Use blurActiveElement() for mWeb and KeyboardUtils.dismiss() for native apps. + blurActiveElement(); + KeyboardUtils.dismiss(); + }} /> - + {isFailedAddContactMethod && ( )} + {isValidateCodeFormVisible && !loginData.validatedDate && !!loginData && ( + validateSecondaryLogin(loginList, contactMethod, validateCode)} + validateError={!isEmptyObject(validateLoginError) ? validateLoginError : getLatestErrorField(loginData, 'validateCodeSent')} + clearError={() => clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')} + sendValidateCode={() => requestContactMethodValidateCode(contactMethod)} + descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod: formattedContactMethod})} + forwardedRef={validateCodeFormRef} + /> + )} - {}} - hasMagicCodeBeenSent={hasMagicCodeBeenSent} - isVisible={isValidateCodeActionModalVisible && !loginData.validatedDate && !!loginData} - validatePendingAction={loginData.pendingFields?.validateCodeSent} - handleSubmitForm={(validateCode) => validateSecondaryLogin(loginList, contactMethod, validateCode)} - validateError={!isEmptyObject(validateLoginError) ? validateLoginError : getLatestErrorField(loginData, 'validateCodeSent')} - clearError={() => clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')} - onClose={() => { - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); - setIsValidateCodeActionModalVisible(false); - }} - sendValidateCode={() => requestContactMethodValidateCode(contactMethod)} - descriptionPrimary={translate('contacts.enterMagicCode', {contactMethod: formattedContactMethod})} - onThreeDotsButtonPress={() => { - // Hide the keyboard when the user clicks the three-dot menu. - // Use blurActiveElement() for mWeb and KeyboardUtils.dismiss() for native apps. - blurActiveElement(); - KeyboardUtils.dismiss(); - }} - threeDotsMenuItems={getThreeDotsMenuItems()} - footer={getDeleteConfirmationModal} - /> - - {!isValidateCodeActionModalVisible && getMenuItems()} + {!isValidateCodeFormVisible && !!loginData.validatedDate && getMenuItems()} + {getDeleteConfirmationModal()} );