diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 85efed35b6d9..672a0cbab2df 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -149,10 +149,6 @@ 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', @@ -231,7 +227,6 @@ 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 624fe1aaa0c2..fab03d37e903 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -74,7 +74,6 @@ 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', @@ -134,7 +133,6 @@ 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 f71b957387a8..9207b9158051 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -62,6 +62,8 @@ type ValidateCodeFormProps = { /** Function to clear error of the form */ clearError: () => void; + + sendValidateCode: () => void; }; function BaseValidateCodeForm({ @@ -73,6 +75,7 @@ function BaseValidateCodeForm({ validateError, handleSubmitForm, clearError, + sendValidateCode, buttonStyles, }: ValidateCodeFormProps) { const {translate} = useLocalize(); @@ -125,10 +128,6 @@ function BaseValidateCodeForm({ }, []), ); - useEffect(() => { - clearError(); - }, [clearError]); - useEffect(() => { if (!hasMagicCodeBeenSent) { return; @@ -140,7 +139,7 @@ function BaseValidateCodeForm({ * Request a validate code / magic code be sent to verify this contact method */ const resendValidateCode = () => { - User.requestValidateCodeAction(); + sendValidateCode(); inputValidateCodeRef.current?.clear(); }; @@ -189,7 +188,7 @@ function BaseValidateCodeForm({ errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})} hasError={!isEmptyObject(validateError)} onFulfill={validateAndSubmitForm} - autoFocus={false} + autoFocus /> (null); @@ -30,15 +42,16 @@ function ValidateCodeActionModal({isVisible, title, description, onClose, valida return; } firstRenderRef.current = false; - User.requestValidateCodeAction(); - }, [isVisible]); + + sendValidateCode(); + }, [isVisible, sendValidateCode]); return ( + {footer?.()} ); diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts index 3cbfe62513d1..5556287b370e 100644 --- a/src/components/ValidateCodeActionModal/type.ts +++ b/src/components/ValidateCodeActionModal/type.ts @@ -1,3 +1,4 @@ +import type React from 'react'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; type ValidateCodeActionModalProps = { @@ -13,6 +14,9 @@ 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; @@ -24,6 +28,15 @@ 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 de446cb16c0e..aaed27025425 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -204,7 +204,6 @@ 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,7 +530,6 @@ 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 cec9e86c5be4..574f4d26a01c 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -6,7 +6,6 @@ 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, @@ -46,7 +45,6 @@ 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 2f7e7e26144f..a25375d97071 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -256,9 +256,6 @@ 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, @@ -309,12 +306,6 @@ 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 cc47de24e7a3..f88c6d79c50e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -750,10 +750,6 @@ 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 06d7093df385..28f2019bb231 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -219,6 +219,7 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: optimisticDelegateData(), }, + isLoading: true, }, }, ]; @@ -263,6 +264,7 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { delegatedAccess: { delegates: successDelegateData(), }, + isLoading: false, }, }, ]; @@ -305,6 +307,7 @@ 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 9ea29506accc..754563b57429 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -315,11 +315,7 @@ function resetContactMethodValidateCodeSentState(contactMethod: string) { * Clears unvalidated new contact method action */ function clearUnvalidatedNewContactMethodAction() { - Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, { - validateCodeSent: null, - pendingFields: null, - errorFields: null, - }); + Onyx.merge(ONYXKEYS.PENDING_CONTACT_ACTION, null); } /** @@ -414,7 +410,6 @@ function addNewContactMethod(contactMethod: string, validateCode = '') { [contactMethod]: { partnerUserID: contactMethod, validatedDate: '', - validateCodeSent: true, errorFields: { addedLogin: null, }, @@ -447,6 +442,7 @@ 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 9fcc28f51912..bd0151cda4ea 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -1,11 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard, View} from 'react-native'; +import {InteractionManager, Keyboard} 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'; @@ -15,6 +14,7 @@ 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,6 +23,7 @@ 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'; @@ -30,7 +31,6 @@ 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,6 +41,7 @@ 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); @@ -71,10 +72,11 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { }); const afterAtSign = contactMethodParam.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%'); - return decodeURIComponent(beforeAtSign + afterAtSign); + return addSMSDomainIfPhoneNumber(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" @@ -133,17 +135,29 @@ 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) { + if (prevValidatedDate || !loginData?.validatedDate || !loginData) { 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]); + }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod, backTo, loginData]); + + useEffect(() => { + setIsValidateCodeActionModalVisible(!loginData?.validatedDate); + }, [loginData?.validatedDate, loginData?.errorFields?.addedLogin]); if (isLoadingOnyxValues || (isLoadingReportData && isEmptyObject(loginList))) { return ; @@ -168,6 +182,64 @@ 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?.()} @@ -178,88 +250,38 @@ 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 /> )} - {!loginData.validatedDate && !isFailedAddContactMethod && ( - - + {}} + 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()} + /> - - - )} - {canChangeDefaultContactMethod ? ( - User.clearContactMethodErrors(contactMethod, 'defaultLogin')} - > - - - ) : null} - {isDefaultContactMethod ? ( - User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')} - > - {translate('contacts.yourDefaultContactMethod')} - - ) : ( - User.clearContactMethodErrors(contactMethod, 'deletedLogin')} - > - toggleDeleteModal(true)} - /> - - )} + {!isValidateCodeActionModalVisible && getMenuItems()} ); diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 893a54c5ccfd..6c6d4268eccd 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -2,8 +2,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import CopyTextToClipboard from '@components/CopyTextToClipboard'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; @@ -19,27 +18,19 @@ 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 ContactMethodsPageOnyxProps = { - /** Login list for the user that is signed in */ - loginList: OnyxEntry; +type ContactMethodsPageProps = StackScreenProps; - /** Current user session */ - session: OnyxEntry; -}; - -type ContactMethodsPageProps = ContactMethodsPageOnyxProps & StackScreenProps; - -function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps) { +function ContactMethodsPage({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); @@ -87,12 +78,7 @@ function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps { - if (!login?.validatedDate && !login?.validateCodeSent) { - User.requestContactMethodValidateCode(loginName); - } - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID, navigateBackTo)); - }} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_DETAILS.getRoute(partnerUserID, navigateBackTo))} brickRoadIndicator={indicator} shouldShowBasicTitle shouldShowRightIcon @@ -152,11 +138,4 @@ function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps ContactMethodsPage.displayName = 'ContactMethodsPage'; -export default withOnyx({ - loginList: { - key: ONYXKEYS.LOGIN_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, -})(ContactMethodsPage); +export default ContactMethodsPage; diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 42ab49e2ed50..124d6525113b 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -19,6 +19,7 @@ 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'; @@ -40,7 +41,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.getEarliestErrorField(loginData, 'validateLogin'); + const validateLoginError = ErrorUtils.getLatestErrorField(loginData, 'addedLogin'); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const isActingAsDelegate = !!account?.delegatedAccess?.delegate; @@ -58,13 +59,19 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { const addNewContactMethod = useCallback( (magicCode: string) => { - User.addNewContactMethod(pendingContactAction?.contactMethod ?? '', magicCode); - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.route); + User.addNewContactMethod(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? ''), magicCode); }, [pendingContactAction?.contactMethod], ); - useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []); + useEffect(() => { + if (!pendingContactAction?.actionVerified) { + return; + } + + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.route); + User.clearUnvalidatedNewContactMethodAction(); + }, [pendingContactAction?.actionVerified]); const validate = React.useCallback( (values: FormOnyxValues): Errors => { @@ -102,6 +109,14 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo)); }, [navigateBackTo]); + const sendValidateCode = () => { + if (loginData?.validateCodeSent) { + return; + } + + User.requestValidateCodeAction(); + }; + return ( User.clearContactMethodErrors(contactMethod, 'validateLogin')} - onClose={() => setIsValidateCodeActionModalVisible(false)} + clearError={() => { + if (!loginData) { + return; + } + User.clearContactMethodErrors(addSMSDomainIfPhoneNumber(pendingContactAction?.contactMethod ?? contactMethod), 'addedLogin'); + }} + onClose={() => { + if (loginData?.errorFields && pendingContactAction?.contactMethod) { + User.clearContactMethod(pendingContactAction?.contactMethod); + User.clearUnvalidatedNewContactMethodAction(); + } + 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 deleted file mode 100644 index 302017adcbe9..000000000000 --- a/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 2c60aef482a8..c769734688c6 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 from 'react'; +import React, {useState} from 'react'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import HeaderPageLayout from '@components/HeaderPageLayout'; @@ -10,7 +10,6 @@ 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'; @@ -18,6 +17,7 @@ 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,8 +29,9 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { const role = route.params.role as ValueOf; const {isOffline} = useNetwork(); - const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(login); + const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(false); + const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(login); const avatarIcon = personalDetails?.avatar ?? FallbackAvatar; const formattedLogin = formatPhoneNumber(login ?? ''); const displayName = personalDetails?.displayName ?? formattedLogin; @@ -43,10 +44,7 @@ function ConfirmDelegatePage({route}: ConfirmDelegatePageProps) { text={translate('delegate.addCopilot')} style={styles.mt6} pressOnEnter - onPress={() => { - requestValidationCode(); - Navigation.navigate(ROUTES.SETTINGS_DELEGATE_MAGIC_CODE.getRoute(login, role)); - }} + onPress={() => setIsValidateCodeActionModalVisible(true)} /> ); @@ -74,6 +72,13 @@ 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 new file mode 100644 index 000000000000..64b8d27dfd73 --- /dev/null +++ b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx @@ -0,0 +1,71 @@ +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 deleted file mode 100644 index 9497507f041a..000000000000 --- a/src/pages/settings/Security/AddDelegate/DelegateMagicCodePage.tsx +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index 5b01568d018e..000000000000 --- a/src/pages/settings/Security/AddDelegate/ValidateCodeForm/BaseValidateCodeForm.tsx +++ /dev/null @@ -1,208 +0,0 @@ -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')} - - - - - - -