From f6502efaf704809f5d955402fddee823bf6972ab Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 7 Mar 2025 09:40:24 +0100 Subject: [PATCH 1/4] Migrate AuthScreens to useOnyx --- .../Navigation/AppNavigator/AuthScreens.tsx | 88 +++++++++---------- src/libs/Navigation/linkingConfig/config.ts | 14 ++- src/libs/actions/App.ts | 2 +- src/libs/actions/Session/index.ts | 17 +--- 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 79c5d5df795e2..d8babc15b72f1 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -2,7 +2,7 @@ import type {RouteProp} from '@react-navigation/native'; import {findFocusedRoute, useNavigation} from '@react-navigation/native'; import React, {memo, useEffect, useMemo, useRef, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import ActiveWorkspaceContextProvider from '@components/ActiveWorkspaceProvider'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; @@ -53,6 +53,7 @@ import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; import createRootStackNavigator from './createRootStackNavigator'; import {reportsSplitsWithEnteringAnimation, workspaceSplitsWithoutEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers'; @@ -67,17 +68,6 @@ import RightModalNavigator from './Navigators/RightModalNavigator'; import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator'; import useRootNavigatorScreenOptions from './useRootNavigatorScreenOptions'; -type AuthScreensProps = { - /** Session of currently logged in user */ - session: OnyxEntry; - - /** The report ID of the last opened public room as anonymous user */ - lastOpenedPublicRoomID: OnyxEntry; - - /** The last Onyx update ID was applied to the client */ - initialLastUpdateIDAppliedToClient: OnyxEntry; -}; - const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default; const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; @@ -210,7 +200,10 @@ const modalScreenListenersWithCancelSearch = { }, }; -function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensProps) { +function AuthScreens() { + const [session] = useOnyx(ONYXKEYS.SESSION); + const [lastOpenedPublicRoomID, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); + const [initialLastUpdateIDAppliedToClient, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); const theme = useTheme(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const rootNavigatorScreenOptions = useRootNavigatorScreenOptions(); @@ -221,6 +214,10 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const modal = useRef({}); const {isOnboardingCompleted} = useOnboardingFlowRouter(); const [shouldShowRequire2FAPage, setShouldShowRequire2FAPage] = useState(!!account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth); + const isLastOpenedPublicRoomIDLoading = isLoadingOnyxValue(lastOpenedPublicRoomIDStatus); + const isInitialLastUpdateIDAppliedToClientLoading = isLoadingOnyxValue(initialLastUpdateIDAppliedToClientStatus); + const isLastOpenedPublicRoomIDLoadedRef = useRef(false); + const isInitialLastUpdateIDAppliedToClientLoadedRef = useRef(false); const navigation = useNavigation(); useEffect(() => { @@ -256,6 +253,36 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie } Navigation.navigate(ROUTES.REQUIRE_TWO_FACTOR_AUTH); }, [shouldShowRequire2FAPage]); + useEffect(() => { + if (isLastOpenedPublicRoomIDLoading || isLastOpenedPublicRoomIDLoadedRef.current) { + return; + } + + isLastOpenedPublicRoomIDLoadedRef.current = true; + if (lastOpenedPublicRoomID) { + // Re-open the last opened public room if the user logged in from a public room link + Report.openLastOpenedPublicRoom(lastOpenedPublicRoomID); + } + }, [isLastOpenedPublicRoomIDLoading, lastOpenedPublicRoomID]); + + useEffect(() => { + if (isInitialLastUpdateIDAppliedToClientLoading || isInitialLastUpdateIDAppliedToClientLoadedRef.current) { + return; + } + + isInitialLastUpdateIDAppliedToClientLoadedRef.current = true; + // In Hybrid App we decide to call one of those method when booting ND and we don't want to duplicate calls + if (!CONFIG.IS_HYBRID_APP) { + // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app + // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). + if (SessionUtils.didUserLogInDuringSession()) { + App.openApp(); + } else { + Log.info('[AuthScreens] Sending ReconnectApp'); + App.reconnectApp(initialLastUpdateIDAppliedToClient); + } + } + }, [initialLastUpdateIDAppliedToClient, isInitialLastUpdateIDAppliedToClientLoading]); useEffect(() => { const shortcutsOverviewShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUTS; @@ -276,28 +303,12 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie PusherConnectionManager.init(); initializePusher(); - // In Hybrid App we decide to call one of those method when booting ND and we don't want to duplicate calls - if (!CONFIG.IS_HYBRID_APP) { - // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app - // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). - if (SessionUtils.didUserLogInDuringSession()) { - App.openApp(); - } else { - Log.info('[AuthScreens] Sending ReconnectApp'); - App.reconnectApp(initialLastUpdateIDAppliedToClient); - } - } - PriorityMode.autoSwitchToFocusMode(); App.setUpPoliciesAndNavigate(session); App.redirectThirdPartyDesktopSignIn(); - if (lastOpenedPublicRoomID) { - // Re-open the last opened public room if the user logged in from a public room link - Report.openLastOpenedPublicRoom(lastOpenedPublicRoomID); - } Download.clearDownloads(); const unsubscribeOnyxModal = onyxSubscribe({ @@ -662,20 +673,5 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie AuthScreens.displayName = 'AuthScreens'; -const AuthScreensMemoized = memo(AuthScreens, () => true); - -// Migration to useOnyx cause re-login if logout from deeplinked report in desktop app -// Further analysis required and more details can be seen here: -// https://github.com/Expensify/App/issues/50560 -// eslint-disable-next-line -export default withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - lastOpenedPublicRoomID: { - key: ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, - }, - initialLastUpdateIDAppliedToClient: { - key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - }, -})(AuthScreensMemoized); +// This memo is needed because is one of the main components in the app and navigation root and is relevant for the app performance. +export default memo(AuthScreens); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 0db3c5fb11f71..f50e61c63ac89 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1563,7 +1563,19 @@ const config: LinkingOptions['config'] = { path: ROUTES.HOME, exact: true, }, - [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, + [SCREENS.REPORT]: { + path: ROUTES.REPORT_WITH_ID.route, + // If params are defined, but reportID is explicitly undefined, we will get the url /r/undefined. + // We want to avoid that situation, so we will return an empty string instead. + parse: { + // eslint-disable-next-line + reportID: (reportID: string | undefined) => reportID ?? '', + }, + stringify: { + // eslint-disable-next-line + reportID: (reportID: string | undefined) => reportID ?? '', + }, + }, }, }, diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 226299c4ad411..a246f435f8c24 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -481,7 +481,7 @@ function setUpPoliciesAndNavigate(session: OnyxEntry) { if (!isLoggingInAsNewUser && exitTo) { Navigation.waitForProtectedRoutes() .then(() => { - Navigation.navigate(exitTo); + Navigation.navigate(exitTo, {forceReplace: true}); }) .then(endSignOnTransition); } else { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index ec145ce2a30dc..dc81842045a07 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -55,7 +55,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridAppRoute, Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type Credentials from '@src/types/onyx/Credentials'; import type Response from '@src/types/onyx/Response'; import type Session from '@src/types/onyx/Session'; @@ -843,19 +842,11 @@ function clearSignInData() { } /** - * Reset all current params of the Home route + * Reset navigation state after logout */ -function resetHomeRouteParams() { +function resetNavigationState() { Navigation.isNavigationReady().then(() => { - const routes = navigationRef.current?.getState()?.routes; - const homeRoute = routes?.find((route) => route.name === SCREENS.HOME); - - const emptyParams: Record = {}; - Object.keys(homeRoute?.params ?? {}).forEach((paramKey) => { - emptyParams[paramKey] = undefined; - }); - - Navigation.setParams(emptyParams, homeRoute?.key ?? ''); + navigationRef.resetRoot(navigationRef.getRootState()); Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); }); } @@ -875,7 +866,7 @@ function cleanupSession() { PersistedRequests.clear(); NetworkConnection.clearReconnectionCallbacks(); SessionUtils.resetDidUserLogInDuringSession(); - resetHomeRouteParams(); + resetNavigationState(); clearCache().then(() => { Log.info('Cleared all cache data', true, {}, true); }); From 91221a42e0298470a517b02427204d5332aa06bd Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 7 Mar 2025 11:39:54 +0100 Subject: [PATCH 2/4] Add wrapper for AuthScreens to load onyx values before first render --- .../Navigation/AppNavigator/AuthScreens.tsx | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index d8babc15b72f1..755d923719899 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx, {useOnyx} from 'react-native-onyx'; import ActiveWorkspaceContextProvider from '@components/ActiveWorkspaceProvider'; import ComposeProviders from '@components/ComposeProviders'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; @@ -200,10 +201,25 @@ const modalScreenListenersWithCancelSearch = { }, }; +// AuthScreens has been migrated to useOnyx. In the previous version, withOnyx waited until all the Onyx values are loaded and then rendered the component first time. +// To avoid breaking functionalities that rely on this logic, we are keeping the same behavior by waiting for the Onyx values to be loaded before rendering the component. function AuthScreens() { + const [, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); + const [, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); + const isLastOpenedPublicRoomIDLoading = isLoadingOnyxValue(lastOpenedPublicRoomIDStatus); + const isInitialLastUpdateIDAppliedToClientLoading = isLoadingOnyxValue(initialLastUpdateIDAppliedToClientStatus); + + if (isLastOpenedPublicRoomIDLoading && isInitialLastUpdateIDAppliedToClientLoading) { + return ; + } + + return ; +} + +function AuthScreensContent() { const [session] = useOnyx(ONYXKEYS.SESSION); - const [lastOpenedPublicRoomID, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); - const [initialLastUpdateIDAppliedToClient, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); + const [lastOpenedPublicRoomID] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); + const [initialLastUpdateIDAppliedToClient] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); const theme = useTheme(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const rootNavigatorScreenOptions = useRootNavigatorScreenOptions(); @@ -214,10 +230,6 @@ function AuthScreens() { const modal = useRef({}); const {isOnboardingCompleted} = useOnboardingFlowRouter(); const [shouldShowRequire2FAPage, setShouldShowRequire2FAPage] = useState(!!account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth); - const isLastOpenedPublicRoomIDLoading = isLoadingOnyxValue(lastOpenedPublicRoomIDStatus); - const isInitialLastUpdateIDAppliedToClientLoading = isLoadingOnyxValue(initialLastUpdateIDAppliedToClientStatus); - const isLastOpenedPublicRoomIDLoadedRef = useRef(false); - const isInitialLastUpdateIDAppliedToClientLoadedRef = useRef(false); const navigation = useNavigation(); useEffect(() => { @@ -253,36 +265,6 @@ function AuthScreens() { } Navigation.navigate(ROUTES.REQUIRE_TWO_FACTOR_AUTH); }, [shouldShowRequire2FAPage]); - useEffect(() => { - if (isLastOpenedPublicRoomIDLoading || isLastOpenedPublicRoomIDLoadedRef.current) { - return; - } - - isLastOpenedPublicRoomIDLoadedRef.current = true; - if (lastOpenedPublicRoomID) { - // Re-open the last opened public room if the user logged in from a public room link - Report.openLastOpenedPublicRoom(lastOpenedPublicRoomID); - } - }, [isLastOpenedPublicRoomIDLoading, lastOpenedPublicRoomID]); - - useEffect(() => { - if (isInitialLastUpdateIDAppliedToClientLoading || isInitialLastUpdateIDAppliedToClientLoadedRef.current) { - return; - } - - isInitialLastUpdateIDAppliedToClientLoadedRef.current = true; - // In Hybrid App we decide to call one of those method when booting ND and we don't want to duplicate calls - if (!CONFIG.IS_HYBRID_APP) { - // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app - // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). - if (SessionUtils.didUserLogInDuringSession()) { - App.openApp(); - } else { - Log.info('[AuthScreens] Sending ReconnectApp'); - App.reconnectApp(initialLastUpdateIDAppliedToClient); - } - } - }, [initialLastUpdateIDAppliedToClient, isInitialLastUpdateIDAppliedToClientLoading]); useEffect(() => { const shortcutsOverviewShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUTS; @@ -303,12 +285,28 @@ function AuthScreens() { PusherConnectionManager.init(); initializePusher(); + // In Hybrid App we decide to call one of those method when booting ND and we don't want to duplicate calls + if (!CONFIG.IS_HYBRID_APP) { + // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app + // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). + if (SessionUtils.didUserLogInDuringSession()) { + App.openApp(); + } else { + Log.info('[AuthScreens] Sending ReconnectApp'); + App.reconnectApp(initialLastUpdateIDAppliedToClient); + } + } + PriorityMode.autoSwitchToFocusMode(); App.setUpPoliciesAndNavigate(session); App.redirectThirdPartyDesktopSignIn(); + if (lastOpenedPublicRoomID) { + // Re-open the last opened public room if the user logged in from a public room link + Report.openLastOpenedPublicRoom(lastOpenedPublicRoomID); + } Download.clearDownloads(); const unsubscribeOnyxModal = onyxSubscribe({ From b07b64eecd4739011df484befb8b7fdb169534e0 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 7 Mar 2025 11:47:55 +0100 Subject: [PATCH 3/4] Adjust comment in AuthScreens wrapper --- src/libs/Navigation/AppNavigator/AuthScreens.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 755d923719899..f4463191b885a 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -202,7 +202,7 @@ const modalScreenListenersWithCancelSearch = { }; // AuthScreens has been migrated to useOnyx. In the previous version, withOnyx waited until all the Onyx values are loaded and then rendered the component first time. -// To avoid breaking functionalities that rely on this logic, we are keeping the same behavior by waiting for the Onyx values to be loaded before rendering the component. +// To avoid breaking functionalities that rely on this logic, we keep the same behavior by waiting for the Onyx values to be loaded before rendering the component. function AuthScreens() { const [, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); const [, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); From 99bb1d21217e1fca2b2427eb5a9ba8a149df01ea Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 7 Mar 2025 13:38:20 +0100 Subject: [PATCH 4/4] Move useOnyx to AuthScreens wrapper --- .../Navigation/AppNavigator/AuthScreens.tsx | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f4463191b885a..bb3c85beeda0b 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -69,6 +69,17 @@ import RightModalNavigator from './Navigators/RightModalNavigator'; import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator'; import useRootNavigatorScreenOptions from './useRootNavigatorScreenOptions'; +type AuthScreensContentProps = { + /** Session of currently logged in user */ + session: OnyxEntry; + + /** The report ID of the last opened public room as anonymous user */ + lastOpenedPublicRoomID: OnyxEntry; + + /** The last Onyx update ID was applied to the client */ + initialLastUpdateIDAppliedToClient: OnyxEntry; +}; + const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default; const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; @@ -204,22 +215,26 @@ const modalScreenListenersWithCancelSearch = { // AuthScreens has been migrated to useOnyx. In the previous version, withOnyx waited until all the Onyx values are loaded and then rendered the component first time. // To avoid breaking functionalities that rely on this logic, we keep the same behavior by waiting for the Onyx values to be loaded before rendering the component. function AuthScreens() { - const [, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); - const [, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [lastOpenedPublicRoomID, lastOpenedPublicRoomIDStatus] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); + const [initialLastUpdateIDAppliedToClient, initialLastUpdateIDAppliedToClientStatus] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); const isLastOpenedPublicRoomIDLoading = isLoadingOnyxValue(lastOpenedPublicRoomIDStatus); const isInitialLastUpdateIDAppliedToClientLoading = isLoadingOnyxValue(initialLastUpdateIDAppliedToClientStatus); - if (isLastOpenedPublicRoomIDLoading && isInitialLastUpdateIDAppliedToClientLoading) { + if (isLastOpenedPublicRoomIDLoading || isInitialLastUpdateIDAppliedToClientLoading) { return ; } - return ; + return ( + + ); } -function AuthScreensContent() { - const [session] = useOnyx(ONYXKEYS.SESSION); - const [lastOpenedPublicRoomID] = useOnyx(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID); - const [initialLastUpdateIDAppliedToClient] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); +function AuthScreensContent({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensContentProps) { const theme = useTheme(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();