diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7aeffc44af9ad..19439ddd1f40a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -102,6 +102,9 @@ const ONYXKEYS = { /** Contains metadata (partner, login, validation date) for all of the user's logins */ LOGIN_LIST: 'loginList', + /** Object containing contact method that's going to be added */ + PENDING_CONTACT_ACTION: 'pendingContactAction', + /** Information about the current session (authToken, accountID, email, loading, error) */ SESSION: 'session', STASHED_SESSION: 'stashedSession', @@ -811,6 +814,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER]: OnyxTypes.User; [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; + [ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cd704720961d7..b1f392bf6db6f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -197,6 +197,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 dc2746ea2daa7..a072307c133b8 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', diff --git a/src/libs/API/parameters/AddNewContactMethodParams.ts b/src/libs/API/parameters/AddNewContactMethodParams.ts index f5cd7824c1915..d362ca2db9777 100644 --- a/src/libs/API/parameters/AddNewContactMethodParams.ts +++ b/src/libs/API/parameters/AddNewContactMethodParams.ts @@ -1,3 +1,3 @@ -type AddNewContactMethodParams = {partnerUserID: string}; +type AddNewContactMethodParams = {partnerUserID: string; validateCode: string}; export default AddNewContactMethodParams; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 52acab1dc6108..5c33847a1230e 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -90,6 +90,7 @@ const WRITE_COMMANDS = { CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid', ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup', + RESEND_VALIDATE_CODE: 'ResendValidateCode', OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications', OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications', READ_NEWEST_ACTION: 'ReadNewestAction', @@ -433,6 +434,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; [WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams; [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; + [WRITE_COMMANDS.RESEND_VALIDATE_CODE]: null; [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.READ_NEWEST_ACTION]: Parameters.ReadNewestActionParams; [WRITE_COMMANDS.MARK_AS_UNREAD]: Parameters.MarkAsUnreadParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 2a4627b84ac56..e1f88ad36c76e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -191,6 +191,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, 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 7098eec5acde6..5cf670f718f27 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, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1946df6564a94..e7d02b3f65dc4 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -251,6 +251,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, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 773cc0c9373c9..1ac44512b8c52 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -260,6 +260,16 @@ function deleteContactMethod(contactMethod: string, loginList: Record {isFailedAddContactMethod && ( - User.clearContactMethod(contactMethod)} + canDismissError /> )} diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index 3380766d0ecf3..10c1bd1fd3e0b 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -1,9 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -38,19 +39,29 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const loginInputRef = useRef(null); + const [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION); const navigateBackTo = route?.params?.backTo ?? ROUTES.SETTINGS_PROFILE; - const addNewContactMethod = useCallback( - (values: FormOnyxValues) => { - const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail); - const validateIfnumber = LoginUtils.validateNumber(phoneLogin); - const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase(); + const hasFailedToSendVerificationCode = !!pendingContactAction?.errorFields?.actionVerified; - User.addNewContactMethodAndNavigate(submitDetail, route.params?.backTo); - }, - [route.params?.backTo], - ); + const addNewContactMethod = useCallback((values: FormOnyxValues) => { + const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail); + const validateIfnumber = LoginUtils.validateNumber(phoneLogin); + const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase(); + + User.saveNewContactMethodAndRequestValidationCode(submitDetail); + }, []); + + useEffect(() => { + if (!pendingContactAction?.validateCodeSent) { + return; + } + + Navigation.navigate(ROUTES.SETINGS_CONTACT_METHOD_VALIDATE_ACTION); + }, [pendingContactAction]); + + useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []); const validate = React.useCallback( (values: FormOnyxValues): Errors => { @@ -105,7 +116,6 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) { onSubmit={addNewContactMethod} submitButtonText={translate('common.add')} style={[styles.flexGrow1, styles.mh5]} - enabledWhenOffline > {translate('common.pleaseEnterEmailOrPhoneNumber')} @@ -122,6 +132,12 @@ function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) { maxLength={CONST.LOGIN_CHARACTER_LIMIT} /> + {hasFailedToSendVerificationCode && ( + + )} ); diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx index e0b7a23d5df3e..c77c6e2a07437 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -23,7 +23,7 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Account, LoginList} from '@src/types/onyx'; +import type {Account, LoginList, PendingContactAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type ValidateCodeFormHandle = { @@ -45,21 +45,36 @@ type ValidateCodeFormProps = { contactMethod: string; /** If the magic code has been resent previously */ - hasMagicCodeBeenSent: boolean; + hasMagicCodeBeenSent?: boolean; /** Login list for the user that is signed in */ - loginList: LoginList; + loginList?: LoginList; /** Specifies autocomplete hints for the system, so it can provide autofill */ autoComplete?: AutoCompleteVariant; /** Forwarded inner ref */ innerRef?: ForwardedRef; + + /** Whether we are validating the action taken to add the magic code */ + isValidatingAction?: boolean; + + /** The contact that's going to be added after successful validation */ + pendingContact?: PendingContactAction; }; type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps; -function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent, loginList, autoComplete = 'one-time-code', innerRef = () => {}}: BaseValidateCodeFormProps) { +function BaseValidateCodeForm({ + account = {}, + contactMethod, + hasMagicCodeBeenSent, + loginList, + autoComplete = 'one-time-code', + innerRef = () => {}, + isValidatingAction = false, + pendingContact, +}: BaseValidateCodeFormProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const theme = useTheme(); @@ -67,7 +82,7 @@ function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent const StyleUtils = useStyleUtils(); const [formError, setFormError] = useState({}); const [validateCode, setValidateCode] = useState(''); - const loginData = loginList[contactMethod]; + const loginData = loginList?.[pendingContact?.contactMethod ?? contactMethod]; const inputValidateCodeRef = useRef(null); const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case @@ -132,7 +147,12 @@ function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent * Request a validate code / magic code be sent to verify this contact method */ const resendValidateCode = () => { - User.requestContactMethodValidateCode(contactMethod); + if (!!pendingContact?.contactMethod && isValidatingAction) { + User.saveNewContactMethodAndRequestValidationCode(pendingContact?.contactMethod); + } else { + User.requestContactMethodValidateCode(contactMethod); + } + inputValidateCodeRef.current?.clear(); }; @@ -166,8 +186,14 @@ function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent } setFormError({}); + + if (!!pendingContact?.contactMethod && isValidatingAction) { + User.addNewContactMethod(pendingContact?.contactMethod, validateCode); + return; + } + User.validateSecondaryLogin(loginList, contactMethod, validateCode); - }, [loginList, validateCode, contactMethod]); + }, [loginList, validateCode, contactMethod, isValidatingAction, pendingContact?.contactMethod]); return ( <> @@ -183,8 +209,8 @@ function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent autoFocus={false} /> User.clearContactMethodErrors(contactMethod, 'validateCodeSent')} > @@ -201,18 +227,18 @@ function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent > {translate('validateCodeForm.magicCodeNotReceived')} - {hasMagicCodeBeenSent && ( + {(hasMagicCodeBeenSent ?? !!pendingContact?.validateCodeSent) && ( )} User.clearContactMethodErrors(contactMethod, 'validateLogin')} diff --git a/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx b/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx new file mode 100644 index 0000000000000..469fb0908fcd9 --- /dev/null +++ b/src/pages/settings/Profile/Contacts/ValidateContactActionPage.tsx @@ -0,0 +1,71 @@ +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 ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import ValidateCodeForm from './ValidateCodeForm'; +import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm'; + +function ValidateContactActionPage() { + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + 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/types/onyx/PendingContactAction.ts b/src/types/onyx/PendingContactAction.ts new file mode 100644 index 0000000000000..9e5f80851556c --- /dev/null +++ b/src/types/onyx/PendingContactAction.ts @@ -0,0 +1,22 @@ +import type * as OnyxCommon from './OnyxCommon'; + +/** Model of action to add a new contact method */ +type ContactAction = OnyxCommon.OnyxValueWithOfflineFeedback< + { + /** Phone/Email associated with user */ + contactMethod?: string; + + /** Whether the user validation code was sent */ + validateCodeSent?: boolean; + + /** Field-specific server side errors keyed by microtime */ + errorFields?: OnyxCommon.ErrorFields; + }, + 'actionVerified' +>; + +/** Record of user login data, indexed by partnerUserID */ +type PendingContactAction = ContactAction; + +export default ContactAction; +export type {PendingContactAction}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 54ee7fe258927..fb017f94b36b8 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -40,6 +40,7 @@ import type OnyxInputOrEntry from './OnyxInputOrEntry'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import type {DecisionName, OriginalMessageIOU} from './OriginalMessage'; import type Pages from './Pages'; +import type {PendingContactAction} from './PendingContactAction'; import type PersonalBankAccount from './PersonalBankAccount'; import type {PersonalDetailsList, PersonalDetailsMetadata} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; @@ -130,6 +131,7 @@ export type { Locale, Login, LoginList, + PendingContactAction, MapboxAccessToken, Modal, Network,