diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 12b52524f113..147343e99ceb 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -4,6 +4,7 @@ import type {ValueOf} from 'type-fest'; import * as defaultAvatars from '@components/Icon/DefaultAvatars'; import {ConciergeAvatar, FallbackAvatar, NotificationsAvatar} from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; +import type {LoginList} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type IconAsset from '@src/types/utils/IconAsset'; import hashCode from './hashCode'; @@ -12,7 +13,7 @@ type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | type AvatarSource = IconAsset | string; -type LoginListIndicator = ValueOf | ''; +type LoginListIndicator = ValueOf | undefined; /** * Searches through given loginList for any contact method / login with an error. @@ -35,8 +36,8 @@ type LoginListIndicator = ValueOf | '' * } * }} */ -function hasLoginListError(loginList: Record): boolean { - return Object.values(loginList).some((loginData) => Object.values(loginData.errorFields ?? {}).some((field) => Object.keys(field ?? {}).length > 0)); +function hasLoginListError(loginList: OnyxEntry): boolean { + return Object.values(loginList ?? {}).some((loginData) => Object.values(loginData.errorFields ?? {}).some((field) => Object.keys(field ?? {}).length > 0)); } /** @@ -44,22 +45,22 @@ function hasLoginListError(loginList: Record): boolean { * an Info brick road status indicator. Currently this only applies if the user * has an unvalidated contact method. */ -function hasLoginListInfo(loginList: Record): boolean { - return !Object.values(loginList).every((field) => field.validatedDate); +function hasLoginListInfo(loginList: OnyxEntry): boolean { + return !Object.values(loginList ?? {}).every((field) => field.validatedDate); } /** * Gets the appropriate brick road indicator status for a given loginList. * Error status is higher priority, so we check for that first. */ -function getLoginListBrickRoadIndicator(loginList: Record): LoginListIndicator { +function getLoginListBrickRoadIndicator(loginList: OnyxEntry): LoginListIndicator { if (hasLoginListError(loginList)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } if (hasLoginListInfo(loginList)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; } - return ''; + return undefined; } /** @@ -227,4 +228,4 @@ export { hashText, isDefaultAvatar, }; -export type {AvatarSource}; +export type {AvatarSource, LoginListIndicator}; diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.tsx similarity index 69% rename from src/pages/settings/InitialSettingsPage.js rename to src/pages/settings/InitialSettingsPage.tsx index f19df710b41a..2f21ee61e8cc 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -1,13 +1,11 @@ import {useNavigationState} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {NativeModules, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; -import bankAccountPropTypes from '@components/bankAccountPropTypes'; -import cardPropTypes from '@components/cardPropTypes'; import ConfirmModal from '@components/ConfirmModal'; import CurrentUserPersonalDetailsSkeletonView from '@components/CurrentUserPersonalDetailsSkeletonView'; import HeaderPageLayout from '@components/HeaderPageLayout'; @@ -15,24 +13,21 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -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 useNetwork from '@hooks/useNetwork'; import useSingleExecution from '@hooks/useSingleExecution'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; -import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import {translatableTextPropTypes} from '@libs/Localize'; import getTopmostSettingsCentralPaneName from '@libs/Navigation/getTopmostSettingsCentralPaneName'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; -import walletTermsPropTypes from '@pages/EnablePayments/walletTermsPropTypes'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import * as Link from '@userActions/Link'; @@ -41,70 +36,68 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import * as Session from '@userActions/Session'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Icon as TIcon} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; -const propTypes = { - /* Onyx Props */ - - /** The session of the logged in person */ - session: PropTypes.shape({ - /** Email of the logged in person */ - email: PropTypes.string, - }), +type InitialSettingsPageOnyxProps = { + /** The user's session */ + session: OnyxEntry; /** The user's wallet account */ - userWallet: PropTypes.shape({ - /** The user's current wallet balance */ - currentBalance: PropTypes.number, - }), + userWallet: OnyxEntry; /** List of bank accounts */ - bankAccountList: PropTypes.objectOf(bankAccountPropTypes), + bankAccountList: OnyxEntry; /** List of user's cards */ - fundList: PropTypes.objectOf(cardPropTypes), + fundList: OnyxEntry; /** Information about the user accepting the terms for payments */ - walletTerms: walletTermsPropTypes, + walletTerms: OnyxEntry; /** 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)), - }), - ), - - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, + loginList: OnyxEntry; }; -const defaultProps = { - session: {}, - userWallet: { - currentBalance: 0, - }, - walletTerms: {}, - bankAccountList: {}, - fundList: null, - loginList: {}, - ...withCurrentUserPersonalDetailsDefaultProps, +type InitialSettingsPageProps = InitialSettingsPageOnyxProps & WithCurrentUserPersonalDetailsProps; + +type MenuData = { + translationKey: TranslationPaths; + icon: IconAsset; + routeName?: Route; + brickRoadIndicator?: ValueOf; + action?: () => void; + link?: string | (() => Promise); + iconType?: typeof CONST.ICON_TYPE_ICON | typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; + iconStyles?: StyleProp; + fallbackIcon?: IconAsset; + shouldStackHorizontally?: boolean; + avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + floatRightAvatars?: TIcon[]; + title?: string; + shouldShowRightIcon?: boolean; + iconRight?: IconAsset; }; -function InitialSettingsPage(props) { +type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]}; + +function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails}: InitialSettingsPageProps) { + const network = useNetwork(); const theme = useTheme(); const styles = useThemeStyles(); const {isExecuting, singleExecution} = useSingleExecution(); const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); - const {translate} = useLocalize(); + const {translate, formatPhoneNumber} = useLocalize(); const activeRoute = useNavigationState(getTopmostSettingsCentralPaneName); - const emojiCode = lodashGet(props, 'currentUserPersonalDetails.status.emojiCode', ''); + const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); @@ -112,13 +105,13 @@ function InitialSettingsPage(props) { Wallet.openInitialSettingsPage(); }, []); - const toggleSignoutConfirmModal = (value) => { + const toggleSignoutConfirmModal = (value: boolean) => { setShouldShowSignoutConfirmModal(value); }; const signOut = useCallback( (shouldForceSignout = false) => { - if (!props.network.isOffline || shouldForceSignout) { + if (!network.isOffline || shouldForceSignout) { Session.signOutAndRedirectToSignIn(); return; } @@ -126,18 +119,18 @@ function InitialSettingsPage(props) { // When offline, warn the user that any actions they took while offline will be lost if they sign out toggleSignoutConfirmModal(true); }, - [props.network.isOffline], + [network.isOffline], ); /** * Retuns a list of menu items data for account section - * @returns {Object} object with translationKey, style and items for the account section + * @returns object with translationKey, style and items for the account section */ - const accountMenuItemsData = useMemo(() => { - const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); - const paymentCardList = props.fundList || {}; + const accountMenuItemsData: Menu = useMemo(() => { + const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(loginList); + const paymentCardList = fundList; - const defaultMenu = { + const defaultMenu: Menu = { sectionStyle: styles.accountSettingsSectionContainer, sectionTranslationKey: 'initialSettingsPage.account', items: [ @@ -157,9 +150,9 @@ function InitialSettingsPage(props) { icon: Expensicons.Wallet, routeName: ROUTES.SETTINGS_WALLET, brickRoadIndicator: - PaymentMethods.hasPaymentMethodError(props.bankAccountList, paymentCardList) || !_.isEmpty(props.userWallet.errors) || !_.isEmpty(props.walletTerms.errors) + PaymentMethods.hasPaymentMethodError(bankAccountList, paymentCardList) || !isEmptyObject(userWallet?.errors) || !isEmptyObject(walletTerms?.errors) ? 'error' - : null, + : undefined, }, { translationKey: 'common.preferences', @@ -182,39 +175,38 @@ function InitialSettingsPage(props) { }; if (NativeModules.HybridAppModule) { - const hybridAppMenuItems = _.filter( - [ - { - translationKey: 'initialSettingsPage.returnToClassic', - icon: Expensicons.RotateLeft, - shouldShowRightIcon: true, - iconRight: Expensicons.NewWindow, - action: () => NativeModules.HybridAppModule.closeReactNativeApp(), + const hybridAppMenuItems: MenuData[] = [ + { + translationKey: 'initialSettingsPage.returnToClassic' as const, + icon: Expensicons.RotateLeft, + shouldShowRightIcon: true, + iconRight: Expensicons.NewWindow, + action: () => { + NativeModules.HybridAppModule.closeReactNativeApp(); }, - ...defaultMenu.items, - ], - (item) => item.translationKey !== 'initialSettingsPage.signOut' && item.translationKey !== 'initialSettingsPage.goToExpensifyClassic', - ); + }, + ...defaultMenu.items, + ].filter((item) => item.translationKey !== 'initialSettingsPage.signOut' && item.translationKey !== 'exitSurvey.goToExpensifyClassic'); return {sectionStyle: styles.accountSettingsSectionContainer, sectionTranslationKey: 'initialSettingsPage.account', items: hybridAppMenuItems}; } return defaultMenu; - }, [props.bankAccountList, props.fundList, props.loginList, props.userWallet.errors, props.walletTerms.errors, signOut, styles.accountSettingsSectionContainer]); + }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, signOut]); /** * Retuns a list of menu items data for general section - * @returns {Object} object with translationKey, style and items for the general section + * @returns object with translationKey, style and items for the general section */ - const generalMenuItemsData = useMemo( + const generalMenuItemsData: Menu = useMemo( () => ({ sectionStyle: { ...styles.pt4, }, - sectionTranslationKey: 'initialSettingsPage.general', + sectionTranslationKey: 'initialSettingsPage.general' as const, items: [ { - translationKey: 'initialSettingsPage.help', + translationKey: 'initialSettingsPage.help' as const, icon: Expensicons.QuestionMark, action: () => { Link.openExternalLink(CONST.NEWHELP_URL); @@ -224,7 +216,7 @@ function InitialSettingsPage(props) { link: CONST.NEWHELP_URL, }, { - translationKey: 'initialSettingsPage.about', + translationKey: 'initialSettingsPage.about' as const, icon: Expensicons.Info, routeName: ROUTES.SETTINGS_ABOUT, }, @@ -235,20 +227,20 @@ function InitialSettingsPage(props) { /** * Retuns JSX.Element with menu items - * @param {Object} menuItemsData list with menu items data - * @returns {JSX.Element} the menu items for passed data + * @param menuItemsData list with menu items data + * @returns the menu items for passed data */ const getMenuItemsSection = useCallback( - (menuItemsData) => { + (menuItemsData: Menu) => { /** - * @param {Boolean} isPaymentItem whether the item being rendered is the payments menu item - * @returns {String|undefined} the user's wallet balance + * @param isPaymentItem whether the item being rendered is the payments menu item + * @returns the user's wallet balance */ - const getWalletBalance = (isPaymentItem) => (isPaymentItem ? CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance) : undefined); + const getWalletBalance = (isPaymentItem: boolean): string | undefined => (isPaymentItem ? CurrencyUtils.convertToDisplayString(userWallet?.currentBalance) : undefined); - const openPopover = (link, event) => { + const openPopover = (link: string | (() => Promise) | undefined, event: GestureResponderEvent | MouseEvent) => { if (typeof link === 'function') { - link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current)); + link?.()?.then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current)); } else if (link) { ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, link, popoverAnchor.current); } @@ -257,13 +249,13 @@ function InitialSettingsPage(props) { return ( {translate(menuItemsData.sectionTranslationKey)} - {_.map(menuItemsData.items, (item, index) => { + {menuItemsData.items.map((item) => { const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; const isPaymentItem = item.translationKey === 'common.wallet'; return ( openPopover(item.link, event) : undefined} - focused={activeRoute && item.routeName && activeRoute.toLowerCase().replaceAll('_', '') === item.routeName.toLowerCase().replaceAll('/', '')} + focused={!!activeRoute && !!item.routeName && !!(activeRoute.toLowerCase().replaceAll('_', '') === item.routeName.toLowerCase().replaceAll('/', ''))} isPaneMenu iconRight={item.iconRight} shouldShowRightIcon={item.shouldShowRightIcon} @@ -306,7 +298,7 @@ function InitialSettingsPage(props) { styles.sectionMenuItem, styles.hoveredComponentBG, translate, - props.userWallet.currentBalance, + userWallet?.currentBalance, isExecuting, singleExecution, activeRoute, @@ -317,20 +309,22 @@ function InitialSettingsPage(props) { const accountMenuItems = useMemo(() => getMenuItemsSection(accountMenuItemsData), [accountMenuItemsData, getMenuItemsSection]); const generalMenuItems = useMemo(() => getMenuItemsSection(generalMenuItemsData), [generalMenuItemsData, getMenuItemsSection]); - const currentUserDetails = props.currentUserPersonalDetails || {}; - const avatarURL = lodashGet(currentUserDetails, 'avatar', ''); - const accountID = lodashGet(currentUserDetails, 'accountID', ''); + const currentUserDetails = currentUserPersonalDetails; + const avatarURL = currentUserDetails?.avatar ?? ''; + const accountID = currentUserDetails?.accountID ?? ''; const headerContent = ( - {_.isEmpty(props.currentUserPersonalDetails) || _.isUndefined(props.currentUserPersonalDetails.displayName) ? ( + {isEmptyObject(currentUserPersonalDetails) || currentUserPersonalDetails.displayName === undefined ? ( ) : ( <> Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE)} > @@ -345,7 +339,9 @@ function InitialSettingsPage(props) { Navigation.navigate(ROUTES.SETTINGS_STATUS)} > @@ -364,39 +360,39 @@ function InitialSettingsPage(props) { Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID))} + onViewPhotoPress={() => Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)} originalFileName={currentUserDetails.originalFileName} - headerTitle={props.translate('profilePage.profileAvatar')} - fallbackIcon={lodashGet(currentUserDetails, 'fallbackIcon')} + headerTitle={translate('profilePage.profileAvatar')} + fallbackIcon={currentUserDetails?.fallbackIcon} /> - {props.currentUserPersonalDetails.displayName ? props.currentUserPersonalDetails.displayName : props.formatPhoneNumber(props.session.email)} + {currentUserPersonalDetails.displayName ? currentUserPersonalDetails.displayName : formatPhoneNumber(session?.email ?? '')} - {Boolean(props.currentUserPersonalDetails.displayName) && ( + {Boolean(currentUserPersonalDetails.displayName) && ( - {props.formatPhoneNumber(props.session.email)} + {formatPhoneNumber(session?.email ?? '')} )} @@ -432,17 +428,10 @@ function InitialSettingsPage(props) { ); } -InitialSettingsPage.propTypes = propTypes; -InitialSettingsPage.defaultProps = defaultProps; InitialSettingsPage.displayName = 'InitialSettingsPage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, +export default withCurrentUserPersonalDetails( + withOnyx({ userWallet: { key: ONYXKEYS.USER_WALLET, }, @@ -458,6 +447,8 @@ export default compose( loginList: { key: ONYXKEYS.LOGIN_LIST, }, - }), - withNetwork(), -)(InitialSettingsPage); + session: { + key: ONYXKEYS.SESSION, + }, + })(InitialSettingsPage), +); diff --git a/src/styles/index.ts b/src/styles/index.ts index d1d60bbe8a75..deb0431273fa 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2641,7 +2641,7 @@ const styles = (theme: ThemeColors) => paddingLeft: 13, fontSize: 13, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, - fontWeight: 400, + fontWeight: '400', lineHeight: 16, color: theme.textSupporting, }, diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index aaa7058737ae..6300d416035a 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -8,6 +8,10 @@ import type {CSSProperties, FocusEventHandler, KeyboardEventHandler, MouseEventH import 'react-native'; import type {BootSplashModule} from '@libs/BootSplash/types'; +type HybridAppModule = { + closeReactNativeApp: () => void; +}; + declare module 'react-native' { // <------ REACT NATIVE WEB (0.19.0) ------> // Extracted from react-native-web, packages/react-native-web/src/exports/View/types.js @@ -351,5 +355,6 @@ declare module 'react-native' { interface NativeModulesStatic { BootSplash: BootSplashModule; + HybridAppModule: HybridAppModule; } }