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()}
);