diff --git a/assets/images/cards-and-domains.svg b/assets/images/cards-and-domains.svg new file mode 100644 index 00000000000..4467ad4cf22 --- /dev/null +++ b/assets/images/cards-and-domains.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/home.svg b/assets/images/home.svg new file mode 100644 index 00000000000..6b2411407be --- /dev/null +++ b/assets/images/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/new-expensify.svg b/assets/images/new-expensify.svg index 7bfef1fd38b..89102ecbc5e 100644 --- a/assets/images/new-expensify.svg +++ b/assets/images/new-expensify.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/images/wrench.svg b/assets/images/wrench.svg new file mode 100644 index 00000000000..2865c40eb95 --- /dev/null +++ b/assets/images/wrench.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch index d64fc4fecf7..877521094cd 100644 --- a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch +++ b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch @@ -43,7 +43,7 @@ index 7558eb3..b7bb75e 100644 }) : STATE_TRANSITIONING_OR_BELOW_TOP; } + -+ const isHomeScreenAndNotOnTop = route.name === 'Home' && isScreenActive !== STATE_ON_TOP; ++ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Settings_Root') && isScreenActive !== STATE_ON_TOP; + const { headerShown = true, diff --git a/src/App.js b/src/App.js index 3553900bbc7..8045f4eb30a 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,7 @@ import Onyx from 'react-native-onyx'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; +import ActiveWorkspaceContextProvider from './components/ActiveWorkspace/ActiveWorkspaceProvider'; import ColorSchemeWrapper from './components/ColorSchemeWrapper'; import ComposeProviders from './components/ComposeProviders'; import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground'; @@ -69,6 +70,7 @@ function App() { PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, + ActiveWorkspaceContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index f434aa28186..69933a623be 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -490,6 +490,8 @@ const CONST = { // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', OLDDOT_URLS: { + ADMIN_POLICIES_URL: 'admin_policies', + ADMIN_DOMAINS_URL: 'admin_domains', INBOX: 'inbox', }, @@ -1013,6 +1015,7 @@ const CONST = { 3: 100, }, }, + CENTRAL_PANE_ANIMATION_HEIGHT: 200, LHN_SKELETON_VIEW_ITEM_HEIGHT: 64, EXPENSIFY_PARTNER_NAME: 'expensify.com', EMAIL: { @@ -1331,6 +1334,7 @@ const CONST = { REIMBURSEMENT_MANUAL: 'reimburseManual', }, ID_FAKE: '_FAKE_', + EMPTY: 'EMPTY', }, CUSTOM_UNITS: { @@ -1481,6 +1485,10 @@ const CONST = { OTHER_INVISIBLE_CHARACTERS: /[\u3164]/g, REPORT_FIELD_TITLE: /{report:([a-zA-Z]+)}/g, + + PATH_WITHOUT_POLICY_ID: /\/w\/[a-zA-Z0-9]+(\/|$)/, + + POLICY_ID_FROM_PATH: /\/w\/([a-zA-Z0-9]+)(\/|$)/, }, PRONOUNS: { @@ -1490,7 +1498,7 @@ const CONST = { GUIDES_CALL_TASK_IDS: { CONCIERGE_DM: 'NewExpensifyConciergeDM', WORKSPACE_INITIAL: 'WorkspaceHome', - WORKSPACE_SETTINGS: 'WorkspaceGeneralSettings', + WORKSPACE_OVERVIEW: 'WorkspaceOverview', WORKSPACE_CARD: 'WorkspaceCorporateCards', WORKSPACE_REIMBURSE: 'WorkspaceReimburseReceipts', WORKSPACE_BILLS: 'WorkspacePayBills', @@ -3102,11 +3110,6 @@ const CONST = { CAROUSEL: 3, }, - BRICK_ROAD: { - GBR: 'GBR', - RBR: 'RBR', - }, - /** * Constants for types of violations. * Defined here because they need to be referenced by the type system to generate the @@ -3165,6 +3168,12 @@ const CONST = { MINI_CONTEXT_MENU_MAX_ITEMS: 4, + WORKSPACE_SWITCHER: { + NAME: 'Expensify', + SUBSCRIPT_ICON_SIZE: 8, + MINIMUM_WORKSPACES_TO_SHOW_SEARCH: 8, + }, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', } as const; diff --git a/src/Expensify.js b/src/Expensify.js index 12003968b28..dfb59a0f884 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -62,7 +62,8 @@ const propTypes = { /** Whether a new update is available and ready to install. */ updateAvailable: PropTypes.bool, - /** Tells us if the sidebar has rendered */ + /** Tells us if the sidebar has rendered - TODO: We don't use it as temporary solution to fix not hidding splashscreen */ + // eslint-disable-next-line react/no-unused-prop-types isSidebarLoaded: PropTypes.bool, /** Information about a screen share call requested by a GuidesPlus agent */ @@ -83,6 +84,9 @@ const propTypes = { /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */ focusModeNotification: PropTypes.bool, + /** Last visited path in the app */ + lastVisitedPath: PropTypes.string, + ...withLocalizePropTypes, }; @@ -97,6 +101,7 @@ const defaultProps = { isCheckingPublicRoom: true, updateRequired: false, focusModeNotification: false, + lastVisitedPath: undefined, }; const SplashScreenHiddenContext = React.createContext({}); @@ -107,6 +112,7 @@ function Expensify(props) { const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); const [isSplashHidden, setIsSplashHidden] = useState(false); const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false); + const [initialUrl, setInitialUrl] = useState(null); useEffect(() => { if (props.isCheckingPublicRoom) { @@ -125,7 +131,7 @@ function Expensify(props) { [isSplashHidden], ); - const shouldInit = isNavigationReady && (!isAuthenticated || props.isSidebarLoaded) && hasAttemptedToOpenPublicRoom; + const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom; const shouldHideSplash = shouldInit && !isSplashHidden; const initializeClient = () => { @@ -187,6 +193,7 @@ function Expensify(props) { // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report Linking.getInitialURL().then((url) => { + setInitialUrl(url); Report.openReportFromDeepLink(url, isAuthenticated); }); @@ -247,6 +254,8 @@ function Expensify(props) { )} @@ -286,6 +295,9 @@ export default compose( key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, initWithStoredValues: false, }, + lastVisitedPath: { + key: ONYXKEYS.LAST_VISITED_PATH, + }, }), )(Expensify); diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index c68a950d350..3bc9c5e57b9 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -4,6 +4,7 @@ * */ export default { CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator', + BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator', LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2867cb3905a..88b740e0e6c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -243,6 +243,9 @@ const ONYXKEYS = { // Max width supported for HTML element MAX_CANVAS_WIDTH: 'maxCanvasWidth', + // Stores last visited path + LAST_VISITED_PATH: 'lastVisitedPath', + // Stores the recently used report fields RECENTLY_USED_REPORT_FIELDS: 'recentlyUsedReportFields', @@ -447,6 +450,7 @@ type OnyxValues = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; + [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.UPDATE_REQUIRED]: boolean; @@ -484,8 +488,8 @@ type OnyxValues = { // Forms [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; - [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.WorkspaceSettingsForm; + [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: OnyxTypes.Form; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9c4375b84ab..a84dc9c8f9a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -12,7 +12,13 @@ function getUrlWithBackToParam(url: TUrl, backTo?: string): } const ROUTES = { - HOME: '', + // If the user opens this route, we'll redirect them to the path saved in the last visited path or to the home page if the last visited path is empty. + ROOT: '', + + // This route renders the list of reports. + HOME: 'home', + + ALL_SETTINGS: 'all-settings', // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', @@ -59,7 +65,7 @@ const ROUTES = { route: 'bank-account/:stepToOpen?', getRoute: (stepToOpen = '', policyID = '', backTo?: string) => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo), }, - + WORKSPACE_SWITCHER: 'workspace-switcher', SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', SETTINGS_SHARE_CODE: 'settings/shareCode', @@ -439,9 +445,17 @@ const ROUTES = { route: 'workspace/:policyID/invite-message', getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const, }, - WORKSPACE_SETTINGS: { - route: 'workspace/:policyID/settings', - getRoute: (policyID: string) => `workspace/${policyID}/settings` as const, + WORKSPACE_OVERVIEW: { + route: 'workspace/:policyID/overview', + getRoute: (policyID: string) => `workspace/${policyID}/overview` as const, + }, + WORKSPACE_OVERVIEW_CURRENCY: { + route: 'workspace/:policyID/overview/currency', + getRoute: (policyID: string) => `workspace/${policyID}/overview/currency` as const, + }, + WORKSPACE_OVERVIEW_NAME: { + route: 'workspace/:policyID/overview/name', + getRoute: (policyID: string) => `workspace/${policyID}/overview/name` as const, }, WORKSPACE_AVATAR: { route: 'workspace/:policyID/avatar', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2bf40caede5..96b284dbea2 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -12,6 +12,7 @@ const PROTECTED_SCREENS = { const SCREENS = { ...PROTECTED_SCREENS, + ALL_SETTINGS: 'AllSettings', REPORT: 'Report', PROFILE_AVATAR: 'ProfileAvatar', WORKSPACE_AVATAR: 'WorkspaceAvatar', @@ -20,6 +21,7 @@ const SCREENS = { TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps', VALIDATE_LOGIN: 'ValidateLogin', UNLINK_LOGIN: 'UnlinkLogin', + SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', SETTINGS: { ROOT: 'Settings_Root', SHARE_CODE: 'Settings_Share_Code', @@ -86,6 +88,10 @@ const SCREENS = { }, LEFT_MODAL: { SEARCH: 'Search', + WORKSPACE_SWITCHER: 'WorkspaceSwitcher', + }, + WORKSPACE_SWITCHER: { + ROOT: 'WorkspaceSwitcher_Root', }, RIGHT_MODAL: { SETTINGS: 'Settings', @@ -194,7 +200,7 @@ const SCREENS = { WORKSPACE: { INITIAL: 'Workspace_Initial', - SETTINGS: 'Workspace_Settings', + OVERVIEW: 'Workspace_Overview', CARD: 'Workspace_Card', REIMBURSE: 'Workspace_Reimburse', RATE_AND_UNIT: 'Workspace_RateAndUnit', @@ -204,7 +210,8 @@ const SCREENS = { MEMBERS: 'Workspace_Members', INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', - CURRENCY: 'Workspace_Settings_Currency', + CURRENCY: 'Workspace_Overview_Currency', + NAME: 'Workspace_Overview_Name', }, EDIT_REQUEST: { diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx new file mode 100644 index 00000000000..466f0f492c8 --- /dev/null +++ b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx @@ -0,0 +1,11 @@ +import {createContext} from 'react'; + +type ActiveWorkspaceContextType = { + activeWorkspaceID?: string; + setActiveWorkspaceID: (activeWorkspaceID?: string) => void; +}; + +const ActiveWorkspaceContext = createContext({activeWorkspaceID: undefined, setActiveWorkspaceID: () => undefined}); + +export default ActiveWorkspaceContext; +export {type ActiveWorkspaceContextType}; diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx b/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx new file mode 100644 index 00000000000..884b9a2a2d9 --- /dev/null +++ b/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx @@ -0,0 +1,19 @@ +import React, {useMemo, useState} from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import ActiveWorkspaceContext from './ActiveWorkspaceContext'; + +function ActiveWorkspaceContextProvider({children}: ChildrenProps) { + const [activeWorkspaceID, setActiveWorkspaceID] = useState(undefined); + + const value = useMemo( + () => ({ + activeWorkspaceID, + setActiveWorkspaceID, + }), + [activeWorkspaceID], + ); + + return {children}; +} + +export default ActiveWorkspaceContextProvider; diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 3ac2e3e3d72..dd6d41f4b22 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -341,6 +341,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose isVisible={isVisible} type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} onModalHide={resetState} + shouldUseCustomBackdrop > {}, onImageRemoved: () => {}, style: [], + disabledStyle: [], DefaultAvatar: () => {}, isUsingDefaultAvatar: false, size: CONST.AVATAR_SIZE.DEFAULT, @@ -118,6 +121,7 @@ const defaultProps = { headerTitle: '', previewSource: '', originalFileName: '', + disabled: false, onViewPhotoPress: undefined, anchorAlignment: { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, @@ -129,6 +133,7 @@ function AvatarWithImagePicker({ isFocused, DefaultAvatar, style, + disabledStyle, pendingAction, errors, errorRowStyles, @@ -142,14 +147,16 @@ function AvatarWithImagePicker({ originalFileName, isUsingDefaultAvatar, onImageRemoved, - anchorPosition, - anchorAlignment, onImageSelected, editorMaskImage, + avatarStyle, + disabled, onViewPhotoPress, }) { const theme = useTheme(); const styles = useThemeStyles(); + const {windowWidth} = useWindowDimensions(); + const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0}); const [isMenuVisible, setIsMenuVisible] = useState(false); const [errorData, setErrorData] = useState({ validationError: null, @@ -291,28 +298,50 @@ function AvatarWithImagePicker({ return menuItems; }; + useEffect(() => { + if (!anchorRef.current) { + return; + } + + if (!isMenuVisible) { + return; + } + + anchorRef.current.measureInWindow((x, y, width, height) => { + setPopoverPosition({ + horizontal: x + (width - variables.photoUploadPopoverWidth) / 2, + vertical: y + height + variables.spacing2, + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isMenuVisible, windowWidth]); + return ( - + - + setIsMenuVisible((prev) => !prev)} accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('avatarWithImagePicker.editImage')} - disabled={isAvatarCropModalOpen} + disabled={isAvatarCropModalOpen || disabled} + disabledStyle={disabledStyle} ref={anchorRef} > {source ? ( )} - - - + {!disabled && ( + + + + )} @@ -376,10 +407,10 @@ function AvatarWithImagePicker({ } }} menuItems={menuItems} - anchorPosition={anchorPosition} + anchorPosition={popoverPosition} + anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} withoutOverlay anchorRef={anchorRef} - anchorAlignment={anchorAlignment} /> ); }} diff --git a/src/components/BlockingViews/ForceFullScreenView/index.native.tsx b/src/components/BlockingViews/ForceFullScreenView/index.native.tsx new file mode 100644 index 00000000000..296e7c26a9b --- /dev/null +++ b/src/components/BlockingViews/ForceFullScreenView/index.native.tsx @@ -0,0 +1,9 @@ +import type ForceFullScreenViewProps from './types'; + +function ForceFullScreenView({children}: ForceFullScreenViewProps) { + return children; +} + +ForceFullScreenView.displayName = 'ForceFullScreenView'; + +export default ForceFullScreenView; diff --git a/src/components/BlockingViews/ForceFullScreenView/index.tsx b/src/components/BlockingViews/ForceFullScreenView/index.tsx new file mode 100644 index 00000000000..8a02028168f --- /dev/null +++ b/src/components/BlockingViews/ForceFullScreenView/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {View} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type ForceFullScreenViewProps from './types'; + +function ForceFullScreenView({children, shouldForceFullScreen = false}: ForceFullScreenViewProps) { + const styles = useThemeStyles(); + + if (shouldForceFullScreen) { + return {children}; + } + + return children; +} + +ForceFullScreenView.displayName = 'ForceFullScreenView'; + +export default ForceFullScreenView; diff --git a/src/components/BlockingViews/ForceFullScreenView/types.ts b/src/components/BlockingViews/ForceFullScreenView/types.ts new file mode 100644 index 00000000000..b03e6d5900d --- /dev/null +++ b/src/components/BlockingViews/ForceFullScreenView/types.ts @@ -0,0 +1,7 @@ +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type ForceFullScreenViewProps = ChildrenProps & { + shouldForceFullScreen?: boolean; +}; + +export default ForceFullScreenViewProps; diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 807029addf5..8cabf7dce49 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -9,6 +9,7 @@ import variables from '@styles/variables'; import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import BlockingView from './BlockingView'; +import ForceFullScreenView from './ForceFullScreenView'; type FullPageNotFoundViewProps = { /** Child elements */ @@ -37,6 +38,9 @@ type FullPageNotFoundViewProps = { /** Function to call when pressing the navigation link */ onLinkPress?: () => void; + + /** Whether we should force the full page view */ + shouldForceFullScreen?: boolean; }; // eslint-disable-next-line rulesdir/no-negated-variables @@ -50,13 +54,14 @@ function FullPageNotFoundView({ shouldShowLink = true, shouldShowBackButton = true, onLinkPress = () => Navigation.dismissModal(), + shouldForceFullScreen = false, }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); if (shouldShow) { return ( - <> + - + ); } diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index 4e9bd22e004..d292f80d135 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -38,6 +38,9 @@ type ContextMenuItemProps = { /** Whether the menu item is focused or not */ isFocused?: boolean; + + /** Whether the width should be limited */ + shouldLimitWidth?: boolean; }; type ContextMenuItemHandle = { @@ -45,7 +48,7 @@ type ContextMenuItemHandle = { }; function ContextMenuItem( - {onPress, successIcon, successText = '', icon, text, isMini = false, description = '', isAnonymousAction = false, isFocused = false}: ContextMenuItemProps, + {onPress, successIcon, successText = '', icon, text, isMini = false, description = '', isAnonymousAction = false, isFocused = false, shouldLimitWidth = true}: ContextMenuItemProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -94,7 +97,7 @@ function ContextMenuItem( success={!isThrottledButtonActive} description={description} descriptionTextStyle={styles.breakWord} - style={StyleUtils.getContextMenuItemStyles(windowWidth)} + style={shouldLimitWidth && StyleUtils.getContextMenuItemStyles(windowWidth)} isAnonymousAction={isAnonymousAction} focused={isFocused} interactive={isThrottledButtonActive} diff --git a/src/components/EnvironmentBadge.tsx b/src/components/EnvironmentBadge.tsx index ceb4acf1b9e..9a0854b815e 100644 --- a/src/components/EnvironmentBadge.tsx +++ b/src/components/EnvironmentBadge.tsx @@ -32,6 +32,7 @@ function EnvironmentBadge() { badgeStyles={[styles.alignSelfStart, styles.headerEnvBadge]} textStyles={[styles.headerEnvBadgeText, {fontWeight: '700'}]} environment={environment} + pressable /> ); } diff --git a/src/components/FeatureList.js b/src/components/FeatureList.js index 8e6a0d1f74d..53d0d8f7c38 100644 --- a/src/components/FeatureList.js +++ b/src/components/FeatureList.js @@ -4,53 +4,105 @@ import {View} from 'react-native'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import stylePropTypes from '@styles/stylePropTypes'; +import variables from '@styles/variables'; +import Button from './Button'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; -import Text from './Text'; +import Section from './Section'; const propTypes = { + /** The text to display in the title of the section */ + title: PropTypes.string.isRequired, + + /** The text to display in the subtitle of the section */ + subtitle: PropTypes.string, + + /** Text of the call to action button */ + ctaText: PropTypes.string, + + /** Accessibility label for the call to action button */ + ctaAccessibilityLabel: PropTypes.string, + + /** Action to call on cta button press */ + onCTAPress: PropTypes.func, + /** A list of menuItems representing the feature list. */ menuItems: PropTypes.arrayOf(PropTypes.shape({...menuItemPropTypes, translationKey: PropTypes.string})).isRequired, - /** A headline translation key to show above the feature list. */ - headline: PropTypes.string.isRequired, + /** The illustration to display in the header. Can be a JSON object representing a Lottie animation. */ + illustration: PropTypes.shape({ + file: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + w: PropTypes.number.isRequired, + h: PropTypes.number.isRequired, + }), + + /** The style passed to the illustration */ + illustrationStyle: stylePropTypes, - /** A description translation key to show below the headline and above the feature list. */ - description: PropTypes.string.isRequired, + /** The background color to apply in the upper half of the screen. */ + illustrationBackgroundColor: PropTypes.string, }; -function FeatureList({menuItems, headline, description}) { +const defaultProps = { + ctaText: '', + ctaAccessibilityLabel: '', + subtitle: '', + onCTAPress: () => {}, + illustration: null, + illustrationBackgroundColor: '', + illustrationStyle: [], +}; + +function FeatureList({title, subtitle, ctaText, ctaAccessibilityLabel, onCTAPress, menuItems, illustration, illustrationStyle, illustrationBackgroundColor}) { const styles = useThemeStyles(); const {translate} = useLocalize(); + return ( - <> - - - {translate(headline)} - - {translate(description)} - - {_.map(menuItems, ({translationKey, icon}) => ( - + + + {_.map(menuItems, ({translationKey, icon}) => ( + + + + ))} + +