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..f7e068686b3 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' && isScreenActive !== STATE_ON_TOP; + const { headerShown = true, diff --git a/src/CONST.ts b/src/CONST.ts index 072f780b54a..e55e3664a29 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1404,7 +1404,7 @@ const CONST = { GUIDES_CALL_TASK_IDS: { CONCIERGE_DM: 'NewExpensifyConciergeDM', WORKSPACE_INITIAL: 'WorkspaceHome', - WORKSPACE_SETTINGS: 'WorkspaceGeneralSettings', + WORKSPACE_OVERVIEW: 'WorkspaceGeneralSettings', WORKSPACE_CARD: 'WorkspaceCorporateCards', WORKSPACE_REIMBURSE: 'WorkspaceReimburseReceipts', WORKSPACE_BILLS: 'WorkspacePayBills', diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index a3a041e6568..4bc11c5e185 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -4,6 +4,7 @@ * */ export default { CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator', + BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 425ff73af56..1ea8bca94cf 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -14,6 +14,8 @@ function getUrlWithBackToParam(url: TUrl, backTo?: string): const ROUTES = { 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', FLAG_COMMENT: { @@ -420,13 +422,13 @@ 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_SETTINGS_CURRENCY: { - route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, + WORKSPACE_OVERVIEW_CURRENCY: { + route: 'workspace/:policyID/overview/currency', + getRoute: (policyID: string) => `workspace/${policyID}/overview/currency` as const, }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 921f5795348..86913760e78 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', NOT_FOUND: 'not-found', TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps', @@ -184,7 +185,7 @@ const SCREENS = { WORKSPACE: { INITIAL: 'Workspace_Initial', - SETTINGS: 'Workspace_Settings', + OVERVIEW: 'Workspace_Overview', CARD: 'Workspace_Card', REIMBURSE: 'Workspace_Reimburse', RATE_AND_UNIT: 'Workspace_RateAndUnit', @@ -194,7 +195,7 @@ const SCREENS = { MEMBERS: 'Workspace_Members', INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', - CURRENCY: 'Workspace_Settings_Currency', + CURRENCY: 'Workspace_Overview_Currency', }, EDIT_REQUEST: { diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 791eb150f8c..e0bbb2ddbe1 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; -import {Animated, Easing, View} from 'react-native'; +import {Animated, Easing} from 'react-native'; import compose from '@libs/compose'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -84,31 +84,29 @@ class FloatingActionButton extends PureComponent { return ( - - { - this.fabPressable = el; - if (this.props.buttonRef) { - this.props.buttonRef.current = el; - } - }} - accessibilityLabel={this.props.accessibilityLabel} - role={this.props.role} - pressDimmingValue={1} - onPress={(e) => { - // Drop focus to avoid blue focus ring. - this.fabPressable.blur(); - this.props.onPress(e); - }} - onLongPress={() => {}} - style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} - > - - - + { + this.fabPressable = el; + if (this.props.buttonRef) { + this.props.buttonRef.current = el; + } + }} + accessibilityLabel={this.props.accessibilityLabel} + role={this.props.role} + pressDimmingValue={1} + onPress={(e) => { + // Drop focus to avoid blue focus ring. + this.fabPressable.blur(); + this.props.onPress(e); + }} + onLongPress={() => {}} + style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} + > + + ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index c4a481cb71c..fe7629a2a97 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1455,6 +1455,7 @@ export default { invoices: 'Invoices', travel: 'Travel', members: 'Members', + overview: 'Overview', bankAccount: 'Bank account', connectBankAccount: 'Connect bank account', testTransactions: 'Test transactions', diff --git a/src/languages/es.ts b/src/languages/es.ts index a91a8768a3e..ee0a38d91c6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1478,6 +1478,7 @@ export default { invoices: 'Enviar facturas', travel: 'Viajes', members: 'Miembros', + overview: 'DescripciĆ³n', bankAccount: 'Cuenta bancaria', connectBankAccount: 'Conectar cuenta bancaria', testTransactions: 'Transacciones de prueba', diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 008015db7a9..4bad503bcdd 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -35,6 +35,7 @@ import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import createCustomStackNavigator from './createCustomStackNavigator'; import defaultScreenOptions from './defaultScreenOptions'; import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions'; +import BottomTabNavigator from './Navigators/BottomTabNavigator'; import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import RightModalNavigator from './Navigators/RightModalNavigator'; @@ -56,7 +57,6 @@ type AuthScreensProps = { }; const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default as React.ComponentType; -const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default as React.ComponentType; const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default as React.ComponentType; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default as React.ComponentType; const loadConciergePage = () => require('../../../pages/ConciergePage').default as React.ComponentType; @@ -255,9 +255,9 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom ({ [SCREENS.SETTINGS.ROOT]: () => require('../../../pages/settings/InitialSettingsPage').default as React.ComponentType, [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType, - [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../pages/workspace/WorkspacesListPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.PRONOUNS]: () => require('../../../pages/settings/Profile/PronounsPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.DISPLAY_NAME]: () => require('../../../pages/settings/Profile/DisplayNamePage').default as React.ComponentType, @@ -220,16 +219,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS]: () => require('../../../pages/settings/Profile/CustomStatus/StatusPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS_SET]: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default as React.ComponentType, - [SCREENS.WORKSPACE.INITIAL]: () => require('../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType, - [SCREENS.WORKSPACE.SETTINGS]: () => require('../../../pages/workspace/WorkspaceSettingsPage').default as React.ComponentType, - [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default as React.ComponentType, - [SCREENS.WORKSPACE.CARD]: () => require('../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, - [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType, [SCREENS.WORKSPACE.RATE_AND_UNIT]: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default as React.ComponentType, - [SCREENS.WORKSPACE.BILLS]: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType, - [SCREENS.WORKSPACE.INVOICES]: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType, - [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, - [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceOverviewCurrencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.INVITE]: () => require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType, [SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx new file mode 100644 index 00000000000..1792460b14d --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx @@ -0,0 +1,61 @@ +import {StackNavigationOptions} from '@react-navigation/stack'; +import React from 'react'; +import {Text, View} from 'react-native'; +import {PressableWithFeedback} from '@components/Pressable'; +import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator'; +import Navigation from '@libs/Navigation/Navigation'; +import {BottomTabNavigatorParamList} from '@libs/Navigation/types'; +import SidebarScreen from '@pages/home/sidebar/SidebarScreen'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType; + +const Tab = createCustomBottomTabNavigator(); + +// TODO-IDEAL replace with the actuall screen. +function SecondTab() { + return ( + + Expensify settings + + { + Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); + }} + > + Workspaces + + + ); +} + +const screenOptions: StackNavigationOptions = { + headerShown: false, +}; + +function BottomTabNavigator() { + return ( + + + + + + ); +} + +BottomTabNavigator.displayName = 'BottomTabNavigator'; + +export default BottomTabNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 228ea6bd3dc..aa4e78137cd 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -11,23 +11,44 @@ const Stack = createStackNavigator(); const url = getCurrentUrl(); const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined; +type Screens = Partial React.ComponentType>>; + +const workspaceSettingsScreens = { + [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType, + [SCREENS.WORKSPACE.OVERVIEW]: () => require('../../../../../pages/workspace/WorkspaceOverviewPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, + [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType, + [SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, +} satisfies Screens; + function BaseCentralPaneNavigator() { const styles = useThemeStyles(); + const options = { + headerShown: false, + title: 'New Expensify', + + // Prevent unnecessary scrolling + cardStyle: styles.cardStyleNavigator, + }; return ( - + + + {Object.entries(workspaceSettingsScreens).map(([screenName, componentGetter]) => ( + + ))} ); } diff --git a/src/libs/Navigation/AppNavigator/Navigators/PublicBottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/PublicBottomTabNavigator.tsx new file mode 100644 index 00000000000..ddf3443e9c0 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/PublicBottomTabNavigator.tsx @@ -0,0 +1,29 @@ +import {StackNavigationOptions} from '@react-navigation/stack'; +import React from 'react'; +import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator'; +import {BottomTabNavigatorParamList} from '@libs/Navigation/types'; +import SignInPage from '@pages/signin/SignInPage'; +import SCREENS from '@src/SCREENS'; + +// This type is not exactly right because we are using the same route in public and auth screens. +const Tab = createCustomBottomTabNavigator(); + +const screenOptions: StackNavigationOptions = { + headerShown: false, +}; + +// The structure for the HOME route have to be the same in public and auth screens. That's why we need to wrap the HOME screen with "fake" bottomTabNavigator. +function PublicBottomTabNavigator() { + return ( + + + + ); +} + +PublicBottomTabNavigator.displayName = 'BottomTabNavigator'; + +export default PublicBottomTabNavigator; diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx index 5c3171214bd..0324cc85dcf 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx +++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx @@ -8,6 +8,7 @@ import SAMLSignInPage from '@pages/signin/SAMLSignInPage'; import SignInPage from '@pages/signin/SignInPage'; import UnlinkLoginPage from '@pages/UnlinkLoginPage'; import ValidateLoginPage from '@pages/ValidateLoginPage'; +import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import defaultScreenOptions from './defaultScreenOptions'; @@ -16,8 +17,9 @@ const RootStack = createStackNavigator(); function PublicScreens() { return ( + {/* The structure for the HOME route have to be the same in public and auth screens. That's why we need to wrap the HOME screen with "fake" bottomTabNavigator. */} diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx new file mode 100644 index 00000000000..3b58e110f0f --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -0,0 +1,57 @@ +import {useNavigationState} from '@react-navigation/native'; +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithFeedback} from '@components/Pressable'; +import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; +import Navigation from '@libs/Navigation/Navigation'; +import {RootStackParamList} from '@libs/Navigation/types'; +import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +function BottomTabBar() { + const theme = useTheme(); + const styles = useThemeStyles(); + + // Parent navigator of the bottom tab bar is the root navigator. + const currentTabName = useNavigationState((state) => getTopmostBottomTabRoute(state).name); + + return ( + + { + Navigation.navigate(ROUTES.HOME); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel="Chats" + > + + + + { + Navigation.navigate(ROUTES.ALL_SETTINGS); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel="Settings" + > + + + + ); +} + +BottomTabBar.displayName = 'BottomTabBar'; + +export default BottomTabBar; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx new file mode 100644 index 00000000000..24be4ce5a17 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx @@ -0,0 +1,88 @@ +import { + createNavigatorFactory, + DefaultNavigatorOptions, + ParamListBase, + StackActionHelpers, + StackNavigationState, + StackRouter, + StackRouterOptions, + useNavigationBuilder, +} from '@react-navigation/native'; +import {StackNavigationEventMap, StackNavigationOptions, StackView} from '@react-navigation/stack'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {View} from 'react-native'; +import {NavigationStateRoute} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; +import BottomTabBar from './BottomTabBar'; + +type CustomNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & { + initialRouteName: string; +}; + +const propTypes = { + /* Children for the useNavigationBuilder hook */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /* initialRouteName for this navigator */ + initialRouteName: PropTypes.oneOf([PropTypes.string, undefined]), + + /* Screen options defined for this navigator */ + // eslint-disable-next-line react/forbid-prop-types + screenOptions: PropTypes.object, +}; + +const defaultProps = { + initialRouteName: undefined, + screenOptions: undefined, +}; + +function getStateToRender(state: StackNavigationState): StackNavigationState { + const routesToRender = [state.routes.at(-1)] as NavigationStateRoute[]; + // We need to render at least one HOME screen to make sure everything load properly. + if (routesToRender[0].name !== SCREENS.HOME) { + const routeToRender = state.routes.find((route) => route.name === SCREENS.HOME); + if (routeToRender) { + routesToRender.unshift(routeToRender); + } + } + + return {...state, routes: routesToRender, index: routesToRender.length - 1}; +} + +function CustomBottomTabNavigator({initialRouteName, children, screenOptions, ...props}: CustomNavigatorProps) { + const {state, navigation, descriptors, NavigationContent} = useNavigationBuilder< + StackNavigationState, + StackRouterOptions, + StackActionHelpers, + StackNavigationOptions, + StackNavigationEventMap + >(StackRouter, { + children, + screenOptions, + initialRouteName, + }); + + const stateToRender = getStateToRender(state); + + return ( + + + + + + + ); +} + +CustomBottomTabNavigator.defaultProps = defaultProps; +CustomBottomTabNavigator.propTypes = propTypes; +CustomBottomTabNavigator.displayName = 'CustomBottomTabNavigator'; + +export default createNavigatorFactory(CustomBottomTabNavigator); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts index 435ebc00362..73e26962e58 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts @@ -1,61 +1,44 @@ -import {NavigationState, PartialState, RouterConfigOptions, StackNavigationState, StackRouter} from '@react-navigation/native'; +import {RouterConfigOptions, StackNavigationState, StackRouter} from '@react-navigation/native'; import {ParamListBase} from '@react-navigation/routers'; import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; +import getMatchingCentralPaneRouteForState from '@libs/Navigation/getMatchingCentralPaneRouteForState'; +import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; +import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; +import TAB_TO_CENTRAL_PANE_MAPPING from '@libs/Navigation/TAB_TO_CENTRAL_PANE_MAPPING'; +import {RootStackParamList, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; import type {ResponsiveStackNavigatorRouterOptions} from './types'; -type State = NavigationState | PartialState; - -const isAtLeastOneCentralPaneNavigatorInState = (state: State): boolean => !!state.routes.find((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); - -const getTopMostReportIDFromRHP = (state: State): string => { - if (!state) { - return ''; - } - - const topmostRightPane = state.routes.filter((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR).at(-1); - - if (topmostRightPane?.state) { - return getTopMostReportIDFromRHP(topmostRightPane.state); - } - - const topmostRoute = state.routes.at(-1); - - if (topmostRoute?.state) { - return getTopMostReportIDFromRHP(topmostRoute.state); - } - - if (topmostRoute?.params && 'reportID' in topmostRoute.params && typeof topmostRoute.params.reportID === 'string' && topmostRoute.params.reportID) { - return topmostRoute.params.reportID; - } - - return ''; -}; /** * Adds report route without any specific reportID to the state. * The report screen will self set proper reportID param based on the helper function findLastAccessedReport (look at ReportScreenWrapper for more info) * * @param state - react-navigation state */ -const addCentralPaneNavigatorRoute = (state: State) => { - const reportID = getTopMostReportIDFromRHP(state); +const addCentralPaneNavigatorRoute = (state: State) => { + const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(state); + + const bottomTabRoute = state.routes.filter((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); + const centralPaneRoutes = state.routes.filter((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); + + // TODO-IDEAL Both RHP and LHP add condition for the LHP + const modalRoutes = state.routes.filter((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + const centralPaneNavigatorRoute = { name: NAVIGATORS.CENTRAL_PANE_NAVIGATOR, - state: { - routes: [ - { - name: SCREENS.REPORT, - params: { - reportID, - }, - }, - ], + params: { + screen: matchingCentralPaneRoute.name, + params: matchingCentralPaneRoute.params, }, }; - state.routes.splice(1, 0, centralPaneNavigatorRoute); - // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style - (state.index as number) = state.routes.length - 1; + + // @ts-expect-error Updating read only property + // noinspection JSConstantReassignment + state.routes = [...bottomTabRoute, ...centralPaneRoutes, centralPaneNavigatorRoute, ...modalRoutes]; // eslint-disable-line + + // @ts-expect-error Updating read only property + // noinspection JSConstantReassignment + state.index = state.routes.length - 1; // eslint-disable-line }; function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { @@ -63,14 +46,19 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { return { ...stackRouter, - getRehydratedState(partialState: StackNavigationState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState { + getRehydratedState(partialState: StackNavigationState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState { const isSmallScreenWidth = getIsSmallScreenWidth(); // Make sure that there is at least one CentralPaneNavigator (ReportScreen by default) in the state if this is a wide layout - if (!isAtLeastOneCentralPaneNavigatorInState(partialState) && !isSmallScreenWidth) { - // If we added a route we need to make sure that the state.stale is true to generate new key for this route + const topmostCentralPaneRoute = getTopmostCentralPaneRoute(partialState); + const topmostBottomTabRoute = getTopmostBottomTabRoute(partialState); - // eslint-disable-next-line no-param-reassign - (partialState.stale as boolean) = true; + const isBottomTabMatchingCentralPane = topmostCentralPaneRoute && TAB_TO_CENTRAL_PANE_MAPPING[topmostBottomTabRoute.name].includes(topmostCentralPaneRoute.name); + + if (!isSmallScreenWidth && !isBottomTabMatchingCentralPane) { + // If we added a route we need to make sure that the state.stale is true to generate new key for this route + // @ts-expect-error Updating read only property + // noinspection JSConstantReassignment + partialState.stale = true; // eslint-disable-line addCentralPaneNavigatorRoute(partialState); } const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx index dd2e548064c..9a882d79c6c 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx @@ -1,7 +1,8 @@ import {createNavigatorFactory, ParamListBase, StackActionHelpers, StackNavigationState, useNavigationBuilder} from '@react-navigation/native'; import {StackNavigationEventMap, StackNavigationOptions, StackView} from '@react-navigation/stack'; -import React, {useMemo, useRef} from 'react'; +import React, {useEffect, useMemo} from 'react'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import navigationRef from '@libs/Navigation/navigationRef'; import NAVIGATORS from '@src/NAVIGATORS'; import CustomRouter from './CustomRouter'; import type {ResponsiveStackNavigatorProps, ResponsiveStackNavigatorRouterOptions} from './types'; @@ -30,10 +31,6 @@ function reduceReportRoutes(routes: Routes): Routes { function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) { const {isSmallScreenWidth} = useWindowDimensions(); - const isSmallScreenWidthRef = useRef(isSmallScreenWidth); - - isSmallScreenWidthRef.current = isSmallScreenWidth; - const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder< StackNavigationState, ResponsiveStackNavigatorRouterOptions, @@ -46,6 +43,13 @@ function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) { initialRouteName: props.initialRouteName, }); + useEffect(() => { + if (!navigationRef.isReady()) { + return; + } + navigationRef.resetRoot(navigationRef.getRootState()); + }, [isSmallScreenWidth]); + const stateToRender = useMemo(() => { const result = reduceReportRoutes(state.routes); diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index c2dd3e76e7a..7c1d2343159 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,5 +1,5 @@ import {findFocusedRoute, getActionFromState} from '@react-navigation/core'; -import {CommonActions, EventMapCore, getPathFromState, NavigationState, PartialState, StackActions} from '@react-navigation/native'; +import {CommonActions, EventMapCore, getPathFromState, NavigationState, StackActions} from '@react-navigation/native'; import findLastIndex from 'lodash/findLastIndex'; import Log from '@libs/Log'; import CONST from '@src/CONST'; @@ -12,7 +12,7 @@ import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; -import {StackNavigationAction, StateOrRoute} from './types'; +import {StackNavigationAction, State, StateOrRoute} from './types'; let resolveNavigationIsReadyPromise: () => void; const navigationIsReadyPromise = new Promise((resolve) => { @@ -286,7 +286,7 @@ function setIsNavigationReady() { * * @param state - react-navigation state object */ -function navContainsProtectedRoutes(state: NavigationState | PartialState | undefined): boolean { +function navContainsProtectedRoutes(state: State | undefined): boolean { if (!state?.routeNames || !Array.isArray(state.routeNames)) { return false; } diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 7c0b9ef4fc8..564daa5787a 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -71,14 +71,6 @@ function NavigationRoot({authenticated, onReady}: NavigationRootProps) { Navigation.setShouldPopAllStateOnUP(); }, [isSmallScreenWidth]); - useEffect(() => { - if (!navigationRef.isReady() || !authenticated) { - return; - } - // We need to force state rehydration so the CustomRouter can add the CentralPaneNavigator route if necessary. - navigationRef.resetRoot(navigationRef.getRootState()); - }, [isSmallScreenWidth, authenticated]); - const handleStateChange = (state: NavigationState | undefined) => { if (!state) { return; diff --git a/src/libs/Navigation/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/TAB_TO_CENTRAL_PANE_MAPPING.ts new file mode 100755 index 00000000000..458b592d939 --- /dev/null +++ b/src/libs/Navigation/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -0,0 +1,18 @@ +import SCREENS from '@src/SCREENS'; +import {BottomTabName, CentralPaneName} from './types'; + +const TAB_TO_CENTRAL_PANE_MAPPING: Record = { + [SCREENS.HOME]: [SCREENS.REPORT], + [SCREENS.ALL_SETTINGS]: [SCREENS.SETTINGS.WORKSPACES], + [SCREENS.WORKSPACE.INITIAL]: [ + SCREENS.WORKSPACE.OVERVIEW, + SCREENS.WORKSPACE.CARD, + SCREENS.WORKSPACE.REIMBURSE, + SCREENS.WORKSPACE.BILLS, + SCREENS.WORKSPACE.INVOICES, + SCREENS.WORKSPACE.TRAVEL, + SCREENS.WORKSPACE.MEMBERS, + ], +}; + +export default TAB_TO_CENTRAL_PANE_MAPPING; diff --git a/src/libs/Navigation/getMatchingBottomTabRouteForState.ts b/src/libs/Navigation/getMatchingBottomTabRouteForState.ts new file mode 100644 index 00000000000..ac4f225fd89 --- /dev/null +++ b/src/libs/Navigation/getMatchingBottomTabRouteForState.ts @@ -0,0 +1,28 @@ +// import CONST from '@src/CONST'; +import SCREENS from '@src/SCREENS'; +import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; +import TAB_TO_CENTRAL_PANE_MAPPING from './TAB_TO_CENTRAL_PANE_MAPPING'; +import {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from './types'; + +// Get the route that matches the topmost central pane route in the navigation stack. e.g REPORT -> HOME +function getMatchingBottomTabRouteForState(state: State): NavigationPartialRoute { + const defaultRoute = {name: SCREENS.HOME}; + const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state); + + if (topmostCentralPaneRoute === undefined) { + return defaultRoute; + } + + for (const [tabName, centralPaneNames] of Object.entries(TAB_TO_CENTRAL_PANE_MAPPING)) { + if (centralPaneNames.includes(topmostCentralPaneRoute.name)) { + if (tabName === SCREENS.WORKSPACE.INITIAL) { + return {name: tabName, params: topmostCentralPaneRoute.params}; + } + return {name: tabName as BottomTabName}; + } + } + + return defaultRoute; +} + +export default getMatchingBottomTabRouteForState; diff --git a/src/libs/Navigation/getMatchingCentralPaneRouteForState.ts b/src/libs/Navigation/getMatchingCentralPaneRouteForState.ts new file mode 100644 index 00000000000..f00107308b2 --- /dev/null +++ b/src/libs/Navigation/getMatchingCentralPaneRouteForState.ts @@ -0,0 +1,51 @@ +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import getTopmostBottomTabRoute from './getTopmostBottomTabRoute'; +import TAB_TO_CENTRAL_PANE_MAPPING from './TAB_TO_CENTRAL_PANE_MAPPING'; +import {CentralPaneName, NavigationPartialRoute, RootStackParamList, State} from './types'; + +/** + * @param state - react-navigation state + */ +const getTopMostReportIDFromRHP = (state: State): string => { + if (!state) { + return ''; + } + + const topmostRightPane = state.routes.filter((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR).at(-1); + + if (topmostRightPane?.state) { + return getTopMostReportIDFromRHP(topmostRightPane.state); + } + + const topmostRoute = state.routes.at(-1); + + if (topmostRoute?.state) { + return getTopMostReportIDFromRHP(topmostRoute.state); + } + + if (topmostRoute?.params && 'reportID' in topmostRoute.params && typeof topmostRoute.params.reportID === 'string') { + return topmostRoute.params.reportID; + } + + return ''; +}; + +// Get matching central pane route for bottom tab navigator. e.g HOME -> REPORT +function getMatchingCentralPaneRouteForState(state: State): NavigationPartialRoute { + const topmostBottomTabRoute = getTopmostBottomTabRoute(state); + + const centralPaneName = TAB_TO_CENTRAL_PANE_MAPPING[topmostBottomTabRoute.name][0]; + + if (topmostBottomTabRoute.name === SCREENS.WORKSPACE.INITIAL) { + return {name: centralPaneName, params: topmostBottomTabRoute.params}; + } + + if (topmostBottomTabRoute.name === SCREENS.HOME) { + return {name: centralPaneName, params: {reportID: getTopMostReportIDFromRHP(state)}}; + } + + return {name: centralPaneName}; +} + +export default getMatchingCentralPaneRouteForState; diff --git a/src/libs/Navigation/getTopmostBottomTabRoute.ts b/src/libs/Navigation/getTopmostBottomTabRoute.ts new file mode 100644 index 00000000000..053f866bc3b --- /dev/null +++ b/src/libs/Navigation/getTopmostBottomTabRoute.ts @@ -0,0 +1,19 @@ +import {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from './types'; + +function getTopmostBottomTabRoute(state: State): NavigationPartialRoute { + const bottomTabNavigatorRoute = state.routes[0]; + + if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.name !== 'BottomTabNavigator' || bottomTabNavigatorRoute.state === undefined) { + throw new Error('There is no bottomTabNavigator route mounted as the first route in the root state.'); + } + + const topmostBottomTabRoute = bottomTabNavigatorRoute.state.routes.at(-1); + + if (!topmostBottomTabRoute) { + throw new Error('BottomTabNavigator route have no routes.'); + } + + return {name: topmostBottomTabRoute.name as BottomTabName, params: topmostBottomTabRoute.params}; +} + +export default getTopmostBottomTabRoute; diff --git a/src/libs/Navigation/getTopmostCentralPaneRoute.ts b/src/libs/Navigation/getTopmostCentralPaneRoute.ts new file mode 100644 index 00000000000..a9935adf6fb --- /dev/null +++ b/src/libs/Navigation/getTopmostCentralPaneRoute.ts @@ -0,0 +1,30 @@ +import NAVIGATORS from '@src/NAVIGATORS'; +import {CentralPaneName, NavigationPartialRoute, RootStackParamList, State} from './types'; + +// Get the name of topmost central pane route in the navigation stack. +function getTopmostCentralPaneRoute(state: State): NavigationPartialRoute | undefined { + if (!state) { + return; + } + + const topmostCentralPane = state.routes.filter((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR).at(-1); + + if (!topmostCentralPane) { + return; + } + + if (!!topmostCentralPane.params && 'screen' in topmostCentralPane.params) { + return {name: topmostCentralPane.params.screen as CentralPaneName, params: topmostCentralPane.params.params}; + } + + if (!topmostCentralPane.state) { + return; + } + + // There will be at least one route in the central pane navigator. + const {name, params} = topmostCentralPane.state.routes.at(-1) as NavigationPartialRoute; + + return {name, params}; +} + +export default getTopmostCentralPaneRoute; diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts index 8be8dd1ecfa..550b5ddb7d6 100644 --- a/src/libs/Navigation/linkTo.ts +++ b/src/libs/Navigation/linkTo.ts @@ -1,13 +1,19 @@ -import {getActionFromState} from '@react-navigation/core'; -import {NavigationAction, NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; +import {getActionFromState, PartialState} from '@react-navigation/core'; +import {NavigationAction, NavigationContainerRef, NavigationState} from '@react-navigation/native'; import {Writable} from 'type-fest'; +import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import {Route} from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForState'; +import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteForState'; import getStateFromPath from './getStateFromPath'; +import getTopmostBottomTabRoute from './getTopmostBottomTabRoute'; +import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; import getTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; -import {NavigationRoot, RootStackParamList, StackNavigationAction} from './types'; +import {NavigationRoot, RootStackParamList, StackNavigationAction, State} from './types'; type ActionPayloadParams = { screen?: string; @@ -28,7 +34,7 @@ type ActionPayload = { */ function getMinimalAction(action: NavigationAction, state: NavigationState): Writable { let currentAction: NavigationAction = action; - let currentState: NavigationState | PartialState | undefined = state; + let currentState: State | undefined = state; let currentTargetKey: string | undefined; while (currentAction.payload && 'name' in currentAction.payload && currentState?.routes[currentState.index ?? -1].name === currentAction.payload.name) { @@ -55,6 +61,27 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Wri return currentAction; } +// Because we need to change the type to push, we also need to set target for this action to the bottom tab navigator. +function getActionForBottomTabNavigator(action: StackNavigationAction, state: NavigationState): Writable | undefined { + const bottomTabNavigatorRoute = state.routes.at(0); + + if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.state === undefined || !action || action.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { + return; + } + + const params = action.payload.params as ActionPayloadParams; + const screen = params.screen; + + return { + type: CONST.NAVIGATION.ACTION_TYPE.PUSH, + payload: { + name: screen, + params: params.params, + }, + target: bottomTabNavigatorRoute.state.key, + }; +} + export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string, isActiveRoute?: boolean) { if (!navigation) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); @@ -69,18 +96,33 @@ export default function linkTo(navigation: NavigationContainerRef; + const stateFromPath = getStateFromPath(path) as PartialState>; + const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config); // If action type is different than NAVIGATE we can't change it to the PUSH safely if (action?.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { + const topmostCentralPaneRoute = getTopmostCentralPaneRoute(rootState); // In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack if (type === CONST.NAVIGATION.TYPE.FORCED_UP) { action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack - } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(rootState) !== getTopmostReportId(state)) { + } else if ( + action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && + topmostCentralPaneRoute && + (topmostCentralPaneRoute.name !== SCREENS.REPORT || getTopmostReportId(rootState) !== getTopmostReportId(stateFromPath)) + ) { + // We need to push a tab if the tab doesn't match the central pane route that we are going to push. + const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState); + const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateFromPath); + if (topmostBottomTabRoute.name !== matchingBottomTabRoute.name) { + root.dispatch({ + type: CONST.NAVIGATION.ACTION_TYPE.PUSH, + payload: matchingBottomTabRoute, + }); + } + action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow @@ -91,6 +133,36 @@ export default function linkTo(navigation: NavigationContainerRef>) { + // If the bottom tab navigator state is defined we don't need to do anything. + const isBottomTabNavigatorStateDefined = state.routes.at(0)?.state !== undefined; + if (isBottomTabNavigatorStateDefined) { + return state; + } + + // If not, we need to insert the tab that matches the currently generated state. + const matchingBottomTabRoute = getMatchingBottomTabRouteForState(state); + + // We need to have at least one HOME route in the state, otherwise the app won't load. + const routesForBottomTabNavigator: Array> = [{name: SCREENS.HOME}]; + + if (matchingBottomTabRoute.name !== SCREENS.HOME) { + // If the generated state requires tab other than HOME, we need to insert it. + routesForBottomTabNavigator.push(matchingBottomTabRoute); + } + + const stateWithTab = {...state}; + + // The first route in root stack is always the BOTTOM_TAB_NAVIGATOR + stateWithTab.routes[0] = {name: NAVIGATORS.BOTTOM_TAB_NAVIGATOR, state: {routes: routesForBottomTabNavigator}}; + + return stateWithTab; +} const linkingConfig: LinkingOptions = { + getStateFromPath: (path, options) => { + const state = getStateFromPath(path, options); + + if (state === undefined) { + throw new Error('Unable to parse path'); + } + const stateWithTab = getStateWithProperTab(state as PartialState>); + return stateWithTab; + }, prefixes: [ 'app://-/', 'new-expensify://', @@ -17,7 +53,7 @@ const linkingConfig: LinkingOptions = { CONST.STAGING_NEW_EXPENSIFY_URL, ], config: { - initialRouteName: SCREENS.HOME, + initialRouteName: NAVIGATORS.BOTTOM_TAB_NAVIGATOR, screens: { // Main Routes [SCREENS.VALIDATE_LOGIN]: ROUTES.VALIDATE_LOGIN, @@ -34,13 +70,43 @@ const linkingConfig: LinkingOptions = { [CONST.DEMO_PAGES.MONEY2020]: ROUTES.MONEY2020, // Sidebar - [SCREENS.HOME]: { - path: ROUTES.HOME, + [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: { + path: '', + initialRouteName: SCREENS.HOME, + screens: { + [SCREENS.HOME]: ROUTES.HOME, + [SCREENS.ALL_SETTINGS]: ROUTES.ALL_SETTINGS, + [SCREENS.WORKSPACE.INITIAL]: { + path: ROUTES.WORKSPACE_INITIAL.route, + exact: true, + }, + }, }, [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: { screens: { [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, + + [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, + [SCREENS.WORKSPACE.OVERVIEW]: ROUTES.WORKSPACE_OVERVIEW.route, + [SCREENS.WORKSPACE.CARD]: { + path: ROUTES.WORKSPACE_CARD.route, + }, + [SCREENS.WORKSPACE.REIMBURSE]: { + path: ROUTES.WORKSPACE_REIMBURSE.route, + }, + [SCREENS.WORKSPACE.BILLS]: { + path: ROUTES.WORKSPACE_BILLS.route, + }, + [SCREENS.WORKSPACE.INVOICES]: { + path: ROUTES.WORKSPACE_INVOICES.route, + }, + [SCREENS.WORKSPACE.TRAVEL]: { + path: ROUTES.WORKSPACE_TRAVEL.route, + }, + [SCREENS.WORKSPACE.MEMBERS]: { + path: ROUTES.WORKSPACE_MEMBERS.route, + }, }, }, [SCREENS.NOT_FOUND]: '*', @@ -52,10 +118,6 @@ const linkingConfig: LinkingOptions = { [SCREENS.SETTINGS.ROOT]: { path: ROUTES.SETTINGS, }, - [SCREENS.SETTINGS.WORKSPACES]: { - path: ROUTES.SETTINGS_WORKSPACES, - exact: true, - }, [SCREENS.SETTINGS.PREFERENCES.ROOT]: { path: ROUTES.SETTINGS_PREFERENCES, exact: true, @@ -218,36 +280,12 @@ const linkingConfig: LinkingOptions = { path: ROUTES.SETTINGS_STATUS_SET, exact: true, }, - [SCREENS.WORKSPACE.INITIAL]: { - path: ROUTES.WORKSPACE_INITIAL.route, - }, - [SCREENS.WORKSPACE.SETTINGS]: { - path: ROUTES.WORKSPACE_SETTINGS.route, - }, [SCREENS.WORKSPACE.CURRENCY]: { - path: ROUTES.WORKSPACE_SETTINGS_CURRENCY.route, - }, - [SCREENS.WORKSPACE.CARD]: { - path: ROUTES.WORKSPACE_CARD.route, - }, - [SCREENS.WORKSPACE.REIMBURSE]: { - path: ROUTES.WORKSPACE_REIMBURSE.route, + path: ROUTES.WORKSPACE_OVERVIEW_CURRENCY.route, }, [SCREENS.WORKSPACE.RATE_AND_UNIT]: { path: ROUTES.WORKSPACE_RATE_AND_UNIT.route, }, - [SCREENS.WORKSPACE.BILLS]: { - path: ROUTES.WORKSPACE_BILLS.route, - }, - [SCREENS.WORKSPACE.INVOICES]: { - path: ROUTES.WORKSPACE_INVOICES.route, - }, - [SCREENS.WORKSPACE.TRAVEL]: { - path: ROUTES.WORKSPACE_TRAVEL.route, - }, - [SCREENS.WORKSPACE.MEMBERS]: { - path: ROUTES.WORKSPACE_MEMBERS.route, - }, [SCREENS.WORKSPACE.INVITE]: { path: ROUTES.WORKSPACE_INVITE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b69552f6fe0..f9e85317959 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1,5 +1,15 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {CommonActions, NavigationContainerRefWithCurrent, NavigationHelpers, NavigationState, NavigatorScreenParams, PartialRoute, Route} from '@react-navigation/native'; +import { + CommonActions, + NavigationContainerRefWithCurrent, + NavigationHelpers, + NavigationState, + NavigatorScreenParams, + ParamListBase, + PartialRoute, + PartialState, + Route, +} from '@react-navigation/native'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -30,8 +40,9 @@ type ActionNavigate = { type StackNavigationAction = GoBackAction | ResetAction | SetParamsAction | ActionNavigate | undefined; type NavigationStateRoute = NavigationState['routes'][number]; -type NavigationPartialRoute = PartialRoute>; +type NavigationPartialRoute = PartialRoute>; type StateOrRoute = NavigationState | NavigationStateRoute | NavigationPartialRoute; +type State = NavigationState | PartialState>; type CentralPaneNavigatorParamList = { [SCREENS.REPORT]: { @@ -39,6 +50,33 @@ type CentralPaneNavigatorParamList = { reportID: string; openOnAdminRoom?: boolean; }; + + [SCREENS.SETTINGS.WORKSPACES]: undefined; + [SCREENS.WORKSPACE.OVERVIEW]: { + policyID: string; + }; + [SCREENS.WORKSPACE.CARD]: { + policyID: string; + }; + [SCREENS.WORKSPACE.REIMBURSE]: { + policyID: string; + }; + [SCREENS.WORKSPACE.BILLS]: { + policyID: string; + }; + [SCREENS.WORKSPACE.INVOICES]: { + policyID: string; + }; + [SCREENS.WORKSPACE.TRAVEL]: { + policyID: string; + }; + [SCREENS.WORKSPACE.MEMBERS]: { + policyID: string; + }; + [SCREENS.REIMBURSEMENT_ACCOUNT]: { + stepToOpen: string; + policyID: string; + }; }; type SettingsNavigatorParamList = { @@ -83,38 +121,14 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_SET]: undefined; - [SCREENS.WORKSPACE.INITIAL]: undefined; - [SCREENS.WORKSPACE.SETTINGS]: undefined; [SCREENS.WORKSPACE.CURRENCY]: undefined; - [SCREENS.WORKSPACE.CARD]: { - policyID: string; - }; - [SCREENS.WORKSPACE.REIMBURSE]: { - policyID: string; - }; [SCREENS.WORKSPACE.RATE_AND_UNIT]: undefined; - [SCREENS.WORKSPACE.BILLS]: { - policyID: string; - }; - [SCREENS.WORKSPACE.INVOICES]: { - policyID: string; - }; - [SCREENS.WORKSPACE.TRAVEL]: { - policyID: string; - }; - [SCREENS.WORKSPACE.MEMBERS]: { - policyID: string; - }; [SCREENS.WORKSPACE.INVITE]: { policyID: string; }; [SCREENS.WORKSPACE.INVITE_MESSAGE]: { policyID: string; }; - [SCREENS.REIMBURSEMENT_ACCOUNT]: { - stepToOpen: string; - policyID: string; - }; [SCREENS.GET_ASSISTANCE]: { taskID: string; }; @@ -359,8 +373,14 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: NavigatorScreenParams; }; -type PublicScreensParamList = { +type BottomTabNavigatorParamList = { [SCREENS.HOME]: undefined; + [SCREENS.ALL_SETTINGS]: undefined; + [SCREENS.WORKSPACE.INITIAL]: undefined; +}; + +type PublicScreensParamList = { + [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: NavigatorScreenParams; [SCREENS.TRANSITION_BETWEEN_APPS]: { shouldForceLogin: string; email: string; @@ -381,7 +401,7 @@ type PublicScreensParamList = { }; type AuthScreensParamList = { - [SCREENS.HOME]: undefined; + [NAVIGATORS.BOTTOM_TAB_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: NavigatorScreenParams; [SCREENS.VALIDATE_LOGIN]: { accountID: string; @@ -412,15 +432,23 @@ type AuthScreensParamList = { type RootStackParamList = PublicScreensParamList & AuthScreensParamList; +type BottomTabName = keyof BottomTabNavigatorParamList; + +type CentralPaneName = keyof CentralPaneNavigatorParamList; + export type { NavigationRef, StackNavigationAction, CentralPaneNavigatorParamList, + BottomTabName, + CentralPaneName, RootStackParamList, StateOrRoute, NavigationStateRoute, + NavigationPartialRoute, NavigationRoot, AuthScreensParamList, + BottomTabNavigatorParamList, RightModalNavigatorParamList, PublicScreensParamList, MoneyRequestNavigatorParamList, @@ -448,4 +476,5 @@ export type { SignInNavigatorParamList, ReferralDetailsNavigatorParamList, ReimbursementAccountNavigatorParamList, + State, }; diff --git a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.native.tsx b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.native.tsx new file mode 100644 index 00000000000..74180256cc7 --- /dev/null +++ b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.native.tsx @@ -0,0 +1,3 @@ +import FloatingActionButton from '@components/FloatingActionButton'; + +export default FloatingActionButton; diff --git a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx new file mode 100644 index 00000000000..43e4c049a83 --- /dev/null +++ b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx @@ -0,0 +1,42 @@ +import React, {useCallback, useRef} from 'react'; +import FloatingActionButtonAndPopover from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; +import FloatingActionButtonPopoverMenuRef from './types'; + +function BottomTabBarFloatingActionButton() { + const popoverModal = useRef(null); + + /** + * Method to hide popover when dragover. + */ + const hidePopoverOnDragOver = useCallback(() => { + if (!popoverModal.current) { + return; + } + popoverModal.current.hideCreateMenu(); + }, []); + + /** + * Method create event listener + */ + const createDragoverListener = () => { + document.addEventListener('dragover', hidePopoverOnDragOver); + }; + + /** + * Method remove event listener. + */ + const removeDragoverListener = () => { + document.removeEventListener('dragover', hidePopoverOnDragOver); + }; + + return ( + + ); +} + +export default BottomTabBarFloatingActionButton; diff --git a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/types.ts b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/types.ts new file mode 100644 index 00000000000..e8fd03ee2ad --- /dev/null +++ b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/types.ts @@ -0,0 +1,5 @@ +type FloatingActionButtonPopoverMenuRef = { + hideCreateMenu: () => void; +}; + +export default FloatingActionButtonPopoverMenuRef; diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js index 0b4c520c78a..7086e8a8561 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.js +++ b/src/pages/home/sidebar/SidebarScreen/index.js @@ -1,50 +1,18 @@ -import React, {useCallback, useRef} from 'react'; +import React from 'react'; import useWindowDimensions from '@hooks/useWindowDimensions'; import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; -import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; import sidebarPropTypes from './sidebarPropTypes'; function SidebarScreen(props) { - const popoverModal = useRef(null); const {isSmallScreenWidth} = useWindowDimensions(); - /** - * Method to hide popover when dragover. - */ - const hidePopoverOnDragOver = useCallback(() => { - if (!popoverModal.current) { - return; - } - popoverModal.current.hideCreateMenu(); - }, []); - - /** - * Method create event listener - */ - const createDragoverListener = () => { - document.addEventListener('dragover', hidePopoverOnDragOver); - }; - - /** - * Method remove event listener. - */ - const removeDragoverListener = () => { - document.removeEventListener('dragover', hidePopoverOnDragOver); - }; - return ( - - + /> ); } diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 3c73687ca81..6078f453b6e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -56,7 +56,7 @@ const defaultProps = { * @param {string} policyID */ function openEditor(policyID) { - Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID)); } /** @@ -150,9 +150,9 @@ function WorkspaceInitialPage(props) { const hasCustomUnitsError = PolicyUtils.hasCustomUnitsError(policy); const menuItems = [ { - translationKey: 'workspace.common.settings', + translationKey: 'workspace.common.overview', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policy.id)))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceOverviewCurrencyPage.js similarity index 97% rename from src/pages/workspace/WorkspaceSettingsCurrencyPage.js rename to src/pages/workspace/WorkspaceOverviewCurrencyPage.js index ce1e1d7b896..e58f53e60a6 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceOverviewCurrencyPage.js @@ -68,11 +68,11 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportDat const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; - const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); + const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_OVERVIEW.getRoute(policy.id)), [policy.id]); const onSelectCurrency = (item) => { Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList); - Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)); + Navigation.goBack(ROUTES.WORKSPACE_OVERVIEW.getRoute(policy.id)); }; return ( diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceOverviewPage.js similarity index 95% rename from src/pages/workspace/WorkspaceSettingsPage.js rename to src/pages/workspace/WorkspaceOverviewPage.js index d121134f26d..eda23089a03 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceOverviewPage.js @@ -56,7 +56,7 @@ const defaultProps = { ...policyDefaultProps, }; -function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { +function WorkspaceOverviewPage({policy, currencyList, windowWidth, route}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -90,15 +90,15 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return errors; }, []); - const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id)), [policy.id]); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_CURRENCY.getRoute(policy.id)), [policy.id]); const policyName = lodashGet(policy, 'name', ''); return ( {(hasVBA) => ( borderRadius: variables.buttonBorderRadius, }, + bottomTabBarContainer: { + height: 80, + borderTopWidth: 1, + borderTopColor: theme.border, + backgroundColor: theme.appBG, + flexDirection: 'row', + justifyContent: 'space-around', + alignItems: 'center', + }, + button: { backgroundColor: theme.buttonDefaultBG, borderRadius: variables.buttonBorderRadius, @@ -1333,15 +1343,6 @@ const styles = (theme: ThemeColors) => zIndex: 10, } satisfies ViewStyle), - floatingActionButtonContainer: { - position: 'absolute', - right: 20, - - // The bottom of the floating action button should align with the bottom of the compose box. - // The value should be equal to the height + marginBottom + marginTop of chatItemComposeSecondaryRow - bottom: variables.fabBottom, - }, - floatingActionButton: { backgroundColor: theme.success, height: variables.componentSizeLarge,