diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cf15013fed9b..73ff5435cb0b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -150,6 +150,10 @@ const ROUTES = { route: 'settings/security/delegate/:login/role/:role/confirm', getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/confirm` as const, }, + SETTINGS_DELEGATE_MAGIC_CODE: { + route: 'settings/security/delegate/:login/role/:role/magic-code', + getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/magic-code` as const, + }, SETTINGS_ABOUT: 'settings/about', SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', @@ -228,6 +232,7 @@ const ROUTES = { route: 'settings/profile/contact-methods/:contactMethod/details', getRoute: (contactMethod: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, backTo), }, + SETINGS_CONTACT_METHOD_VALIDATE_ACTION: 'settings/profile/contact-methods/validate-action', SETTINGS_NEW_CONTACT_METHOD: { route: 'settings/profile/contact-methods/new', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ff428edcd7eb..39df4594c277 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -74,6 +74,7 @@ const SCREENS = { DISPLAY_NAME: 'Settings_Display_Name', CONTACT_METHODS: 'Settings_ContactMethods', CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails', + CONTACT_METHOD_VALIDATE_ACTION: 'Settings_ValidateContactMethodAction', NEW_CONTACT_METHOD: 'Settings_NewContactMethod', STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After', STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date', @@ -133,6 +134,7 @@ const SCREENS = { ADD_DELEGATE: 'Settings_Delegate_Add', DELEGATE_ROLE: 'Settings_Delegate_Role', DELEGATE_CONFIRM: 'Settings_Delegate_Confirm', + DELEGATE_MAGIC_CODE: 'Settings_Delegate_Magic_Code', UPDATE_DELEGATE_ROLE: 'Settings_Delegate_Update_Role', UPDATE_DELEGATE_ROLE_MAGIC_CODE: 'Settings_Delegate_Update_Magic_Code', }, diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index 9207b9158051..f71b957387a8 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -62,8 +62,6 @@ type ValidateCodeFormProps = { /** Function to clear error of the form */ clearError: () => void; - - sendValidateCode: () => void; }; function BaseValidateCodeForm({ @@ -75,7 +73,6 @@ function BaseValidateCodeForm({ validateError, handleSubmitForm, clearError, - sendValidateCode, buttonStyles, }: ValidateCodeFormProps) { const {translate} = useLocalize(); @@ -128,6 +125,10 @@ function BaseValidateCodeForm({ }, []), ); + useEffect(() => { + clearError(); + }, [clearError]); + useEffect(() => { if (!hasMagicCodeBeenSent) { return; @@ -139,7 +140,7 @@ function BaseValidateCodeForm({ * Request a validate code / magic code be sent to verify this contact method */ const resendValidateCode = () => { - sendValidateCode(); + User.requestValidateCodeAction(); inputValidateCodeRef.current?.clear(); }; @@ -188,7 +189,7 @@ function BaseValidateCodeForm({ errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})} hasError={!isEmptyObject(validateError)} onFulfill={validateAndSubmitForm} - autoFocus + autoFocus={false} /> (null); @@ -42,16 +30,15 @@ function ValidateCodeActionModal({ return; } firstRenderRef.current = false; - - sendValidateCode(); - }, [isVisible, sendValidateCode]); + User.requestValidateCodeAction(); + }, [isVisible]); return ( - {footer?.()} ); diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts index 5556287b370e..3cbfe62513d1 100644 --- a/src/components/ValidateCodeActionModal/type.ts +++ b/src/components/ValidateCodeActionModal/type.ts @@ -1,4 +1,3 @@ -import type React from 'react'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; type ValidateCodeActionModalProps = { @@ -14,9 +13,6 @@ type ValidateCodeActionModalProps = { /** Function to call when the user closes the modal */ onClose: () => void; - /** Function to be called when the modal is closed */ - onModalHide?: () => void; - /** The pending action for submitting form */ validatePendingAction?: PendingAction | null; @@ -28,15 +24,6 @@ type ValidateCodeActionModalProps = { /** Function to clear error of the form */ clearError: () => void; - - /** A component to be rendered inside the modal */ - footer?: () => React.JSX.Element; - - /** 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; }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b15c5235ae75..c9f6a0c37aa1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -204,6 +204,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default, [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default, + [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: () => require('../../../../pages/settings/Profile/Contacts/ValidateContactActionPage').default, [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: () => require('../../../../pages/settings/Profile/Contacts/NewContactMethodPage').default, [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: () => require('../../../../pages/settings/Preferences/PriorityModePage').default, [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default, @@ -531,6 +532,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage').default, [SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM]: () => require('../../../../pages/settings/Security/AddDelegate/ConfirmDelegatePage').default, + [SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE]: () => require('../../../../pages/settings/Security/AddDelegate/DelegateMagicCodePage').default, [SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE]: () => require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage').default, [SCREENS.WORKSPACE.RULES_CUSTOM_NAME]: () => require('../../../../pages/workspace/rules/RulesCustomNamePage').default, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 574f4d26a01c..cec9e86c5be4 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -6,6 +6,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SETTINGS.PROFILE.DISPLAY_NAME, SCREENS.SETTINGS.PROFILE.CONTACT_METHODS, SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS, + SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION, SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD, SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER, SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE, @@ -45,6 +46,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SETTINGS.DELEGATE.DELEGATE_ROLE, SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE, SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM, + SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE, SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE, ], [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS], diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 72e5f398c1d8..32c173aa19b3 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -256,6 +256,9 @@ const config: LinkingOptions['config'] = { [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: { path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.route, }, + [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_VALIDATE_ACTION]: { + path: ROUTES.SETINGS_CONTACT_METHOD_VALIDATE_ACTION, + }, [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: { path: ROUTES.SETTINGS_NEW_CONTACT_METHOD.route, exact: true, @@ -306,6 +309,12 @@ const config: LinkingOptions['config'] = { login: (login: string) => decodeURIComponent(login), }, }, + [SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE]: { + path: ROUTES.SETTINGS_DELEGATE_MAGIC_CODE.route, + parse: { + login: (login: string) => decodeURIComponent(login), + }, + }, [SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE]: { path: ROUTES.SETTINGS_UPDATE_DELEGATE_ROLE_MAGIC_CODE.route, parse: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0aa6e7474329..e4fabfa7630e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -753,6 +753,10 @@ type SettingsNavigatorParamList = { login: string; role: string; }; + [SCREENS.SETTINGS.DELEGATE.DELEGATE_MAGIC_CODE]: { + login: string; + role: string; + }; [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: { /** cardID of selected card */ cardID: string; diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index 28f2019bb231..06d7093df385 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -219,7 +219,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: optimisticDelegateData(), }, - isLoading: true, }, }, ]; @@ -264,7 +263,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: successDelegateData(), }, - isLoading: false, }, }, ]; @@ -307,7 +305,6 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: failureDelegateData(), }, - isLoading: false, }, }, ]; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 754563b57429..9ea29506accc 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -315,7 +315,11 @@ function resetContactMethodValidateCodeSentState(contactMethod: string) { * Clears unvalidated new contact method action */ function clearUnvalidatedNewContactMethodAction() { - Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, null); + Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, { + validateCodeSent: null, + pendingFields: null, + errorFields: null, + }); } /** @@ -410,6 +414,7 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { [contactMethod]: { partnerUserID: contactMethod, validatedDate: '', + validateCodeSent: true, errorFields: { addedLogin: null, }, @@ -442,7 +447,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { key: ONYXKEYS.PENDING_CONTACT_ACTION, value: { validateCodeSent: null, - actionVerified: true, errorFields: { actionVerified: null, }, diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index bd0151cda4ea..9fcc28f51912 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -1,10 +1,11 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard} from 'react-native'; +import {InteractionManager, Keyboard, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; import ErrorMessageRow from '@components/ErrorMessageRow'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,7 +15,6 @@ 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 useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; @@ -23,7 +23,6 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -31,6 +30,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import ValidateCodeForm from './ValidateCodeForm'; import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; type ContactMethodDetailsPageProps = StackScreenProps; @@ -41,7 +41,6 @@ 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 isLoadingOnyxValues = isLoadingOnyxValue(loginListResult, sessionResult, myDomainSecurityGroupsResult, securityGroupsResult, isLoadingReportDataResult); @@ -72,11 +71,10 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { }); const afterAtSign = contactMethodParam.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%'); - return addSMSDomainIfPhoneNumber(decodeURIComponent(beforeAtSign + afterAtSign)); + return decodeURIComponent(beforeAtSign + afterAtSign); }, [route.params.contactMethod]); const loginData = useMemo(() => loginList?.[contactMethod], [loginList, contactMethod]); const isDefaultContactMethod = useMemo(() => session?.email === loginData?.partnerUserID, [session?.email, loginData?.partnerUserID]); - const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); /** * Attempt to set this contact method as user's "Default contact method" @@ -135,29 +133,17 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { User.deleteContactMethod(contactMethod, loginList ?? {}, backTo); }, [contactMethod, loginList, toggleDeleteModal, backTo]); - const sendValidateCode = () => { - if (loginData?.validateCodeSent) { - return; - } - - User.requestContactMethodValidateCode(contactMethod); - }; - const prevValidatedDate = usePrevious(loginData?.validatedDate); useEffect(() => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (prevValidatedDate || !loginData?.validatedDate || !loginData) { + if (prevValidatedDate || !loginData?.validatedDate) { return; } // Navigate to methods page on successful magic code verification // validatedDate property is responsible to decide the status of the magic code verification Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); - }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod, backTo, loginData]); - - useEffect(() => { - setIsValidateCodeActionModalVisible(!loginData?.validatedDate); - }, [loginData?.validatedDate, loginData?.errorFields?.addedLogin]); + }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod, backTo]); if (isLoadingOnyxValues || (isLoadingReportData && isEmptyObject(loginList))) { return ; @@ -182,64 +168,6 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { const isFailedAddContactMethod = !!loginData.errorFields?.addedLogin; const isFailedRemovedContactMethod = !!loginData.errorFields?.deletedLogin; - const getMenuItems = () => ( - <> - {canChangeDefaultContactMethod ? ( - User.clearContactMethodErrors(contactMethod, 'defaultLogin')} - > - - - ) : null} - {isDefaultContactMethod ? ( - User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')} - > - {translate('contacts.yourDefaultContactMethod')} - - ) : ( - User.clearContactMethodErrors(contactMethod, 'deletedLogin')} - > - toggleDeleteModal(true)} - /> - - )} - - toggleDeleteModal(false)} - onModalHide={() => { - InteractionManager.runAfterInteractions(() => { - validateCodeFormRef.current?.focusLastSelected?.(); - }); - }} - prompt={translate('contacts.removeAreYouSure')} - confirmText={translate('common.yesContinue')} - cancelText={translate('common.cancel')} - isVisible={isDeleteModalOpen && !isDefaultContactMethod} - danger - /> - - ); - return ( validateCodeFormRef.current?.focus?.()} @@ -250,38 +178,88 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo))} /> + toggleDeleteModal(false)} + onModalHide={() => { + InteractionManager.runAfterInteractions(() => { + validateCodeFormRef.current?.focusLastSelected?.(); + }); + }} + prompt={translate('contacts.removeAreYouSure')} + confirmText={translate('common.yesContinue')} + cancelText={translate('common.cancel')} + isVisible={isDeleteModalOpen && !isDefaultContactMethod} + danger + /> + {isFailedAddContactMethod && ( { User.clearContactMethod(contactMethod); - User.clearUnvalidatedNewContactMethodAction(); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }} canDismissError /> )} - {}} - hasMagicCodeBeenSent={hasMagicCodeBeenSent} - isVisible={isValidateCodeActionModalVisible && !loginData.validatedDate && !!loginData} - validatePendingAction={loginData.pendingFields?.validateCodeSent} - handleSubmitForm={(validateCode) => User.validateSecondaryLogin(loginList, contactMethod, validateCode)} - validateError={!isEmptyObject(validateLoginError) ? validateLoginError : ErrorUtils.getLatestErrorField(loginData, 'validateCodeSent')} - clearError={() => User.clearContactMethodErrors(contactMethod, !isEmptyObject(validateLoginError) ? 'validateLogin' : 'validateCodeSent')} - onClose={() => { - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); - setIsValidateCodeActionModalVisible(false); - }} - sendValidateCode={sendValidateCode} - description={translate('contacts.enterMagicCode', {contactMethod})} - footer={() => getMenuItems()} - /> + {!loginData.validatedDate && !isFailedAddContactMethod && ( + + - {!isValidateCodeActionModalVisible && getMenuItems()} + + + )} + {canChangeDefaultContactMethod ? ( + User.clearContactMethodErrors(contactMethod, 'defaultLogin')} + > + + + ) : null} + {isDefaultContactMethod ? ( + User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')} + > + {translate('contacts.yourDefaultContactMethod')} + + ) : ( + User.clearContactMethodErrors(contactMethod, 'deletedLogin')} + > + toggleDeleteModal(true)} + /> + + )} ); diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 6c6d4268eccd..893a54c5ccfd 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -2,7 +2,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import CopyTextToClipboard from '@components/CopyTextToClipboard'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; @@ -18,19 +19,27 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import type {LoginList, Session} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -type ContactMethodsPageProps = StackScreenProps; +type ContactMethodsPageOnyxProps = { + /** Login list for the user that is signed in */ + loginList: OnyxEntry; -function ContactMethodsPage({route}: ContactMethodsPageProps) { + /** Current user session */ + session: OnyxEntry; +}; + +type ContactMethodsPageProps = ContactMethodsPageOnyxProps & StackScreenProps; + +function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps) { const styles = useThemeStyles(); const {formatPhoneNumber, translate} = useLocalize(); - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); - const [session] = useOnyx(ONYXKEYS.SESSION); const loginNames = Object.keys(loginList ?? {}); const navigateBackTo = route?.params?.backTo; const [account] = useOnyx(ONYXKEYS.ACCOUNT); @@ -78,7 +87,12 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID, navigateBackTo))} + onPress={() => { + if (!login?.validatedDate && !login?.validateCodeSent) { + User.requestContactMethodValidateCode(loginName); + } + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID, navigateBackTo)); + }} brickRoadIndicator={indicator} shouldShowBasicTitle shouldShowRightIcon @@ -138,4 +152,11 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { ContactMethodsPage.displayName = 'ContactMethodsPage'; -export default ContactMethodsPage; +export default withOnyx({ + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})(ContactMethodsPage); diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 124d6525113b..42ab49e2ed50 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -19,7 +19,6 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; import * as UserUtils from '@libs/UserUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as User from '@userActions/User'; @@ -41,7 +40,7 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); const loginData = loginList?.[pendingContactAction?.contactMethod ?? contactMethod]; - const validateLoginError = ErrorUtils.getLatestErrorField(loginData, 'addedLogin'); + const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const isActingAsDelegate = !!account?.delegatedAccess?.delegate; @@ -59,19 +58,13 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const addNewContactMethod = useCallback( (magicCode: string) => { - User.addNewContactMethod(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? ''), magicCode); + User.addNewContactMethod(pendingContactAction?.contactMethod ?? '', magicCode); + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.route); }, [pendingContactAction?.contactMethod], ); - useEffect(() => { - if (!pendingContactAction?.actionVerified) { - return; - } - - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.route); - User.clearUnvalidatedNewContactMethodAction(); - }, [pendingContactAction?.actionVerified]); + useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []); const validate = React.useCallback( (values: FormOnyxValues): Errors => { @@ -109,14 +102,6 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo)); }, [navigateBackTo]); - const sendValidateCode = () => { - if (loginData?.validateCodeSent) { - return; - } - - User.requestValidateCodeAction(); - }; - return ( { - if (!loginData) { - return; - } - User.clearContactMethodErrors(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? contactMethod), 'addedLogin'); - }} - onClose={() => { - if (loginData?.errorFields && pendingContactAction?.contactMethod) { - User.clearContactMethod(pendingContactAction?.contactMethod); - User.clearUnvalidatedNewContactMethodAction(); - } - setIsValidateCodeActionModalVisible(false); - }} + clearError={() => User.clearContactMethodErrors(contactMethod, 'validateLogin')} + onClose={() => setIsValidateCodeActionModalVisible(false)} isVisible={isValidateCodeActionModalVisible} title={contactMethod} - sendValidateCode={sendValidateCode} description={translate('contacts.enterMagicCode', {contactMethod})} /> diff --git a/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx b/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx new file mode 100644 index 000000000000..302017adcbe9 --- /dev/null +++ b/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx @@ -0,0 +1,73 @@ +import React, {useEffect, useRef} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as User from '@libs/actions/User'; +import Navigation from '@libs/Navigation/Navigation'; +import * as UserUtils from '@libs/UserUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import ValidateCodeForm from './ValidateCodeForm'; +import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; + +function ValidateContactActionPage() { + const contactMethod = UserUtils.getContactMethod(); + const themeStyles = useThemeStyles(); + const {translate} = useLocalize(); + const validateCodeFormRef = useRef(null); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + + const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION); + const loginData = loginList?.[pendingContactAction?.contactMethod ?? '']; + + useEffect(() => { + if (!loginData || !!loginData.pendingFields?.addedLogin) { + return; + } + + // Navigate to methods page on successful magic code verification + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.route); + }, [loginData, loginData?.pendingFields, loginList]); + + const onBackButtonPress = () => { + User.clearUnvalidatedNewContactMethodAction(); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); + }; + + return ( + + + + + + + + ); +} + +ValidateContactActionPage.displayName = 'ValidateContactActionPage'; + +export default ValidateContactActionPage; diff --git a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx index c769734688c6..2c60aef482a8 100644 --- a/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useState} from 'react'; +import React from 'react'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import HeaderPageLayout from '@components/HeaderPageLayout'; @@ -10,6 +10,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import {requestValidationCode} from '@libs/actions/Delegate'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -17,7 +18,6 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import DelegateMagicCodeModal from './DelegateMagicCodeModal'; type ConfirmDelegatePageProps = StackScreenProps; @@ -29,9 +29,8 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { const role = route.params.role as ValueOf; const {isOffline} = useNetwork(); - const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); - const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(login); + const avatarIcon = personalDetails?.avatar ?? FallbackAvatar; const formattedLogin = formatPhoneNumber(login ?? ''); const displayName = personalDetails?.displayName ?? formattedLogin; @@ -44,7 +43,10 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { text={translate('delegate.addCopilot')} style={styles.mt6} pressOnEnter - onPress={() => setIsValidateCodeActionModalVisible(true)} + onPress={() => { + requestValidationCode(); + Navigation.navigate(ROUTES.SETTINGS_DELEGATE_MAGIC_CODE.getRoute(login, role)); + }} /> ); @@ -72,13 +74,6 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { onPress={() => Navigation.navigate(ROUTES.SETTINGS_DELEGATE_ROLE.getRoute(login, role))} shouldShowRightIcon /> - - {isValidateCodeActionModalVisible && ( - - )} ); } diff --git a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx deleted file mode 100644 index 64b8d27dfd73..000000000000 --- a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import {useOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; -import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; -import useLocalize from '@hooks/useLocalize'; -import * as User from '@libs/actions/User'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as Delegate from '@userActions/Delegate'; -import type CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -type DelegateMagicCodeModalProps = { - login: string; - role: ValueOf; -}; - -function DelegateMagicCodeModal({login, role}: DelegateMagicCodeModalProps) { - const {translate} = useLocalize(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(true); - - const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === login); - const validateLoginError = ErrorUtils.getLatestErrorField(currentDelegate, 'addDelegate'); - - useEffect(() => { - if (!currentDelegate || !!currentDelegate.pendingFields?.email || !!currentDelegate.errorFields?.addDelegate) { - return; - } - - // Dismiss modal on successful magic code verification - Navigation.navigate(ROUTES.SETTINGS_SECURITY); - }, [login, currentDelegate, role]); - - const onBackButtonPress = () => { - setIsValidateCodeActionModalVisible(false); - }; - - const clearError = () => { - if (!validateLoginError) { - return; - } - Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'addDelegate'); - }; - - const sendValidateCode = () => { - if (currentDelegate?.validateCodeSent) { - return; - } - - User.requestValidateCodeAction(); - }; - - return ( - Delegate.addDelegate(login, role, validateCode)} - description={translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} - /> - ); -} - -DelegateMagicCodeModal.displayName = 'DelegateMagicCodeModal'; - -export default DelegateMagicCodeModal; diff --git a/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx b/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx new file mode 100644 index 000000000000..9497507f041a --- /dev/null +++ b/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx @@ -0,0 +1,73 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect, useRef} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import type CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import ValidateCodeForm from './ValidateCodeForm'; +import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; + +type DelegateMagicCodePageProps = StackScreenProps; + +function DelegateMagicCodePage({route}: DelegateMagicCodePageProps) { + const {translate} = useLocalize(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const login = route.params.login; + const role = route.params.role as ValueOf; + + const styles = useThemeStyles(); + const validateCodeFormRef = useRef(null); + + const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === login); + + useEffect(() => { + if (!currentDelegate || !!currentDelegate.pendingFields?.email || !!currentDelegate.errorFields?.addDelegate) { + return; + } + + // Dismiss modal on successful magic code verification + Navigation.dismissModal(); + }, [login, currentDelegate, role]); + + const onBackButtonPress = () => { + Navigation.goBack(ROUTES.SETTINGS_DELEGATE_CONFIRM.getRoute(login, role)); + }; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + <> + + {translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} + + + )} + + ); +} + +DelegateMagicCodePage.displayName = 'DelegateMagicCodePage'; + +export default DelegateMagicCodePage; diff --git a/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx new file mode 100644 index 000000000000..5b01568d018e --- /dev/null +++ b/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -0,0 +1,208 @@ +import {useFocusEffect} from '@react-navigation/native'; +import type {ForwardedRef} from 'react'; +import React, {useCallback, useImperativeHandle, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +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 Delegate from '@userActions/Delegate'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {DelegateRole} from '@src/types/onyx/Account'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ValidateCodeFormHandle = { + focus: () => void; + focusLastSelected: () => void; +}; + +type ValidateCodeFormError = { + validateCode?: TranslationPaths; +}; + +type BaseValidateCodeFormProps = { + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete?: AutoCompleteVariant; + + /** Forwarded inner ref */ + innerRef?: ForwardedRef; + + /** The email of the delegate */ + delegate: string; + + /** The role of the delegate */ + role: DelegateRole; + + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; +}; + +function BaseValidateCodeForm({autoComplete = 'one-time-code', innerRef = () => {}, delegate, role, wrapperStyle}: 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); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const login = account?.primaryLogin; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case + const focusTimeoutRef = useRef(null); + + const currentDelegate = account?.delegatedAccess?.delegates?.find((d) => d.email === delegate); + const validateLoginError = ErrorUtils.getLatestErrorField(currentDelegate, 'addDelegate'); + + const shouldDisableResendValidateCode = !!isOffline || currentDelegate?.isLoading; + + 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); + }; + }, []), + ); + + /** + * Request a validate code / magic code be sent to verify this contact method + */ + const resendValidateCode = () => { + if (!login) { + return; + } + Delegate.requestValidationCode(); + + inputValidateCodeRef.current?.clear(); + }; + + /** + * Handle text input and clear formError upon text change + */ + const onTextInput = useCallback( + (text: string) => { + setValidateCode(text); + setFormError({}); + if (validateLoginError) { + Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'addDelegate'); + } + }, + [currentDelegate?.email, validateLoginError], + ); + + /** + * 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({}); + + Delegate.addDelegate(delegate, role, validateCode); + }, [delegate, role, validateCode]); + + return ( + + + + + + + {translate('validateCodeForm.magicCodeNotReceived')} + + + + + + +