diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index ee3b3607401e0..e9c5b80b37ac3 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,5 +1,5 @@ import lodashIsEqual from 'lodash/isEqual'; -import type {ForwardedRef, MutableRefObject, ReactNode} from 'react'; +import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react'; import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -18,7 +18,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {RegisterInput} from './FormContext'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {FormInputErrors, FormOnyxValues, FormProps, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types'; +import type {FormInputErrors, FormOnyxValues, FormProps, FormRef, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -73,10 +73,6 @@ type FormProviderProps = FormProvider submitFlexEnabled?: boolean; }; -type FormRef = { - resetForm: (optionalValue: FormOnyxValues) => void; -}; - function FormProvider( { formID, @@ -393,6 +389,6 @@ export default withOnyx({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any key: (props) => `${props.formID}Draft` as any, }, -})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; +})(forwardRef(FormProvider)) as (props: Omit & RefAttributes, keyof FormProviderOnyxProps>) => ReactNode; export type {FormProviderProps}; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 33d1273084490..c4f1fe081ee56 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -128,8 +128,25 @@ type FormProps = { disablePressOnEnter?: boolean; }; +type FormRef = { + resetForm: (optionalValue: FormOnyxValues) => void; +}; + type InputRefs = Record>; type FormInputErrors = Partial, MaybePhraseKey>>; -export type {FormProps, ValidInputs, InputComponentValueProps, FormValue, ValueTypeKey, FormOnyxValues, FormOnyxKeys, FormInputErrors, InputRefs, InputComponentBaseProps, ValueTypeMap}; +export type { + FormProps, + ValidInputs, + InputComponentValueProps, + FormValue, + ValueTypeKey, + FormOnyxValues, + FormOnyxKeys, + FormInputErrors, + InputRefs, + InputComponentBaseProps, + ValueTypeMap, + FormRef, +}; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index fdd657f801f26..6655d78cb0a8b 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -33,7 +33,7 @@ import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BlockedFromConcierge, FrequentlyUsedEmoji, Policy} from '@src/types/onyx'; +import type {BlockedFromConcierge, CustomStatusDraft, FrequentlyUsedEmoji, Policy} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; @@ -945,7 +945,7 @@ function clearCustomStatus() { * @param status.emojiCode * @param status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared */ -function updateDraftCustomStatus(status: Status) { +function updateDraftCustomStatus(status: CustomStatusDraft) { Onyx.merge(ONYXKEYS.CUSTOM_STATUS_DRAFT, status); } diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.js b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx similarity index 76% rename from src/pages/settings/Profile/CustomStatus/StatusPage.js rename to src/pages/settings/Profile/CustomStatus/StatusPage.tsx index f6c5f5543fa04..bb7be4a6866cb 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -1,10 +1,12 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types'; import HeaderPageLayout from '@components/HeaderPageLayout'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -13,13 +15,13 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; @@ -27,41 +29,43 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/SettingsStatusSetForm'; +import type {CustomStatusDraft} from '@src/types/onyx'; -const INPUT_IDS = { - EMOJI_CODE: 'emojiCode', - STATUS_TEXT: 'statusText', +type StatusPageOnyxProps = { + draftStatus: OnyxEntry; }; -const propTypes = { - ...withCurrentUserPersonalDetailsPropTypes, -}; +type StatusPageProps = StatusPageOnyxProps & WithCurrentUserPersonalDetailsProps; const initialEmoji = '💬'; -function StatusPage({draftStatus, currentUserPersonalDetails}) { +function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const formRef = useRef(null); - const [brickRoadIndicator, setBrickRoadIndicator] = useState(''); - const currentUserEmojiCode = lodashGet(currentUserPersonalDetails, 'status.emojiCode', ''); - const currentUserStatusText = lodashGet(currentUserPersonalDetails, 'status.text', ''); - const currentUserClearAfter = lodashGet(currentUserPersonalDetails, 'status.clearAfter', ''); - const draftEmojiCode = lodashGet(draftStatus, 'emojiCode'); - const draftText = lodashGet(draftStatus, 'text'); - const draftClearAfter = lodashGet(draftStatus, 'clearAfter'); - + const formRef = useRef(null); + const [brickRoadIndicator, setBrickRoadIndicator] = useState>(); + const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; + const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? ''; + const currentUserClearAfter = currentUserPersonalDetails?.status?.clearAfter ?? ''; + const draftEmojiCode = draftStatus?.emojiCode; + const draftText = draftStatus?.text; + const draftClearAfter = draftStatus?.clearAfter; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const defaultEmoji = draftEmojiCode || currentUserEmojiCode; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const defaultText = draftText || currentUserStatusText; const customClearAfter = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const dataToShow = draftClearAfter || currentUserClearAfter; return DateUtils.getLocalizedTimePeriodDescription(dataToShow); }, [draftClearAfter, currentUserClearAfter]); const isValidClearAfterDate = useCallback(() => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const clearAfterTime = draftClearAfter || currentUserClearAfter; if (clearAfterTime === CONST.CUSTOM_STATUS_TYPES.NEVER || clearAfterTime === '') { return true; @@ -72,11 +76,12 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []); const updateStatus = useCallback( - ({emojiCode, statusText}) => { + ({emojiCode, statusText}: FormOnyxValues) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const clearAfterTime = draftClearAfter || currentUserClearAfter || CONST.CUSTOM_STATUS_TYPES.NEVER; const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime}); if (!isValid && clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER) { - setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); + setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); return; } User.updateCustomStatus({ @@ -84,7 +89,6 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { emojiCode: !emojiCode && statusText ? initialEmoji : emojiCode, clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); - User.clearDraftCustomStatus(); InteractionManager.runAfterInteractions(() => { navigateBackToPreviousScreen(); @@ -100,13 +104,14 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { emojiCode: '', clearAfter: DateUtils.getEndOfToday(), }); - formRef.current.resetForm({[INPUT_IDS.EMOJI_CODE]: ''}); + formRef.current?.resetForm({[INPUT_IDS.EMOJI_CODE]: ''}); + InteractionManager.runAfterInteractions(() => { navigateBackToPreviousScreen(); }); }; - useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]); + useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]); useEffect(() => { if (!currentUserEmojiCode && !currentUserClearAfter && !draftClearAfter) { @@ -119,7 +124,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const validateForm = useCallback(() => { + const validateForm = useCallback((): FormInputErrors => { if (brickRoadIndicator) { return {clearAfter: ''}; } @@ -157,8 +162,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { @@ -166,7 +172,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { InputComponent={TextInput} ref={inputCallbackRef} inputID={INPUT_IDS.STATUS_TEXT} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} label={translate('statusPage.message')} accessibilityLabel={INPUT_IDS.STATUS_TEXT} defaultValue={defaultText} @@ -198,13 +204,11 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { } StatusPage.displayName = 'StatusPage'; -StatusPage.propTypes = propTypes; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ draftStatus: { key: () => ONYXKEYS.CUSTOM_STATUS_DRAFT, }, - }), -)(StatusPage); + })(StatusPage), +); diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.tsx similarity index 67% rename from src/pages/settings/Profile/DisplayNamePage.js rename to src/pages/settings/Profile/DisplayNamePage.tsx index c443a91a73134..2e7a67509139e 100644 --- a/src/pages/settings/Profile/DisplayNamePage.js +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -1,19 +1,19 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -22,39 +22,27 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/DisplayNameForm'; -const propTypes = { - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, - isLoadingApp: PropTypes.bool, +type DisplayNamePageOnyxProps = { + isLoadingApp: OnyxEntry; }; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - isLoadingApp: true, -}; +type DisplayNamePageProps = DisplayNamePageOnyxProps & WithCurrentUserPersonalDetailsProps; /** * Submit form to update user's first and last name (and display name) - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName */ -const updateDisplayName = (values) => { +const updateDisplayName = (values: FormOnyxValues) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); }; -function DisplayNamePage(props) { +function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: DisplayNamePageProps) { const styles = useThemeStyles(); - const currentUserDetails = props.currentUserPersonalDetails || {}; + const {translate} = useLocalize(); + + const currentUserDetails = currentUserPersonalDetails ?? {}; - /** - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName - * @returns {Object} - An object containing the errors for each inputID - */ - const validate = (values) => { - const errors = {}; + const validate = (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { @@ -77,7 +65,6 @@ function DisplayNamePage(props) { } return errors; }; - return ( Navigation.goBack()} /> - {props.isLoadingApp ? ( + {isLoadingApp ? ( ) : ( - {props.translate('displayNamePage.isShownOnProfile')} + {translate('displayNamePage.isShownOnProfile')} @@ -119,10 +106,10 @@ function DisplayNamePage(props) { InputComponent={TextInput} inputID={INPUT_IDS.LAST_NAME} name="lname" - label={props.translate('common.lastName')} - aria-label={props.translate('common.lastName')} + label={translate('common.lastName')} + aria-label={translate('common.lastName')} role={CONST.ROLE.PRESENTATION} - defaultValue={lodashGet(currentUserDetails, 'lastName', '')} + defaultValue={currentUserDetails.lastName ?? ''} spellCheck={false} /> @@ -132,16 +119,12 @@ function DisplayNamePage(props) { ); } -DisplayNamePage.propTypes = propTypes; -DisplayNamePage.defaultProps = defaultProps; DisplayNamePage.displayName = 'DisplayNamePage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, - }), -)(DisplayNamePage); + })(DisplayNamePage), +); diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.tsx similarity index 53% rename from src/pages/settings/Profile/ProfilePage.js rename to src/pages/settings/Profile/ProfilePage.tsx index 2fa133f416162..58a323bf0a10f 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,10 +1,8 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemGroup from '@components/MenuItemGroup'; @@ -12,63 +10,36 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; -import {translatableTextPropTypes} from '@libs/Localize'; +import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as UserUtils from '@libs/UserUtils'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {LoginList, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; -const propTypes = { - /* Onyx Props */ - - /** Login list for the user that is signed in */ - loginList: PropTypes.objectOf( - PropTypes.shape({ - /** Date login was validated, used to show brickroad info status */ - validatedDate: PropTypes.string, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), - }), - ), - +type ProfilePageOnyxProps = { + loginList: OnyxEntry; /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - dob: PropTypes.string, - - /** User's home address */ - address: PropTypes.shape({ - street: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - zip: PropTypes.string, - country: PropTypes.string, - }), - }), - - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, - ...withCurrentUserPersonalDetailsPropTypes, + privatePersonalDetails: OnyxEntry; }; -const defaultProps = { - loginList: {}, - ...withCurrentUserPersonalDetailsDefaultProps, - privatePersonalDetails: { +type ProfilePageProps = ProfilePageOnyxProps & WithCurrentUserPersonalDetailsProps; + +function ProfilePage({ + loginList, + privatePersonalDetails = { legalFirstName: '', legalLastName: '', dob: '', @@ -81,79 +52,73 @@ const defaultProps = { country: '', }, }, -}; - -function ProfilePage(props) { + currentUserPersonalDetails, +}: ProfilePageProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const getPronouns = () => { - let pronounsKey = lodashGet(props.currentUserPersonalDetails, 'pronouns', ''); - if (pronounsKey.startsWith(CONST.PRONOUNS.PREFIX)) { - pronounsKey = pronounsKey.slice(CONST.PRONOUNS.PREFIX.length); - } + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); - if (!pronounsKey) { - return props.translate('profilePage.selectYourPronouns'); - } - return props.translate(`pronouns.${pronounsKey}`); + const getPronouns = (): string => { + const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; + return pronounsKey ? translate(`pronouns.${pronounsKey}` as TranslationPaths) : translate('profilePage.selectYourPronouns'); }; - const currentUserDetails = props.currentUserPersonalDetails || {}; - const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); - const emojiCode = lodashGet(props, 'currentUserPersonalDetails.status.emojiCode', ''); - const {isSmallScreenWidth} = useWindowDimensions(); + + const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(loginList); + const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; usePrivatePersonalDetails(); - const privateDetails = props.privatePersonalDetails || {}; - const legalName = `${privateDetails.legalFirstName || ''} ${privateDetails.legalLastName || ''}`.trim(); - const isLoadingPersonalDetails = lodashGet(props.privatePersonalDetails, 'isLoading', true); + const privateDetails = privatePersonalDetails ?? {}; + const legalName = `${privateDetails.legalFirstName ?? ''} ${privateDetails.legalLastName ?? ''}`.trim(); + const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true; const publicOptions = [ { - description: props.translate('displayNamePage.headerTitle'), - title: lodashGet(currentUserDetails, 'displayName', ''), + description: translate('displayNamePage.headerTitle'), + title: currentUserPersonalDetails?.displayName ?? '', pageRoute: ROUTES.SETTINGS_DISPLAY_NAME, }, { - description: props.translate('contacts.contactMethod'), - title: props.formatPhoneNumber(lodashGet(currentUserDetails, 'login', '')), + description: translate('contacts.contactMethod'), + title: LocalePhoneNumber.formatPhoneNumber(currentUserPersonalDetails?.login ?? ''), pageRoute: ROUTES.SETTINGS_CONTACT_METHODS.route, brickRoadIndicator: contactMethodBrickRoadIndicator, }, { - description: props.translate('statusPage.status'), - title: emojiCode ? `${emojiCode} ${lodashGet(props, 'currentUserPersonalDetails.status.text', '')}` : '', + description: translate('statusPage.status'), + title: emojiCode ? `${emojiCode} ${currentUserPersonalDetails?.status?.text ?? ''}` : '', pageRoute: ROUTES.SETTINGS_STATUS, }, { - description: props.translate('pronounsPage.pronouns'), + description: translate('pronounsPage.pronouns'), title: getPronouns(), pageRoute: ROUTES.SETTINGS_PRONOUNS, }, { - description: props.translate('timezonePage.timezone'), - title: `${lodashGet(currentUserDetails, 'timezone.selected', '')}`, + description: translate('timezonePage.timezone'), + title: currentUserPersonalDetails?.timezone?.selected ?? '', pageRoute: ROUTES.SETTINGS_TIMEZONE, }, ]; useEffect(() => { - App.openProfile(props.currentUserPersonalDetails); - }, [props.currentUserPersonalDetails]); + App.openProfile(currentUserPersonalDetails as PersonalDetails); + }, [currentUserPersonalDetails]); const privateOptions = [ { - description: props.translate('privatePersonalDetails.legalName'), + description: translate('privatePersonalDetails.legalName'), title: legalName, pageRoute: ROUTES.SETTINGS_LEGAL_NAME, }, { - description: props.translate('common.dob'), - title: privateDetails.dob || '', + description: translate('common.dob'), + title: privateDetails.dob ?? '', pageRoute: ROUTES.SETTINGS_DATE_OF_BIRTH, }, { - description: props.translate('privatePersonalDetails.address'), - title: PersonalDetailsUtils.getFormattedAddress(props.privatePersonalDetails), + description: translate('privatePersonalDetails.address'), + title: PersonalDetailsUtils.getFormattedAddress(privateDetails), pageRoute: ROUTES.SETTINGS_ADDRESS, }, ]; @@ -165,24 +130,25 @@ function ProfilePage(props) { shouldShowOfflineIndicatorInWideScreen > Navigation.goBack()} - shouldShowBackButton={props.isSmallScreenWidth} + shouldShowBackButton={isSmallScreenWidth} icon={Illustrations.Profile} />
- {_.map(publicOptions, (detail, index) => ( + {publicOptions.map((detail, index) => (
{isLoadingPersonalDetails ? ( - + ) : ( <> - {_.map(privateOptions, (detail, index) => ( + {privateOptions.map((detail, index) => ( ({ loginList: { key: ONYXKEYS.LOGIN_LIST, }, privatePersonalDetails: { key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, - user: { - key: ONYXKEYS.USER, - }, - }), -)(ProfilePage); + })(ProfilePage), +); diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.tsx similarity index 58% rename from src/pages/settings/Profile/PronounsPage.js rename to src/pages/settings/Profile/PronounsPage.tsx index 1d4675a42b8a8..5bd2737a98a4a 100644 --- a/src/pages/settings/Profile/PronounsPage.js +++ b/src/pages/settings/Profile/PronounsPage.tsx @@ -1,49 +1,43 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const propTypes = { - ...withCurrentUserPersonalDetailsPropTypes, - - /** Indicates whether the app is loading initial data */ - isLoadingApp: PropTypes.bool, +type PronounEntry = ListItem & { + value: string; }; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - isLoadingApp: true, +type PronounsPageOnyxProps = { + isLoadingApp: OnyxEntry; }; +type PronounsPageProps = PronounsPageOnyxProps & WithCurrentUserPersonalDetailsProps; -function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { +function PronounsPage({currentUserPersonalDetails, isLoadingApp = true}: PronounsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const currentPronouns = lodashGet(currentUserPersonalDetails, 'pronouns', ''); + const currentPronouns = currentUserPersonalDetails?.pronouns ?? ''; const currentPronounsKey = currentPronouns.substring(CONST.PRONOUNS.PREFIX.length); const [searchValue, setSearchValue] = useState(''); useEffect(() => { - if (isLoadingApp && _.isUndefined(currentUserPersonalDetails.pronouns)) { + if (isLoadingApp && !currentUserPersonalDetails.pronouns) { return; } - const currentPronounsText = _.chain(CONST.PRONOUNS_LIST) - .find((_value) => _value === currentPronounsKey) - .value(); + const currentPronounsText = CONST.PRONOUNS_LIST.find((value) => value === currentPronounsKey); setSearchValue(currentPronounsText ? translate(`pronouns.${currentPronounsText}`) : ''); @@ -51,34 +45,31 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingApp]); - const filteredPronounsList = useMemo(() => { - const pronouns = _.chain(CONST.PRONOUNS_LIST) - .map((value) => { - const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`; - const isCurrentPronouns = fullPronounKey === currentPronouns; + const filteredPronounsList = useMemo((): PronounEntry[] => { + const pronouns = CONST.PRONOUNS_LIST.map((value) => { + const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`; + const isCurrentPronouns = fullPronounKey === currentPronouns; - return { - text: translate(`pronouns.${value}`), - value: fullPronounKey, - keyForList: value, - isSelected: isCurrentPronouns, - }; - }) - .sortBy((pronoun) => pronoun.text.toLowerCase()) - .value(); + return { + text: translate(`pronouns.${value}`), + value: fullPronounKey, + keyForList: value, + isSelected: isCurrentPronouns, + }; + }).sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase())); const trimmedSearch = searchValue.trim(); if (trimmedSearch.length === 0) { return []; } - return _.filter(pronouns, (pronoun) => pronoun.text.toLowerCase().indexOf(trimmedSearch.toLowerCase()) >= 0); + return pronouns.filter((pronoun) => pronoun.text.toLowerCase().indexOf(trimmedSearch.toLowerCase()) >= 0); }, [searchValue, currentPronouns, translate]); - const headerMessage = searchValue.trim() && filteredPronounsList.length === 0 ? translate('common.noResultsFound') : ''; + const headerMessage = searchValue.trim() && filteredPronounsList?.length === 0 ? translate('common.noResultsFound') : ''; - const updatePronouns = (selectedPronouns) => { - PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : lodashGet(selectedPronouns, 'value', '')); + const updatePronouns = (selectedPronouns: PronounEntry) => { + PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : selectedPronouns?.value ?? ''); }; return ( @@ -86,7 +77,7 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { includeSafeAreaPaddingBottom={false} testID={PronounsPage.displayName} > - {isLoadingApp && _.isUndefined(currentUserPersonalDetails.pronouns) ? ( + {isLoadingApp && !currentUserPersonalDetails.pronouns ? ( ) : ( <> @@ -112,15 +103,12 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { ); } -PronounsPage.propTypes = propTypes; -PronounsPage.defaultProps = defaultProps; PronounsPage.displayName = 'PronounsPage'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, - }), -)(PronounsPage); + })(PronounsPage), +); diff --git a/src/pages/settings/Profile/TimezoneInitialPage.js b/src/pages/settings/Profile/TimezoneInitialPage.tsx similarity index 55% rename from src/pages/settings/Profile/TimezoneInitialPage.js rename to src/pages/settings/Profile/TimezoneInitialPage.tsx index 1b2596b9d369d..1df0063c57b17 100644 --- a/src/pages/settings/Profile/TimezoneInitialPage.js +++ b/src/pages/settings/Profile/TimezoneInitialPage.tsx @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -6,62 +5,56 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import Switch from '@components/Switch'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; -const propTypes = { - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, -}; +type TimezoneInitialPageProps = WithCurrentUserPersonalDetailsProps; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function TimezoneInitialPage(props) { +function TimezoneInitialPage({currentUserPersonalDetails}: TimezoneInitialPageProps) { const styles = useThemeStyles(); - const timezone = lodashGet(props.currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE); + const timezone: Timezone = currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE; + + const {translate} = useLocalize(); /** * Updates setting for automatic timezone selection. * Note: If we are updating automatically, we'll immediately calculate the user's timezone. - * - * @param {Boolean} isAutomatic */ - const updateAutomaticTimezone = (isAutomatic) => { + const updateAutomaticTimezone = (isAutomatic: boolean) => { PersonalDetails.updateAutomaticTimezone({ automatic: isAutomatic, - selected: isAutomatic ? Intl.DateTimeFormat().resolvedOptions().timeZone : timezone.selected, + selected: isAutomatic ? (Intl.DateTimeFormat().resolvedOptions().timeZone as SelectedTimezone) : timezone.selected, }); }; return ( Navigation.goBack()} /> - {props.translate('timezonePage.isShownOnProfile')} + {translate('timezonePage.isShownOnProfile')} - {props.translate('timezonePage.getLocationAutomatically')} + {translate('timezonePage.getLocationAutomatically')} Navigation.navigate(ROUTES.SETTINGS_TIMEZONE_SELECT)} @@ -71,8 +64,6 @@ function TimezoneInitialPage(props) { ); } -TimezoneInitialPage.propTypes = propTypes; -TimezoneInitialPage.defaultProps = defaultProps; TimezoneInitialPage.displayName = 'TimezoneInitialPage'; -export default compose(withLocalize, withCurrentUserPersonalDetails)(TimezoneInitialPage); +export default withCurrentUserPersonalDetails(TimezoneInitialPage); diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.tsx similarity index 53% rename from src/pages/settings/Profile/TimezoneSelectPage.js rename to src/pages/settings/Profile/TimezoneSelectPage.tsx index b6c8a5967abc3..3aff5f820cf8a 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.js +++ b/src/pages/settings/Profile/TimezoneSelectPage.tsx @@ -1,11 +1,11 @@ -import lodashGet from 'lodash/get'; import React, {useState} from 'react'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useInitialValue from '@hooks/useInitialValue'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; @@ -13,67 +13,45 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import TIMEZONES from '@src/TIMEZONES'; +import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; -const propTypes = { - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, -}; +type TimezoneSelectPageProps = Pick; /** * We add the current time to the key to fix a bug where the list options don't update unless the key is updated. - * @param {String} text - * @return {string} key for list item */ -const getKey = (text) => `${text}-${new Date().getTime()}`; +const getKey = (text: string): string => `${text}-${new Date().getTime()}`; -/** - * @param {Object} currentUserPersonalDetails - * @return {Object} user's timezone data - */ -const getUserTimezone = (currentUserPersonalDetails) => lodashGet(currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE); +const getUserTimezone = (currentUserPersonalDetails: ValueOf) => + currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE; -function TimezoneSelectPage(props) { +function TimezoneSelectPage({currentUserPersonalDetails}: TimezoneSelectPageProps) { const {translate} = useLocalize(); - const timezone = getUserTimezone(props.currentUserPersonalDetails); + const timezone = getUserTimezone(currentUserPersonalDetails); const allTimezones = useInitialValue(() => - _.chain(TIMEZONES) - .filter((tz) => !tz.startsWith('Etc/GMT')) - .map((text) => ({ - text, - keyForList: getKey(text), - isSelected: text === timezone.selected, - })) - .value(), + TIMEZONES.filter((tz: string) => !tz.startsWith('Etc/GMT')).map((text: string) => ({ + text, + keyForList: getKey(text), + isSelected: text === timezone.selected, + })), ); const [timezoneInputText, setTimezoneInputText] = useState(''); const [timezoneOptions, setTimezoneOptions] = useState(allTimezones); - /** - * @param {Object} timezone - * @param {String} timezone.text - */ - const saveSelectedTimezone = ({text}) => { - PersonalDetails.updateSelectedTimezone(text); + const saveSelectedTimezone = ({text}: {text: string}) => { + PersonalDetails.updateSelectedTimezone(text as SelectedTimezone); }; - /** - * @param {String} searchText - */ - const filterShownTimezones = (searchText) => { + const filterShownTimezones = (searchText: string) => { setTimezoneInputText(searchText); - const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) || []; + const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? []; setTimezoneOptions( - _.filter(allTimezones, (tz) => - _.every( - searchWords, - (word) => - tz.text - .toLowerCase() - .replace(/[^a-z0-9]/g, ' ') - .indexOf(word) > -1, + allTimezones.filter((tz) => + searchWords.every((word) => + tz.text + .toLowerCase() + .replace(/[^a-z0-9]/g, ' ') + .includes(word), ), ), ); @@ -95,7 +73,7 @@ function TimezoneSelectPage(props) { onChangeText={filterShownTimezones} onSelectRow={saveSelectedTimezone} sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]} - initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')} + initiallyFocusedOptionKey={timezoneOptions.find((tz) => tz.text === timezone.selected)?.keyForList} showScrollIndicator shouldShowTooltips={false} ListItem={RadioListItem} @@ -104,8 +82,6 @@ function TimezoneSelectPage(props) { ); } -TimezoneSelectPage.propTypes = propTypes; -TimezoneSelectPage.defaultProps = defaultProps; TimezoneSelectPage.displayName = 'TimezoneSelectPage'; export default withCurrentUserPersonalDetails(TimezoneSelectPage); diff --git a/src/types/form/SettingsStatusSetForm.ts b/src/types/form/SettingsStatusSetForm.ts index 9aeec26c4887d..53e42ba01cfa9 100644 --- a/src/types/form/SettingsStatusSetForm.ts +++ b/src/types/form/SettingsStatusSetForm.ts @@ -1,6 +1,23 @@ +import type {ValueOf} from 'type-fest'; import type Form from './Form'; -type SettingsStatusSetForm = Form; +const INPUT_IDS = { + EMOJI_CODE: 'emojiCode', + STATUS_TEXT: 'statusText', + clearAfter: 'clearAfter', +} as const; + +type InputID = ValueOf; + +type SettingsStatusSetForm = Form< + InputID, + { + [INPUT_IDS.EMOJI_CODE]: string; + [INPUT_IDS.STATUS_TEXT]: string; + [INPUT_IDS.clearAfter]: string; + } +>; // eslint-disable-next-line import/prefer-default-export export type {SettingsStatusSetForm}; +export default INPUT_IDS; diff --git a/src/types/onyx/CustomStatusDraft.ts b/src/types/onyx/CustomStatusDraft.ts index b2801a1d89e0b..73c8fa4baa1a4 100644 --- a/src/types/onyx/CustomStatusDraft.ts +++ b/src/types/onyx/CustomStatusDraft.ts @@ -1,9 +1,9 @@ type CustomStatusDraft = { /** The emoji code of the draft status */ - emojiCode: string; + emojiCode?: string; /** The text of the draft status */ - text: string; + text?: string; /** ISO 8601 format string, which represents the time when the status should be cleared */ clearAfter: string;