diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 9d35994875e1..8fcbb106268a 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -416,7 +416,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.Form;
[ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.Form;
[ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM]: FormTypes.MoneyRequestHoldReasonForm;
- [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.NewContactMethodForm;
[ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.Form;
[ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.Form;
[ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.Form;
diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx
index 584b349c508f..93febc4fd3c0 100644
--- a/src/components/MagicCodeInput.tsx
+++ b/src/components/MagicCodeInput.tsx
@@ -430,4 +430,4 @@ function MagicCodeInput(
MagicCodeInput.displayName = 'MagicCodeInput';
export default forwardRef(MagicCodeInput);
-export type {MagicCodeInputHandle};
+export type {AutoCompleteVariant, MagicCodeInputHandle};
diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx
index b717c4890a2d..74ea4596046e 100644
--- a/src/components/Pressable/PressableWithFeedback.tsx
+++ b/src/components/Pressable/PressableWithFeedback.tsx
@@ -2,6 +2,7 @@ import React, {forwardRef, useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import type {AnimatedStyle} from 'react-native-reanimated';
import OpacityView from '@components/OpacityView';
+import type {Color} from '@styles/theme/types';
import variables from '@styles/variables';
import GenericPressable from './GenericPressable';
import type {PressableRef} from './GenericPressable/types';
@@ -27,6 +28,9 @@ type PressableWithFeedbackProps = PressableProps & {
/** Whether the view needs to be rendered offscreen (for Android only) */
needsOffscreenAlphaCompositing?: boolean;
+
+ /** The color of the underlay that will show through when the Pressable is active. */
+ underlayColor?: Color;
};
function PressableWithFeedback(
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 81229f353e52..4fd218d4fd42 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -92,9 +92,15 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: undefined;
- [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: undefined;
- [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: undefined;
- [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: undefined;
+ [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: {
+ backTo: Routes;
+ };
+ [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: {
+ contactMethod: string;
+ };
+ [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: {
+ backTo: Routes;
+ };
[SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined;
[SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: undefined;
[SCREENS.SETTINGS.PREFERENCES.LANGUAGE]: undefined;
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
deleted file mode 100644
index a9acf37ae556..000000000000
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
+++ /dev/null
@@ -1,393 +0,0 @@
-import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {Component} from 'react';
-import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import ConfirmModal from '@components/ConfirmModal';
-import DotIndicatorMessage from '@components/DotIndicatorMessage';
-import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import * as Expensicons from '@components/Icon/Expensicons';
-import MenuItem from '@components/MenuItem';
-import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import ScreenWrapper from '@components/ScreenWrapper';
-import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withTheme, {withThemePropTypes} from '@components/withTheme';
-import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles';
-import compose from '@libs/compose';
-import {canUseTouchScreen} from '@libs/DeviceCapabilities';
-import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
-import Navigation from '@libs/Navigation/Navigation';
-import * as Session from '@userActions/Session';
-import * as User from '@userActions/User';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import ValidateCodeForm from './ValidateCodeForm';
-
-const propTypes = {
- /* Onyx Props */
-
- /** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** Value of partner name */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** Date when login was validated */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }),
-
- /** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
-
- /** User's security group IDs by domain */
- myDomainSecurityGroups: PropTypes.objectOf(PropTypes.string),
-
- /** All of the user's security groups and their settings */
- securityGroups: PropTypes.shape({
- hasRestrictedPrimaryLogin: PropTypes.bool,
- }),
-
- /** Route params */
- route: PropTypes.shape({
- params: PropTypes.shape({
- /** Passed via route /settings/profile/contact-methods/:contactMethod/details */
- contactMethod: PropTypes.string,
- }),
- }),
-
- /** Indicated whether the report data is loading */
- isLoadingReportData: PropTypes.bool,
-
- ...withLocalizePropTypes,
- ...withThemeStylesPropTypes,
- ...withThemePropTypes,
-};
-
-const defaultProps = {
- loginList: {},
- session: {
- email: null,
- },
- myDomainSecurityGroups: {},
- securityGroups: {},
- route: {
- params: {
- contactMethod: '',
- },
- },
- isLoadingReportData: true,
-};
-
-class ContactMethodDetailsPage extends Component {
- constructor(props) {
- super(props);
-
- this.deleteContactMethod = this.deleteContactMethod.bind(this);
- this.toggleDeleteModal = this.toggleDeleteModal.bind(this);
- this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this);
- this.getContactMethod = this.getContactMethod.bind(this);
- this.setAsDefault = this.setAsDefault.bind(this);
-
- this.state = {
- isDeleteModalOpen: false,
- };
-
- this.validateCodeFormRef = React.createRef();
- }
-
- componentDidMount() {
- const contactMethod = this.getContactMethod();
- const loginData = lodashGet(this.props.loginList, contactMethod, {});
- if (_.isEmpty(loginData)) {
- return;
- }
- User.resetContactMethodValidateCodeSentState(this.getContactMethod());
- }
-
- componentDidUpdate(prevProps) {
- const contactMethod = this.getContactMethod();
- const validatedDate = lodashGet(this.props.loginList, [contactMethod, 'validatedDate']);
- const prevValidatedDate = lodashGet(prevProps.loginList, [contactMethod, 'validatedDate']);
-
- const loginData = lodashGet(this.props.loginList, contactMethod, {});
- const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID;
- // Navigate to methods page on successful magic code verification
- // validatedDate property is responsible to decide the status of the magic code verification
- if (!prevValidatedDate && validatedDate) {
- // If the selected contactMethod is the current session['login'] and the account is unvalidated,
- // the current authToken is invalid after the successful magic code verification.
- // So we need to sign out the user and redirect to the sign in page.
- if (isDefaultContactMethod) {
- Session.signOutAndRedirectToSignIn();
- return;
- }
- Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route);
- }
- }
-
- /**
- * Gets the current contact method from the route params
- * @returns {string}
- */
- getContactMethod() {
- const contactMethod = lodashGet(this.props.route, 'params.contactMethod');
-
- // We find the number of times the url is encoded based on the last % sign and remove them.
- const lastPercentIndex = contactMethod.lastIndexOf('%');
- const encodePercents = contactMethod.substring(lastPercentIndex).match(new RegExp('25', 'g'));
- let numberEncodePercents = encodePercents ? encodePercents.length : 0;
- const beforeAtSign = contactMethod.substring(0, lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, (match) => {
- if (numberEncodePercents > 0) {
- numberEncodePercents--;
- return '%';
- }
- return match;
- });
- const afterAtSign = contactMethod.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%');
-
- return decodeURIComponent(beforeAtSign + afterAtSign);
- }
-
- /**
- * Attempt to set this contact method as user's "Default contact method"
- */
- setAsDefault() {
- User.setContactMethodAsDefault(this.getContactMethod());
- }
-
- /**
- * Checks if the user is allowed to change their default contact method. This should only be allowed if:
- * 1. The viewed contact method is not already their default contact method
- * 2. The viewed contact method is validated
- * 3. If the user is on a private domain, their security group must allow primary login switching
- *
- * @returns {Boolean}
- */
- canChangeDefaultContactMethod() {
- const contactMethod = this.getContactMethod();
- const loginData = lodashGet(this.props.loginList, contactMethod, {});
- const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID;
-
- // Cannot set this contact method as default if:
- // 1. This contact method is already their default
- // 2. This contact method is not validated
- if (isDefaultContactMethod || !loginData.validatedDate) {
- return false;
- }
-
- const domainName = Str.extractEmailDomain(this.props.session.email);
- const primaryDomainSecurityGroupID = lodashGet(this.props.myDomainSecurityGroups, domainName);
-
- // If there's no security group associated with the user for the primary domain,
- // default to allowing the user to change their default contact method.
- if (!primaryDomainSecurityGroupID) {
- return true;
- }
-
- // Allow user to change their default contact method if they don't have a security group OR if their security group
- // does NOT restrict primary login switching.
- return !lodashGet(this.props.securityGroups, [`${ONYXKEYS.COLLECTION.SECURITY_GROUP}${primaryDomainSecurityGroupID}`, 'hasRestrictedPrimaryLogin'], false);
- }
-
- /**
- * Deletes the contact method if it has errors. Otherwise, it shows the confirmation alert and deletes it only if the user confirms.
- */
- deleteContactMethod() {
- if (!_.isEmpty(lodashGet(this.props.loginList, [this.getContactMethod(), 'errorFields'], {}))) {
- User.deleteContactMethod(this.getContactMethod(), this.props.loginList);
- return;
- }
- this.toggleDeleteModal(true);
- }
-
- /**
- * Toggle delete confirm modal visibility
- * @param {Boolean} isOpen
- */
- toggleDeleteModal(isOpen) {
- if (canUseTouchScreen() && isOpen) {
- InteractionManager.runAfterInteractions(() => {
- this.setState({isDeleteModalOpen: isOpen});
- });
- Keyboard.dismiss();
- } else {
- this.setState({isDeleteModalOpen: isOpen});
- }
- }
-
- /**
- * Delete the contact method and hide the modal
- */
- confirmDeleteAndHideModal() {
- this.toggleDeleteModal(false);
- User.deleteContactMethod(this.getContactMethod(), this.props.loginList);
- }
-
- render() {
- const contactMethod = this.getContactMethod();
-
- // Replacing spaces with "hard spaces" to prevent breaking the number
- const formattedContactMethod = Str.isSMSLogin(contactMethod) ? this.props.formatPhoneNumber(contactMethod).replace(/ /g, '\u00A0') : contactMethod;
-
- if (this.props.isLoadingReportData && _.isEmpty(this.props.loginList)) {
- return ;
- }
-
- const loginData = this.props.loginList[contactMethod];
- if (!contactMethod || !loginData) {
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
- onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
- />
-
- );
- }
-
- const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID;
- const hasMagicCodeBeenSent = lodashGet(this.props.loginList, [contactMethod, 'validateCodeSent'], false);
- const isFailedAddContactMethod = Boolean(lodashGet(loginData, 'errorFields.addedLogin'));
- const isFailedRemovedContactMethod = Boolean(lodashGet(loginData, 'errorFields.deletedLogin'));
-
- return (
- this.validateCodeFormRef.current && this.validateCodeFormRef.current.focus()}
- testID={ContactMethodDetailsPage.displayName}
- >
- Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
- />
-
- this.toggleDeleteModal(false)}
- onModalHide={() => {
- InteractionManager.runAfterInteractions(() => {
- if (!this.validateCodeFormRef.current) {
- return;
- }
- this.validateCodeFormRef.current.focusLastSelected();
- });
- }}
- prompt={this.props.translate('contacts.removeAreYouSure')}
- confirmText={this.props.translate('common.yesContinue')}
- cancelText={this.props.translate('common.cancel')}
- isVisible={this.state.isDeleteModalOpen && !isDefaultContactMethod}
- danger
- />
-
- {isFailedAddContactMethod && (
-
- )}
-
- {!loginData.validatedDate && !isFailedAddContactMethod && (
-
-
-
-
-
- )}
- {this.canChangeDefaultContactMethod() ? (
- User.clearContactMethodErrors(contactMethod, 'defaultLogin')}
- >
-
-
- ) : null}
- {isDefaultContactMethod ? (
- User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')}
- >
- {this.props.translate('contacts.yourDefaultContactMethod')}
-
- ) : (
- User.clearContactMethodErrors(contactMethod, 'deletedLogin')}
- >
-
- )}
-
-
- );
- }
-}
-
-ContactMethodDetailsPage.propTypes = propTypes;
-ContactMethodDetailsPage.defaultProps = defaultProps;
-ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage';
-
-export default compose(
- withLocalize,
- withOnyx({
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- myDomainSecurityGroups: {
- key: ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS,
- },
- securityGroups: {
- key: `${ONYXKEYS.COLLECTION.SECURITY_GROUP}`,
- },
- isLoadingReportData: {
- key: `${ONYXKEYS.IS_LOADING_REPORT_DATA}`,
- },
- }),
- withThemeStyles,
- withTheme,
-)(ContactMethodDetailsPage);
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
new file mode 100644
index 000000000000..7de22da728dd
--- /dev/null
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -0,0 +1,305 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import Str from 'expensify-common/lib/str';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import ConfirmModal from '@components/ConfirmModal';
+import DotIndicatorMessage from '@components/DotIndicatorMessage';
+import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import usePrevious from '@hooks/usePrevious';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+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 * as Session from '@userActions/Session';
+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, SecurityGroup, Session as TSession} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import ValidateCodeForm from './ValidateCodeForm';
+import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';
+
+type ContactMethodDetailsPageOnyxProps = {
+ /** Login list for the user that is signed in */
+ loginList: OnyxEntry;
+
+ /** Current user session */
+ session: OnyxEntry;
+
+ /** User's security group IDs by domain */
+ myDomainSecurityGroups: OnyxEntry>;
+
+ /** All of the user's security groups and their settings */
+ securityGroups: OnyxCollection;
+
+ /** Indicated whether the report data is loading */
+ isLoadingReportData: OnyxEntry;
+};
+
+type ContactMethodDetailsPageProps = ContactMethodDetailsPageOnyxProps & StackScreenProps;
+
+function ContactMethodDetailsPage({loginList, session, myDomainSecurityGroups, securityGroups, isLoadingReportData = true, route}: ContactMethodDetailsPageProps) {
+ const {formatPhoneNumber, translate} = useLocalize();
+ const theme = useTheme();
+ const themeStyles = useThemeStyles();
+
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const validateCodeFormRef = useRef(null);
+
+ /**
+ * Gets the current contact method from the route params
+ */
+ const contactMethod: string = useMemo(() => {
+ const contactMethodParam = route.params.contactMethod;
+
+ // We find the number of times the url is encoded based on the last % sign and remove them.
+ const lastPercentIndex = contactMethodParam.lastIndexOf('%');
+ const encodePercents = contactMethodParam.substring(lastPercentIndex).match(new RegExp('25', 'g'));
+ let numberEncodePercents = encodePercents?.length ?? 0;
+ const beforeAtSign = contactMethodParam.substring(0, lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, (match) => {
+ if (numberEncodePercents > 0) {
+ numberEncodePercents--;
+ return '%';
+ }
+ return match;
+ });
+ const afterAtSign = contactMethodParam.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%');
+
+ 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]);
+
+ /**
+ * Attempt to set this contact method as user's "Default contact method"
+ */
+ const setAsDefault = useCallback(() => {
+ User.setContactMethodAsDefault(contactMethod);
+ }, [contactMethod]);
+
+ /**
+ * Checks if the user is allowed to change their default contact method. This should only be allowed if:
+ * 1. The viewed contact method is not already their default contact method
+ * 2. The viewed contact method is validated
+ * 3. If the user is on a private domain, their security group must allow primary login switching
+ */
+ const canChangeDefaultContactMethod = useMemo(() => {
+ // Cannot set this contact method as default if:
+ // 1. This contact method is already their default
+ // 2. This contact method is not validated
+ if (isDefaultContactMethod || !loginData?.validatedDate) {
+ return false;
+ }
+
+ const domainName = Str.extractEmailDomain(session?.email ?? '');
+ const primaryDomainSecurityGroupID = myDomainSecurityGroups?.[domainName];
+
+ // If there's no security group associated with the user for the primary domain,
+ // default to allowing the user to change their default contact method.
+ if (!primaryDomainSecurityGroupID) {
+ return true;
+ }
+
+ // Allow user to change their default contact method if they don't have a security group OR if their security group
+ // does NOT restrict primary login switching.
+ return !securityGroups?.[`${ONYXKEYS.COLLECTION.SECURITY_GROUP}${primaryDomainSecurityGroupID}`]?.hasRestrictedPrimaryLogin;
+ }, [isDefaultContactMethod, loginData?.validatedDate, session?.email, myDomainSecurityGroups, securityGroups]);
+
+ /**
+ * Toggle delete confirm modal visibility
+ */
+ const toggleDeleteModal = useCallback((isOpen: boolean) => {
+ if (canUseTouchScreen() && isOpen) {
+ InteractionManager.runAfterInteractions(() => {
+ setIsDeleteModalOpen(isOpen);
+ });
+ Keyboard.dismiss();
+ } else {
+ setIsDeleteModalOpen(isOpen);
+ }
+ }, []);
+
+ /**
+ * Delete the contact method and hide the modal
+ */
+ const confirmDeleteAndHideModal = useCallback(() => {
+ toggleDeleteModal(false);
+ User.deleteContactMethod(contactMethod, loginList ?? {});
+ }, [contactMethod, loginList, toggleDeleteModal]);
+
+ useEffect(() => {
+ if (isEmptyObject(loginData)) {
+ return;
+ }
+ User.resetContactMethodValidateCodeSentState(contactMethod);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const prevValidatedDate = usePrevious(loginData?.validatedDate);
+ useEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (prevValidatedDate || !loginData?.validatedDate) {
+ return;
+ }
+
+ // If the selected contactMethod is the current session['login'] and the account is unvalidated,
+ // the current authToken is invalid after the successful magic code verification.
+ // So we need to sign out the user and redirect to the sign in page.
+ if (isDefaultContactMethod) {
+ Session.signOutAndRedirectToSignIn();
+ 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.route);
+ }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod]);
+
+ if (isLoadingReportData && isEmptyObject(loginList)) {
+ return ;
+ }
+
+ if (!contactMethod || !loginData) {
+ return (
+
+ Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
+ onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
+ />
+
+ );
+ }
+
+ // Replacing spaces with "hard spaces" to prevent breaking the number
+ const formattedContactMethod = Str.isSMSLogin(contactMethod) ? formatPhoneNumber(contactMethod).replace(/ /g, '\u00A0') : contactMethod;
+ const hasMagicCodeBeenSent = !!loginData.validateCodeSent;
+ const isFailedAddContactMethod = !!loginData.errorFields?.addedLogin;
+ const isFailedRemovedContactMethod = !!loginData.errorFields?.deletedLogin;
+
+ return (
+ validateCodeFormRef.current?.focus?.()}
+ testID={ContactMethodDetailsPage.displayName}
+ >
+ Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
+ />
+
+ 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 && (
+
+ )}
+
+ {!loginData.validatedDate && !isFailedAddContactMethod && (
+
+
+
+
+
+ )}
+ {canChangeDefaultContactMethod ? (
+ User.clearContactMethodErrors(contactMethod, 'defaultLogin')}
+ >
+
+
+ ) : null}
+ {isDefaultContactMethod ? (
+ User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')}
+ >
+ {translate('contacts.yourDefaultContactMethod')}
+
+ ) : (
+ User.clearContactMethodErrors(contactMethod, 'deletedLogin')}
+ >
+
+ )}
+
+
+ );
+}
+
+ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage';
+
+export default withOnyx({
+ loginList: {
+ key: ONYXKEYS.LOGIN_LIST,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ myDomainSecurityGroups: {
+ key: ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS,
+ },
+ securityGroups: {
+ key: `${ONYXKEYS.COLLECTION.SECURITY_GROUP}`,
+ },
+ isLoadingReportData: {
+ key: `${ONYXKEYS.IS_LOADING_REPORT_DATA}`,
+ },
+})(ContactMethodDetailsPage);
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
similarity index 53%
rename from src/pages/settings/Profile/Contacts/ContactMethodsPage.js
rename to src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
index c85d123ad3fd..5d150e782c44 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js
+++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
@@ -1,10 +1,9 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
import {ScrollView, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import Button from '@components/Button';
import CopyTextToClipboard from '@components/CopyTextToClipboard';
import FixedFooter from '@components/FixedFooter';
@@ -13,86 +12,64 @@ import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import {translatableTextPropTypes} from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
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';
-const propTypes = {
- /* Onyx Props */
-
+type ContactMethodsPageOnyxProps = {
/** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** The partner creating the account. It depends on the source: website, mobile, integrations, ... */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** The date when the login was validated, used to show the brickroad status */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }),
+ loginList: OnyxEntry;
/** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
-
- ...withLocalizePropTypes,
+ session: OnyxEntry;
};
-const defaultProps = {
- loginList: {},
- session: {
- email: null,
- },
-};
+type ContactMethodsPageProps = ContactMethodsPageOnyxProps & StackScreenProps;
-function ContactMethodsPage(props) {
+function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps) {
const styles = useThemeStyles();
- const loginNames = _.keys(props.loginList);
- const navigateBackTo = lodashGet(props.route, 'params.backTo', '');
+ const {formatPhoneNumber, translate} = useLocalize();
+ const loginNames = Object.keys(loginList ?? {});
+ const navigateBackTo = route?.params?.backTo || ROUTES.SETTINGS_PROFILE;
// Sort the login names by placing the one corresponding to the default contact method as the first item before displaying the contact methods.
// The default contact method is determined by checking against the session email (the current login).
- const sortedLoginNames = _.sortBy(loginNames, (loginName) => (props.loginList[loginName].partnerUserID === props.session.email ? 0 : 1));
+ const sortedLoginNames = loginNames.sort((loginName) => (loginList?.[loginName].partnerUserID === session?.email ? -1 : 1));
- const loginMenuItems = _.map(sortedLoginNames, (loginName) => {
- const login = props.loginList[loginName];
- const pendingAction = lodashGet(login, 'pendingFields.deletedLogin') || lodashGet(login, 'pendingFields.addedLogin');
- if (!login.partnerUserID && _.isEmpty(pendingAction)) {
+ const loginMenuItems = sortedLoginNames.map((loginName) => {
+ const login = loginList?.[loginName];
+ const pendingAction = login?.pendingFields?.deletedLogin ?? login?.pendingFields?.addedLogin ?? undefined;
+ if (!login?.partnerUserID && !pendingAction) {
return null;
}
let description = '';
- if (props.session.email === login.partnerUserID) {
- description = props.translate('contacts.getInTouch');
- } else if (lodashGet(login, 'errorFields.addedLogin')) {
- description = props.translate('contacts.failedNewContact');
- } else if (!login.validatedDate) {
- description = props.translate('contacts.pleaseVerify');
+ if (session?.email === login?.partnerUserID) {
+ description = translate('contacts.getInTouch');
+ } else if (login?.errorFields?.addedLogin) {
+ description = translate('contacts.failedNewContact');
+ } else if (!login?.validatedDate) {
+ description = translate('contacts.pleaseVerify');
}
- let indicator = null;
- if (_.some(lodashGet(login, 'errorFields', {}), (errorField) => !_.isEmpty(errorField))) {
+ let indicator;
+ if (Object.values(login?.errorFields ?? {}).some((errorField) => !isEmptyObject(errorField))) {
indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
- } else if (!login.validatedDate) {
+ } else if (!login?.validatedDate) {
indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
}
// Default to using login key if we deleted login.partnerUserID optimistically
// but still need to show the pending login being deleted while offline.
- const partnerUserID = login.partnerUserID || loginName;
- const menuItemTitle = Str.isSMSLogin(partnerUserID) ? props.formatPhoneNumber(partnerUserID) : partnerUserID;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const partnerUserID = login?.partnerUserID || loginName;
+ const menuItemTitle = Str.isSMSLogin(partnerUserID) ? formatPhoneNumber(partnerUserID) : partnerUserID;
return (
);
@@ -126,25 +103,25 @@ function ContactMethodsPage(props) {
testID={ContactMethodsPage.displayName}
>
Navigation.goBack(navigateBackTo)}
/>
- {props.translate('contacts.helpTextBeforeEmail')}
+ {translate('contacts.helpTextBeforeEmail')}
- {props.translate('contacts.helpTextAfterEmail')}
+ {translate('contacts.helpTextAfterEmail')}
{loginMenuItems}
@@ -154,18 +131,13 @@ function ContactMethodsPage(props) {
);
}
-ContactMethodsPage.propTypes = propTypes;
-ContactMethodsPage.defaultProps = defaultProps;
ContactMethodsPage.displayName = 'ContactMethodsPage';
-export default compose(
- withLocalize,
- withOnyx({
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- }),
-)(ContactMethodsPage);
+export default withOnyx({
+ loginList: {
+ key: ONYXKEYS.LOGIN_LIST,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(ContactMethodsPage);
diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
similarity index 56%
rename from src/pages/settings/Profile/Contacts/NewContactMethodPage.js
rename to src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
index b9d5dee8f4be..20e12f71664e 100644
--- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js
+++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
@@ -1,57 +1,40 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
import * as LoginUtils from '@libs/LoginUtils';
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 INPUT_IDS from '@src/types/form/NewContactMethodForm';
+import type {LoginList} from '@src/types/onyx';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
-const propTypes = {
- /* Onyx Props */
-
+type NewContactMethodPageOnyxProps = {
/** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** The partner creating the account. It depends on the source: website, mobile, integrations, ... */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** The date when the login was validated, used to show the brickroad status */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }),
-
- ...withLocalizePropTypes,
-};
-const defaultProps = {
- loginList: {},
+ loginList: OnyxEntry;
};
-const addNewContactMethod = (values) => {
+type NewContactMethodPageProps = NewContactMethodPageOnyxProps & StackScreenProps;
+
+const addNewContactMethod = (values: FormOnyxValues) => {
const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail);
const validateIfnumber = LoginUtils.validateNumber(phoneLogin);
const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase();
@@ -59,35 +42,36 @@ const addNewContactMethod = (values) => {
User.addNewContactMethodAndNavigate(submitDetail);
};
-function NewContactMethodPage(props) {
+function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) {
const styles = useThemeStyles();
- const loginInputRef = useRef(null);
+ const {translate} = useLocalize();
+ const loginInputRef = useRef(null);
- const navigateBackTo = lodashGet(props.route, 'params.backTo', ROUTES.SETTINGS_PROFILE);
+ const navigateBackTo = route?.params?.backTo ?? ROUTES.SETTINGS_PROFILE;
const validate = React.useCallback(
- (values) => {
+ (values: FormOnyxValues): Errors => {
const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail);
const validateIfnumber = LoginUtils.validateNumber(phoneLogin);
const errors = {};
- if (_.isEmpty(values.phoneOrEmail)) {
+ if (!values.phoneOrEmail) {
ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.contactMethodRequired');
}
- if (!_.isEmpty(values.phoneOrEmail) && !(validateIfnumber || Str.isValidEmail(values.phoneOrEmail))) {
+ if (!!values.phoneOrEmail && !(validateIfnumber || Str.isValidEmail(values.phoneOrEmail))) {
ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.invalidContactMethod');
}
- if (!_.isEmpty(values.phoneOrEmail) && lodashGet(props.loginList, validateIfnumber || values.phoneOrEmail.toLowerCase())) {
+ if (!!values.phoneOrEmail && loginList?.[validateIfnumber || values.phoneOrEmail.toLowerCase()]) {
ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.enteredMethodIsAlreadySubmited');
}
return errors;
},
- // We don't need `props.loginList` because when submitting this form
- // the props.loginList gets updated, causing this function to run again.
+ // We don't need `loginList` because when submitting this form
+ // the loginList gets updated, causing this function to run again.
// https://github.com/Expensify/App/issues/20610
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
@@ -103,38 +87,32 @@ function NewContactMethodPage(props) {
return (
{
- if (!loginInputRef.current) {
- return;
- }
-
- loginInputRef.current.focus();
- }}
+ onEntryTransitionEnd={() => loginInputRef.current?.focus()}
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
testID={NewContactMethodPage.displayName}
>
- {props.translate('common.pleaseEnterEmailOrPhoneNumber')}
-
+ {translate('common.pleaseEnterEmailOrPhoneNumber')}
+
(loginInputRef.current = el)}
+ ref={loginInputRef}
inputID={INPUT_IDS.PHONE_OR_EMAIL}
autoCapitalize="none"
enterKeyHint="done"
@@ -146,13 +124,8 @@ function NewContactMethodPage(props) {
);
}
-NewContactMethodPage.propTypes = propTypes;
-NewContactMethodPage.defaultProps = defaultProps;
NewContactMethodPage.displayName = 'NewContactMethodPage';
-export default compose(
- withLocalize,
- withOnyx({
- loginList: {key: ONYXKEYS.LOGIN_LIST},
- }),
-)(NewContactMethodPage);
+export default withOnyx({
+ loginList: {key: ONYXKEYS.LOGIN_LIST},
+})(NewContactMethodPage);
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx
similarity index 56%
rename from src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
rename to src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx
index 5c1fa30a88f1..adf2680549c7 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -1,95 +1,82 @@
import {useFocusEffect} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MagicCodeInput from '@components/MagicCodeInput';
+import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {withNetwork} from '@components/OnyxProvider';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+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 compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as Session from '@userActions/Session';
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 {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- ...withLocalizePropTypes,
+type ValidateCodeFormHandle = {
+ focus: () => void;
+ focusLastSelected: () => void;
+};
+
+type ValidateCodeFormError = {
+ validateCode?: TranslationPaths;
+};
+
+type BaseValidateCodeFormOnyxProps = {
+ /** The details about the account that the user is signing in with */
+ account: OnyxEntry;
+};
+type ValidateCodeFormProps = {
/** The contact method being valdiated */
- contactMethod: PropTypes.string.isRequired,
+ contactMethod: string;
/** If the magic code has been resent previously */
- hasMagicCodeBeenSent: PropTypes.bool.isRequired,
+ hasMagicCodeBeenSent: boolean;
/** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** Value of partner name */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** Date when login was validated */
- validatedDate: PropTypes.string,
+ loginList: LoginList;
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }).isRequired,
+ /** Specifies autocomplete hints for the system, so it can provide autofill */
+ autoComplete?: AutoCompleteVariant;
/** Forwarded inner ref */
- innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
-
- /* Onyx Props */
-
- /** The details about the account that the user is signing in with */
- account: PropTypes.shape({
- /** Whether or not a sign on form is loading (being submitted) */
- isLoading: PropTypes.bool,
- }),
-
- /** Specifies autocomplete hints for the system, so it can provide autofill */
- autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired,
+ innerRef?: ForwardedRef;
};
-const defaultProps = {
- account: {},
- innerRef: () => {},
-};
+type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps;
-function BaseValidateCodeForm(props) {
+function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent, loginList, autoComplete = 'one-time-code', innerRef = () => {}}: BaseValidateCodeFormProps) {
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const [formError, setFormError] = useState({});
+ const [formError, setFormError] = useState({});
const [validateCode, setValidateCode] = useState('');
- const loginData = props.loginList[props.contactMethod];
- const inputValidateCodeRef = useRef();
+ const loginData = loginList[contactMethod];
+ const inputValidateCodeRef = useRef(null);
const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin');
- const shouldDisableResendValidateCode = props.network.isOffline || props.account.isLoading;
- const focusTimeoutRef = useRef(null);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case
+ const shouldDisableResendValidateCode = !!isOffline || account?.isLoading;
+ const focusTimeoutRef = useRef(null);
- useImperativeHandle(props.innerRef, () => ({
+ useImperativeHandle(innerRef, () => ({
focus() {
- if (!inputValidateCodeRef.current) {
- return;
- }
- inputValidateCodeRef.current.focus();
+ inputValidateCodeRef.current?.focus();
},
focusLastSelected() {
if (!inputValidateCodeRef.current) {
@@ -98,7 +85,9 @@ function BaseValidateCodeForm(props) {
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current);
}
- focusTimeoutRef.current = setTimeout(inputValidateCodeRef.current.focusLastSelected, CONST.ANIMATED_TRANSITION);
+ focusTimeoutRef.current = setTimeout(() => {
+ inputValidateCodeRef.current?.focusLastSelected();
+ }, CONST.ANIMATED_TRANSITION);
},
}));
@@ -110,7 +99,9 @@ function BaseValidateCodeForm(props) {
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current);
}
- focusTimeoutRef.current = setTimeout(inputValidateCodeRef.current.focusLastSelected, CONST.ANIMATED_TRANSITION);
+ focusTimeoutRef.current = setTimeout(() => {
+ inputValidateCodeRef.current?.focusLastSelected();
+ }, CONST.ANIMATED_TRANSITION);
return () => {
if (!focusTimeoutRef.current) {
return;
@@ -125,41 +116,39 @@ function BaseValidateCodeForm(props) {
if (!validateLoginError) {
return;
}
- User.clearContactMethodErrors(props.contactMethod, 'validateLogin');
+ User.clearContactMethodErrors(contactMethod, 'validateLogin');
// contactMethod is not added as a dependency since it does not change between renders
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
- if (!props.hasMagicCodeBeenSent) {
+ if (!hasMagicCodeBeenSent) {
return;
}
- inputValidateCodeRef.current.clear();
- }, [props.hasMagicCodeBeenSent]);
+ inputValidateCodeRef.current?.clear();
+ }, [hasMagicCodeBeenSent]);
/**
* Request a validate code / magic code be sent to verify this contact method
*/
const resendValidateCode = () => {
- User.requestContactMethodValidateCode(props.contactMethod);
- inputValidateCodeRef.current.clear();
+ User.requestContactMethodValidateCode(contactMethod);
+ inputValidateCodeRef.current?.clear();
};
/**
* Handle text input and clear formError upon text change
- *
- * @param {String} text
*/
const onTextInput = useCallback(
- (text) => {
+ (text: string) => {
setValidateCode(text);
setFormError({});
if (validateLoginError) {
- User.clearContactMethodErrors(props.contactMethod, 'validateLogin');
+ User.clearContactMethodErrors(contactMethod, 'validateLogin');
}
},
- [validateLoginError, props.contactMethod],
+ [validateLoginError, contactMethod],
);
/**
@@ -177,28 +166,27 @@ function BaseValidateCodeForm(props) {
}
setFormError({});
- User.validateSecondaryLogin(props.contactMethod, validateCode);
- }, [validateCode, props.contactMethod]);
+ User.validateSecondaryLogin(contactMethod, validateCode);
+ }, [validateCode, contactMethod]);
return (
<>
User.clearContactMethodErrors(props.contactMethod, 'validateCodeSent')}
+ onClose={() => User.clearContactMethodErrors(contactMethod, 'validateCodeSent')}
>
- {props.translate('validateCodeForm.magicCodeNotReceived')}
+ {translate('validateCodeForm.magicCodeNotReceived')}
- {props.hasMagicCodeBeenSent && (
+ {hasMagicCodeBeenSent && (
)}
User.clearContactMethodErrors(props.contactMethod, 'validateLogin')}
+ onClose={() => User.clearContactMethodErrors(contactMethod, 'validateLogin')}
>
>
);
}
-BaseValidateCodeForm.propTypes = propTypes;
-BaseValidateCodeForm.defaultProps = defaultProps;
BaseValidateCodeForm.displayName = 'BaseValidateCodeForm';
-export default compose(
- withLocalize,
- withOnyx({
- account: {key: ONYXKEYS.ACCOUNT},
- }),
- withNetwork(),
-)(BaseValidateCodeForm);
+export type {ValidateCodeFormProps, ValidateCodeFormHandle};
+
+export default withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+})(BaseValidateCodeForm);
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.tsx
similarity index 61%
rename from src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.js
rename to src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.tsx
index a193dc8d2eae..704405f93a2c 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.tsx
@@ -1,7 +1,8 @@
import React, {forwardRef} from 'react';
import BaseValidateCodeForm from './BaseValidateCodeForm';
+import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm';
-const ValidateCodeForm = forwardRef((props, ref) => (
+const ValidateCodeForm = forwardRef((props, ref) => (
(
/>
));
-ValidateCodeForm.displayName = 'ValidateCodeForm';
-
export default ValidateCodeForm;
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.tsx
similarity index 62%
rename from src/pages/settings/Profile/Contacts/ValidateCodeForm/index.js
rename to src/pages/settings/Profile/Contacts/ValidateCodeForm/index.tsx
index bb4e5ed36b47..453fc9c3f373 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.tsx
@@ -1,7 +1,8 @@
import React, {forwardRef} from 'react';
import BaseValidateCodeForm from './BaseValidateCodeForm';
+import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm';
-const ValidateCodeForm = forwardRef((props, ref) => (
+const ValidateCodeForm = forwardRef((props, ref) => (
(
/>
));
-ValidateCodeForm.displayName = 'ValidateCodeForm';
-
export default ValidateCodeForm;