From 765ea1616bb3c513800256d1435c618a23122530 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 5 Jul 2024 16:01:01 +0200 Subject: [PATCH 1/7] Fix transitions --- src/CONST.ts | 1 + src/components/HybridAppMiddleware.tsx | 139 ++++++++++++------ src/hooks/useHybridAppMiddleware.ts | 4 +- src/hooks/useTransitionRouteParams.ts | 17 +++ src/libs/Navigation/NavigationRoot.tsx | 2 +- .../LogInWithShortLivedAuthTokenPage.tsx | 8 +- src/pages/LogOutPreviousUserPage.tsx | 8 +- .../ExitSurvey/ExitSurveyConfirmPage.tsx | 4 +- 8 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 src/hooks/useTransitionRouteParams.ts diff --git a/src/CONST.ts b/src/CONST.ts index 00f2245a55c01..7ee967114966e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -100,6 +100,7 @@ const CONST = { ANIMATION_GYROSCOPE_VALUE: 0.4, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, SCREEN_TRANSITION_END_TIMEOUT: 1000, + HYBRID_APP_MAX_TRANSITION_TIMEOUT: 5000, ARROW_HIDE_DELAY: 3000, API_ATTACHMENT_VALIDATIONS: { diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware.tsx index 5c6934f4fc3d5..303a2793edd6d 100644 --- a/src/components/HybridAppMiddleware.tsx +++ b/src/components/HybridAppMiddleware.tsx @@ -1,104 +1,149 @@ -import {useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import useSplashScreen from '@hooks/useSplashScreen'; +import useTransitionRouteParams from '@hooks/useTransitionRouteParams'; import BootSplash from '@libs/BootSplash'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; +import * as SessionUtils from '@libs/SessionUtils'; import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; -import type {Route} from '@src/ROUTES'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridAppRoute, Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; +import {InitialURLContext} from './InitialURLContextProvider'; type HybridAppMiddlewareProps = { + authenticated: boolean; children: React.ReactNode; }; type HybridAppMiddlewareContextType = { - navigateToExitUrl: (exitUrl: Route) => void; showSplashScreenOnNextStart: () => void; }; const HybridAppMiddlewareContext = React.createContext({ - navigateToExitUrl: () => {}, showSplashScreenOnNextStart: () => {}, }); /* * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. * It is crucial to make transitions between OldDot and NewDot look smooth. + * The middleware assumes that the entry point for HybridApp is the /transition route. */ -function HybridAppMiddleware(props: HybridAppMiddlewareProps) { +function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) { const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); const [startedTransition, setStartedTransition] = useState(false); const [finishedTransition, setFinishedTransition] = useState(false); - const navigation = useNavigation>(); + const [forcedTransition, setForcedTransition] = useState(false); - /* - * Handles navigation during transition from OldDot. For ordinary NewDot app it is just pure navigation. - */ - const navigateToExitUrl = useCallback((exitUrl: Route) => { - if (NativeModules.HybridAppModule) { - setStartedTransition(true); - Log.info(`[HybridApp] Started transition to ${exitUrl}`, true); + const initialURL = useContext(InitialURLContext); + const routeParams = useTransitionRouteParams(); + const [exitTo, setExitTo] = useState(); + + const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + + const maxTimeoutRef = useRef(null); + + // We need to ensure that the BootSplash is always hidden after a certain period. + useEffect(() => { + if (!NativeModules.HybridAppModule) { + return; } - Navigation.navigate(exitUrl); + maxTimeoutRef.current = setTimeout(() => { + Log.info('[HybridApp] Forcing transition due to unknown problem', true); + setStartedTransition(true); + setForcedTransition(true); + setExitTo(ROUTES.HOME); + }, CONST.HYBRID_APP_MAX_TRANSITION_TIMEOUT); }, []); - /** - * This function only affects iOS. If during a single app lifecycle we frequently transition from OldDot to NewDot, - * we need to artificially show the bootsplash because the app is only booted once. - */ + // Save `exitTo` when we reach /transition route. + // `exitTo` should always exist during OldDot -> NewDot transitions. + useEffect(() => { + if (!NativeModules.HybridAppModule || !routeParams?.exitTo || exitTo) { + return; + } + + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: routeParams?.exitTo}); + setExitTo(routeParams?.exitTo); + + Log.info(`[HybridApp] Started transition`, true); + setStartedTransition(true); + }, [exitTo, routeParams?.email, routeParams?.exitTo]); + + // This function only affects iOS. If during a single app lifecycle we frequently transition from OldDot to NewDot, + // we need to artificially show the bootsplash because the app is only booted once. const showSplashScreenOnNextStart = useCallback(() => { + Log.info('[HybridApp] Resetting the state of HybridAppMiddleware to show the BootSplash on the next transition', true); setIsSplashHidden(false); setStartedTransition(false); setFinishedTransition(false); + setForcedTransition(false); + setExitTo(undefined); }, [setIsSplashHidden]); useEffect(() => { - if (!finishedTransition || isSplashHidden) { + if (!startedTransition || finishedTransition) { return; } - Log.info('[HybridApp] Finished transtion', true); - BootSplash.hide().then(() => { - setIsSplashHidden(true); - Log.info('[HybridApp] Handling onboarding flow', true); - Welcome.handleHybridAppOnboarding(); - }); - }, [finishedTransition, isSplashHidden, setIsSplashHidden]); + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); - useEffect(() => { - if (!startedTransition) { + // We need to wait with navigating to exitTo until all login-related actions are complete. + if ((!authenticated || isLoggingInAsNewUser || isAccountLoading) && !forcedTransition) { return; } - // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout. - const timeout = setTimeout(() => { - setFinishedTransition(true); - }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + if (exitTo) { + Navigation.isNavigationReady().then(() => { + if (maxTimeoutRef.current) { + clearTimeout(maxTimeoutRef.current); + } + + // We need to remove /transition from route history. + // `useTransitionRouteParams` returns undefined for routes other than /transition. + if (routeParams) { + Log.info('[HybridApp] Removing /transition route from history', true); + Navigation.goBack(); + } + + Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); + Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setTimeout(() => { + Log.info('[HybridApp] Setting `finishedTransition` to true', true); + setFinishedTransition(true); + }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + }); + } + }, [authenticated, exitTo, finishedTransition, forcedTransition, initialURL, isAccountLoading, routeParams, sessionEmail, startedTransition]); - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - clearTimeout(timeout); - setFinishedTransition(true); - }); + useEffect(() => { + if (!finishedTransition || isSplashHidden) { + return; + } - return () => { - clearTimeout(timeout); - unsubscribeTransitionEnd(); - }; - }, [navigation, startedTransition]); + Log.info('[HybridApp] Finished transition, hiding BootSplash', true); + BootSplash.hide().then(() => { + setIsSplashHidden(true); + if (authenticated) { + Log.info('[HybridApp] Handling onboarding flow', true); + Welcome.handleHybridAppOnboarding(); + } + }); + }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); const contextValue = useMemo( () => ({ - navigateToExitUrl, showSplashScreenOnNextStart, }), - [navigateToExitUrl, showSplashScreenOnNextStart], + [showSplashScreenOnNextStart], ); - return {props.children}; + return {children}; } HybridAppMiddleware.displayName = 'HybridAppMiddleware'; diff --git a/src/hooks/useHybridAppMiddleware.ts b/src/hooks/useHybridAppMiddleware.ts index 18ebd97306301..897e60d58771c 100644 --- a/src/hooks/useHybridAppMiddleware.ts +++ b/src/hooks/useHybridAppMiddleware.ts @@ -4,8 +4,8 @@ import {HybridAppMiddlewareContext} from '@components/HybridAppMiddleware'; type SplashScreenHiddenContextType = {isSplashHidden: boolean}; export default function useHybridAppMiddleware() { - const {navigateToExitUrl, showSplashScreenOnNextStart} = useContext(HybridAppMiddlewareContext); - return {navigateToExitUrl, showSplashScreenOnNextStart}; + const {showSplashScreenOnNextStart} = useContext(HybridAppMiddlewareContext); + return showSplashScreenOnNextStart; } export type {SplashScreenHiddenContextType}; diff --git a/src/hooks/useTransitionRouteParams.ts b/src/hooks/useTransitionRouteParams.ts new file mode 100644 index 0000000000000..a2a01dc17fe93 --- /dev/null +++ b/src/hooks/useTransitionRouteParams.ts @@ -0,0 +1,17 @@ +import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; +import type {PublicScreensParamList, RootStackParamList} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; + +export default function useTransitionRouteParams() { + const activeRouteParams = useNavigationState((state) => { + const focusedRoute = findFocusedRoute(state); + + if (focusedRoute?.name !== SCREENS.TRANSITION_BETWEEN_APPS) { + return undefined; + } + + return focusedRoute?.params as PublicScreensParamList[typeof SCREENS.TRANSITION_BETWEEN_APPS]; + }); + + return activeRouteParams; +} diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index dd3a2890d0ec7..9a92557f83011 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -154,7 +154,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N }} > {/* HybridAppMiddleware needs to have access to navigation ref and SplashScreenHidden context */} - + diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index 47843aa434fac..a9fd0ef393145 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -9,7 +9,6 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import useHybridAppMiddleware from '@hooks/useHybridAppMiddleware'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -35,7 +34,6 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {navigateToExitUrl} = useHybridAppMiddleware(); const {email = '', shortLivedAuthToken = '', shortLivedToken = '', authTokenType, exitTo, error} = route?.params ?? {}; useEffect(() => { @@ -64,10 +62,10 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA Session.setAccountError(error); } - if (exitTo) { + // For HybridApp we have separate logic to handle transitions. + if (!NativeModules.HybridAppModule && exitTo) { Navigation.isNavigationReady().then(() => { - const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : (exitTo as Route); - navigateToExitUrl(url); + Navigation.navigate(exitTo as Route); }); } // The only dependencies of the effect are based on props.route diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index d46b13459c469..c279ba5b1e5e1 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -5,7 +5,6 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import {InitialURLContext} from '@components/InitialURLContextProvider'; -import useHybridAppMiddleware from '@hooks/useHybridAppMiddleware'; import * as SessionUtils from '@libs/SessionUtils'; import Navigation from '@navigation/Navigation'; import type {AuthScreensParamList} from '@navigation/types'; @@ -33,7 +32,6 @@ type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreen // This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPreviousUserPageProps) { const initialURL = useContext(InitialURLContext); - const {navigateToExitUrl} = useHybridAppMiddleware(); useEffect(() => { const sessionEmail = session?.email; @@ -78,12 +76,12 @@ function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPrevio // We don't want to navigate to the exitTo route when creating a new workspace from a deep link, // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate, // which is already called when AuthScreens mounts. - if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !isAccountLoading && !isLoggingInAsNewUser) { + // For HybridApp we have separate logic to handle transitions. + if (!NativeModules.HybridAppModule && exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !isAccountLoading && !isLoggingInAsNewUser) { Navigation.isNavigationReady().then(() => { // remove this screen and navigate to exit route - const exitUrl = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo; Navigation.goBack(); - navigateToExitUrl(exitUrl); + Navigation.navigate(exitTo); }); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx index a61875e76c2b9..8eae28c7cf5b1 100644 --- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx +++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx @@ -36,7 +36,7 @@ type ExitSurveyConfirmPageProps = ExitSurveyConfirmPageOnyxProps & StackScreenPr function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitSurveyConfirmPageProps) { const {translate} = useLocalize(); - const {showSplashScreenOnNextStart} = useHybridAppMiddleware(); + const showSplashScreenOnNextStart = useHybridAppMiddleware(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -88,8 +88,8 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS onPress={() => { ExitSurvey.switchToOldDot().then(() => { if (NativeModules.HybridAppModule) { - Navigation.resetToHome(); showSplashScreenOnNextStart(); + Navigation.resetToHome(); NativeModules.HybridAppModule.closeReactNativeApp(); return; } From d16db21151677bef7084d41cc510248febeed7f6 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 5 Jul 2024 16:17:01 +0200 Subject: [PATCH 2/7] Disable closing NewDot during changing user --- src/libs/actions/Session/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index db78b94731ae4..828e044f05fb9 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -198,12 +198,12 @@ function hasAuthToken(): boolean { return !!session.authToken; } -function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean) { +function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) { Log.info('Redirecting to Sign In because signOut() was called'); hideContextMenu(false); if (!isAnonymousUser()) { // In the HybridApp, we want the Old Dot to handle the sign out process - if (NativeModules.HybridAppModule) { + if (NativeModules.HybridAppModule && killHybridApp) { NativeModules.HybridAppModule.closeReactNativeApp(); return; } From 4b3f34a19edddd501c36bf29aed687e080ee1314 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 5 Jul 2024 17:04:51 +0200 Subject: [PATCH 3/7] Don't kill HybridApp on LogOut --- src/pages/LogOutPreviousUserPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index c279ba5b1e5e1..9bcf567143e82 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -40,7 +40,8 @@ function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPrevio const isSupportalLogin = route.params.authTokenType === CONST.AUTH_TOKEN_TYPES.SUPPORT; if (isLoggingInAsNewUser) { - SessionActions.signOutAndRedirectToSignIn(false, isSupportalLogin); + // We don't want to close react-native app in this particular case. + SessionActions.signOutAndRedirectToSignIn(false, isSupportalLogin, false); return; } From 4012735111010ae0f8b94cb61567840fb584d718 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 5 Jul 2024 17:07:35 +0200 Subject: [PATCH 4/7] Remove forcedTransition --- src/components/HybridAppMiddleware.tsx | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware.tsx index 303a2793edd6d..0b730185bdff2 100644 --- a/src/components/HybridAppMiddleware.tsx +++ b/src/components/HybridAppMiddleware.tsx @@ -35,7 +35,6 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); const [startedTransition, setStartedTransition] = useState(false); const [finishedTransition, setFinishedTransition] = useState(false); - const [forcedTransition, setForcedTransition] = useState(false); const initialURL = useContext(InitialURLContext); const routeParams = useTransitionRouteParams(); @@ -44,22 +43,6 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); - const maxTimeoutRef = useRef(null); - - // We need to ensure that the BootSplash is always hidden after a certain period. - useEffect(() => { - if (!NativeModules.HybridAppModule) { - return; - } - - maxTimeoutRef.current = setTimeout(() => { - Log.info('[HybridApp] Forcing transition due to unknown problem', true); - setStartedTransition(true); - setForcedTransition(true); - setExitTo(ROUTES.HOME); - }, CONST.HYBRID_APP_MAX_TRANSITION_TIMEOUT); - }, []); - // Save `exitTo` when we reach /transition route. // `exitTo` should always exist during OldDot -> NewDot transitions. useEffect(() => { @@ -81,7 +64,6 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps setIsSplashHidden(false); setStartedTransition(false); setFinishedTransition(false); - setForcedTransition(false); setExitTo(undefined); }, [setIsSplashHidden]); @@ -94,16 +76,12 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); // We need to wait with navigating to exitTo until all login-related actions are complete. - if ((!authenticated || isLoggingInAsNewUser || isAccountLoading) && !forcedTransition) { + if (!authenticated || isLoggingInAsNewUser || isAccountLoading) { return; } if (exitTo) { Navigation.isNavigationReady().then(() => { - if (maxTimeoutRef.current) { - clearTimeout(maxTimeoutRef.current); - } - // We need to remove /transition from route history. // `useTransitionRouteParams` returns undefined for routes other than /transition. if (routeParams) { @@ -119,7 +97,7 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps }, CONST.SCREEN_TRANSITION_END_TIMEOUT); }); } - }, [authenticated, exitTo, finishedTransition, forcedTransition, initialURL, isAccountLoading, routeParams, sessionEmail, startedTransition]); + }, [authenticated, exitTo, finishedTransition, initialURL, isAccountLoading, routeParams, sessionEmail, startedTransition]); useEffect(() => { if (!finishedTransition || isSplashHidden) { From 9b8bc4e1ff400eb1fb6429ceb8b53c0e3332a798 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 5 Jul 2024 17:09:38 +0200 Subject: [PATCH 5/7] Remove unused imports --- src/components/HybridAppMiddleware.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware.tsx index 0b730185bdff2..1b752ceaa2795 100644 --- a/src/components/HybridAppMiddleware.tsx +++ b/src/components/HybridAppMiddleware.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import useSplashScreen from '@hooks/useSplashScreen'; @@ -11,7 +11,6 @@ import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridAppRoute, Route} from '@src/ROUTES'; -import ROUTES from '@src/ROUTES'; import {InitialURLContext} from './InitialURLContextProvider'; type HybridAppMiddlewareProps = { From 5132dd597e20e4fd79984bb3923b04ff6391e958 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 5 Jul 2024 17:11:42 +0200 Subject: [PATCH 6/7] Remove unused const --- src/CONST.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7ee967114966e..00f2245a55c01 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -100,7 +100,6 @@ const CONST = { ANIMATION_GYROSCOPE_VALUE: 0.4, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, SCREEN_TRANSITION_END_TIMEOUT: 1000, - HYBRID_APP_MAX_TRANSITION_TIMEOUT: 5000, ARROW_HIDE_DELAY: 3000, API_ATTACHMENT_VALIDATIONS: { From 9f6a89572103ee54eae84c0eb90481f0cfec0ebf Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 8 Jul 2024 16:00:56 +0200 Subject: [PATCH 7/7] Use native events instead of hooks --- src/CONST.ts | 1 + .../HybridAppMiddleware/index.ios.tsx | 130 ++++++++++++++++++ .../index.tsx} | 53 ++----- ...eTransitionRouteParams.ts => useExitTo.ts} | 4 +- src/hooks/useHybridAppMiddleware.ts | 11 -- .../ExitSurvey/ExitSurveyConfirmPage.tsx | 3 - 6 files changed, 148 insertions(+), 54 deletions(-) create mode 100644 src/components/HybridAppMiddleware/index.ios.tsx rename src/components/{HybridAppMiddleware.tsx => HybridAppMiddleware/index.tsx} (66%) rename src/hooks/{useTransitionRouteParams.ts => useExitTo.ts} (88%) delete mode 100644 src/hooks/useHybridAppMiddleware.ts diff --git a/src/CONST.ts b/src/CONST.ts index 00f2245a55c01..f6ff060543215 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3724,6 +3724,7 @@ const CONST = { }, EVENTS: { SCROLLING: 'scrolling', + ON_RETURN_TO_OLD_DOT: 'onReturnToOldDot', }, CHAT_HEADER_LOADER_HEIGHT: 36, diff --git a/src/components/HybridAppMiddleware/index.ios.tsx b/src/components/HybridAppMiddleware/index.ios.tsx new file mode 100644 index 0000000000000..5b06e5626c6ef --- /dev/null +++ b/src/components/HybridAppMiddleware/index.ios.tsx @@ -0,0 +1,130 @@ +import type React from 'react'; +import {useContext, useEffect, useState} from 'react'; +import {NativeEventEmitter, NativeModules} from 'react-native'; +import type {NativeModule} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import useExitTo from '@hooks/useExitTo'; +import useSplashScreen from '@hooks/useSplashScreen'; +import BootSplash from '@libs/BootSplash'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SessionUtils from '@libs/SessionUtils'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridAppRoute, Route} from '@src/ROUTES'; + +type HybridAppMiddlewareProps = { + authenticated: boolean; + children: React.ReactNode; +}; + +/* + * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. + * It is crucial to make transitions between OldDot and NewDot look smooth. + * The middleware assumes that the entry point for HybridApp is the /transition route. + */ +function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) { + const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); + const [startedTransition, setStartedTransition] = useState(false); + const [finishedTransition, setFinishedTransition] = useState(false); + + const initialURL = useContext(InitialURLContext); + const exitToParam = useExitTo(); + const [exitTo, setExitTo] = useState(); + + const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + + // In iOS, the HybridApp defines the `onReturnToOldDot` event. + // If we frequently transition from OldDot to NewDot during a single app lifecycle, + // we need to artificially display the bootsplash since the app is booted only once. + // Therefore, isSplashHidden needs to be updated at the appropriate time. + useEffect(() => { + if (!NativeModules.HybridAppModule) { + return; + } + const HybridAppEvents = new NativeEventEmitter(NativeModules.HybridAppModule as unknown as NativeModule); + const listener = HybridAppEvents.addListener(CONST.EVENTS.ON_RETURN_TO_OLD_DOT, () => { + Log.info('[HybridApp] `onReturnToOldDot` event received. Resetting state of HybridAppMiddleware', true); + setIsSplashHidden(false); + setStartedTransition(false); + setFinishedTransition(false); + setExitTo(undefined); + }); + + return () => { + listener.remove(); + }; + }, [setIsSplashHidden]); + + // Save `exitTo` when we reach /transition route. + // `exitTo` should always exist during OldDot -> NewDot transitions. + useEffect(() => { + if (!NativeModules.HybridAppModule || !exitToParam || exitTo) { + return; + } + + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam}); + setExitTo(exitToParam); + + Log.info(`[HybridApp] Started transition`, true); + setStartedTransition(true); + }, [exitTo, exitToParam]); + + useEffect(() => { + if (!startedTransition || finishedTransition) { + return; + } + + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); + + // We need to wait with navigating to exitTo until all login-related actions are complete. + if (!authenticated || isLoggingInAsNewUser || isAccountLoading) { + return; + } + + if (exitTo) { + Navigation.isNavigationReady().then(() => { + // We need to remove /transition from route history. + // `useExitTo` returns undefined for routes other than /transition. + if (exitToParam) { + Log.info('[HybridApp] Removing /transition route from history', true); + Navigation.goBack(); + } + + Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); + Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setExitTo(undefined); + + setTimeout(() => { + Log.info('[HybridApp] Setting `finishedTransition` to true', true); + setFinishedTransition(true); + }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + }); + } + }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]); + + useEffect(() => { + if (!finishedTransition || isSplashHidden) { + return; + } + + Log.info('[HybridApp] Finished transition, hiding BootSplash', true); + BootSplash.hide().then(() => { + setIsSplashHidden(true); + if (authenticated) { + Log.info('[HybridApp] Handling onboarding flow', true); + Welcome.handleHybridAppOnboarding(); + } + }); + }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); + + return children; +} + +HybridAppMiddleware.displayName = 'HybridAppMiddleware'; + +export default HybridAppMiddleware; diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware/index.tsx similarity index 66% rename from src/components/HybridAppMiddleware.tsx rename to src/components/HybridAppMiddleware/index.tsx index 1b752ceaa2795..b8c72d9200ac8 100644 --- a/src/components/HybridAppMiddleware.tsx +++ b/src/components/HybridAppMiddleware/index.tsx @@ -1,8 +1,10 @@ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import type React from 'react'; +import {useContext, useEffect, useState} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import useExitTo from '@hooks/useExitTo'; import useSplashScreen from '@hooks/useSplashScreen'; -import useTransitionRouteParams from '@hooks/useTransitionRouteParams'; import BootSplash from '@libs/BootSplash'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -11,20 +13,12 @@ import * as Welcome from '@userActions/Welcome'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridAppRoute, Route} from '@src/ROUTES'; -import {InitialURLContext} from './InitialURLContextProvider'; type HybridAppMiddlewareProps = { authenticated: boolean; children: React.ReactNode; }; -type HybridAppMiddlewareContextType = { - showSplashScreenOnNextStart: () => void; -}; -const HybridAppMiddlewareContext = React.createContext({ - showSplashScreenOnNextStart: () => {}, -}); - /* * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. * It is crucial to make transitions between OldDot and NewDot look smooth. @@ -36,7 +30,7 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps const [finishedTransition, setFinishedTransition] = useState(false); const initialURL = useContext(InitialURLContext); - const routeParams = useTransitionRouteParams(); + const exitToParam = useExitTo(); const [exitTo, setExitTo] = useState(); const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); @@ -45,26 +39,16 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps // Save `exitTo` when we reach /transition route. // `exitTo` should always exist during OldDot -> NewDot transitions. useEffect(() => { - if (!NativeModules.HybridAppModule || !routeParams?.exitTo || exitTo) { + if (!NativeModules.HybridAppModule || !exitToParam || exitTo) { return; } - Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: routeParams?.exitTo}); - setExitTo(routeParams?.exitTo); + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam}); + setExitTo(exitToParam); Log.info(`[HybridApp] Started transition`, true); setStartedTransition(true); - }, [exitTo, routeParams?.email, routeParams?.exitTo]); - - // This function only affects iOS. If during a single app lifecycle we frequently transition from OldDot to NewDot, - // we need to artificially show the bootsplash because the app is only booted once. - const showSplashScreenOnNextStart = useCallback(() => { - Log.info('[HybridApp] Resetting the state of HybridAppMiddleware to show the BootSplash on the next transition', true); - setIsSplashHidden(false); - setStartedTransition(false); - setFinishedTransition(false); - setExitTo(undefined); - }, [setIsSplashHidden]); + }, [exitTo, exitToParam]); useEffect(() => { if (!startedTransition || finishedTransition) { @@ -82,21 +66,23 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps if (exitTo) { Navigation.isNavigationReady().then(() => { // We need to remove /transition from route history. - // `useTransitionRouteParams` returns undefined for routes other than /transition. - if (routeParams) { + // `useExitTo` returns undefined for routes other than /transition. + if (exitToParam) { Log.info('[HybridApp] Removing /transition route from history', true); Navigation.goBack(); } Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setExitTo(undefined); + setTimeout(() => { Log.info('[HybridApp] Setting `finishedTransition` to true', true); setFinishedTransition(true); }, CONST.SCREEN_TRANSITION_END_TIMEOUT); }); } - }, [authenticated, exitTo, finishedTransition, initialURL, isAccountLoading, routeParams, sessionEmail, startedTransition]); + }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]); useEffect(() => { if (!finishedTransition || isSplashHidden) { @@ -113,18 +99,9 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps }); }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); - const contextValue = useMemo( - () => ({ - showSplashScreenOnNextStart, - }), - [showSplashScreenOnNextStart], - ); - - return {children}; + return children; } HybridAppMiddleware.displayName = 'HybridAppMiddleware'; export default HybridAppMiddleware; -export type {HybridAppMiddlewareContextType}; -export {HybridAppMiddlewareContext}; diff --git a/src/hooks/useTransitionRouteParams.ts b/src/hooks/useExitTo.ts similarity index 88% rename from src/hooks/useTransitionRouteParams.ts rename to src/hooks/useExitTo.ts index a2a01dc17fe93..74226453d3f61 100644 --- a/src/hooks/useTransitionRouteParams.ts +++ b/src/hooks/useExitTo.ts @@ -2,7 +2,7 @@ import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; import type {PublicScreensParamList, RootStackParamList} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; -export default function useTransitionRouteParams() { +export default function useExitTo() { const activeRouteParams = useNavigationState((state) => { const focusedRoute = findFocusedRoute(state); @@ -13,5 +13,5 @@ export default function useTransitionRouteParams() { return focusedRoute?.params as PublicScreensParamList[typeof SCREENS.TRANSITION_BETWEEN_APPS]; }); - return activeRouteParams; + return activeRouteParams?.exitTo; } diff --git a/src/hooks/useHybridAppMiddleware.ts b/src/hooks/useHybridAppMiddleware.ts deleted file mode 100644 index 897e60d58771c..0000000000000 --- a/src/hooks/useHybridAppMiddleware.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {useContext} from 'react'; -import {HybridAppMiddlewareContext} from '@components/HybridAppMiddleware'; - -type SplashScreenHiddenContextType = {isSplashHidden: boolean}; - -export default function useHybridAppMiddleware() { - const {showSplashScreenOnNextStart} = useContext(HybridAppMiddlewareContext); - return showSplashScreenOnNextStart; -} - -export type {SplashScreenHiddenContextType}; diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx index 8eae28c7cf5b1..db9f3199954f5 100644 --- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx +++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx @@ -10,7 +10,6 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {MushroomTopHat} from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; -import useHybridAppMiddleware from '@hooks/useHybridAppMiddleware'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -36,7 +35,6 @@ type ExitSurveyConfirmPageProps = ExitSurveyConfirmPageOnyxProps & StackScreenPr function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitSurveyConfirmPageProps) { const {translate} = useLocalize(); - const showSplashScreenOnNextStart = useHybridAppMiddleware(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -88,7 +86,6 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS onPress={() => { ExitSurvey.switchToOldDot().then(() => { if (NativeModules.HybridAppModule) { - showSplashScreenOnNextStart(); Navigation.resetToHome(); NativeModules.HybridAppModule.closeReactNativeApp(); return;