From f32ffa8b6350f504e953525b14bc1e28c4b7ac5f Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 18:14:34 +0700 Subject: [PATCH 01/13] Migrate NewContactMethodPage to TS --- src/ONYXKEYS.ts | 2 +- src/libs/Navigation/types.ts | 8 +- ...MethodPage.js => NewContactMethodPage.tsx} | 96 +++++++------------ src/types/onyx/Form.ts | 6 +- src/types/onyx/index.ts | 3 +- 5 files changed, 50 insertions(+), 65 deletions(-) rename src/pages/settings/Profile/Contacts/{NewContactMethodPage.js => NewContactMethodPage.tsx} (56%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7abf6db1769d..ae0507bf0360 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -514,7 +514,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: OnyxTypes.NewContactMethodForm; [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WAYPOINT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WAYPOINT_FORM_DRAFT]: OnyxTypes.Form; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b4a77f96cc74..407b1141c60e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -56,9 +56,13 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.PROFILE.PERSONAL_DETAILS.DATE_OF_BIRTH]: undefined; [SCREENS.SETTINGS.PROFILE.PERSONAL_DETAILS.ADDRESS]: undefined; [SCREENS.SETTINGS.PROFILE.PERSONAL_DETAILS.ADDRESS_COUNTRY]: undefined; - [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: undefined; + [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: { + backTo: Routes; + }; [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: undefined; - [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: undefined; + [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/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 8f6982e24b98..508d3fe4fbeb 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -1,55 +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 {OnyxFormValuesFields} 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 * 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 type {LoginList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -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(PropTypes.string)), - - /** 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: OnyxFormValuesFields) => { const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail); const validateIfnumber = LoginUtils.validateNumber(phoneLogin); const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase(); @@ -57,35 +42,37 @@ 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: OnyxFormValuesFields): Errors => { const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail); const validateIfnumber = LoginUtils.validateNumber(phoneLogin); const errors = {}; - if (_.isEmpty(values.phoneOrEmail)) { + if (isEmptyObject(values.phoneOrEmail)) { ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.contactMethodRequired'); } - if (!_.isEmpty(values.phoneOrEmail) && !(validateIfnumber || Str.isValidEmail(values.phoneOrEmail))) { + if (!isEmptyObject(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())) { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + if (!isEmptyObject(values.phoneOrEmail) && loginList && 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 [], @@ -101,38 +88,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="phoneOrEmail" autoCapitalize="none" enterKeyHint="done" @@ -144,13 +125,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/types/onyx/Form.ts b/src/types/onyx/Form.ts index c3bcec2a2d3b..af3fdcc6869a 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -30,6 +30,10 @@ type DisplayNameForm = Form<{ lastName: string; }>; +type NewContactMethodForm = Form<{ + phoneOrEmail: string; +}>; + type NewRoomForm = Form<{ roomName?: string; welcomeMessage?: string; @@ -56,4 +60,4 @@ type PrivateNotesForm = Form<{ export default Form; -export type {AddDebitCardForm, DateOfBirthForm, PrivateNotesForm, DisplayNameForm, FormValueType, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; +export type {AddDebitCardForm, DateOfBirthForm, PrivateNotesForm, DisplayNameForm, FormValueType, NewContactMethodForm, NewRoomForm, BaseForm, IKnowATeacherForm, IntroSchoolPrincipalForm}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 5b04cae58671..a0f1d838c3b0 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, PrivateNotesForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewContactMethodForm, NewRoomForm, PrivateNotesForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -147,6 +147,7 @@ export type { PolicyReportField, PolicyReportFields, RecentlyUsedReportFields, + NewContactMethodForm, NewRoomForm, IKnowATeacherForm, IntroSchoolPrincipalForm, From 70645b1d285f8390093d9169640d93c8d428e359 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 18:14:52 +0700 Subject: [PATCH 02/13] Migrate ContactMethodsPage to TS --- ...tMethodsPage.js => ContactMethodsPage.tsx} | 119 +++++++----------- 1 file changed, 46 insertions(+), 73 deletions(-) rename src/pages/settings/Profile/Contacts/{ContactMethodsPage.js => ContactMethodsPage.tsx} (53%) 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 34399daf55e3..fbff97f243c8 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,89 +12,68 @@ 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 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(PropTypes.string)), - - /** 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', ROUTES.SETTINGS_PROFILE); + 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 && loginList[loginName].partnerUserID === session?.email ? 0 : 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) => { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + const login = loginList && loginList[loginName]; + const pendingAction = login?.pendingFields?.deletedLogin ?? login?.pendingFields?.addedLogin; + if (!login?.partnerUserID && isEmptyObject(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; + const partnerUserID = login?.partnerUserID ?? loginName; + const menuItemTitle = Str.isSMSLogin(partnerUserID) ? formatPhoneNumber(partnerUserID) : partnerUserID; return ( ); @@ -125,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}