Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3724,6 +3724,7 @@ const CONST = {
},
EVENTS: {
SCROLLING: 'scrolling',
ON_RETURN_TO_OLD_DOT: 'onReturnToOldDot',
},

CHAT_HEADER_LOADER_HEIGHT: 36,
Expand Down
108 changes: 0 additions & 108 deletions src/components/HybridAppMiddleware.tsx

This file was deleted.

130 changes: 130 additions & 0 deletions src/components/HybridAppMiddleware/index.ios.tsx
Original file line number Diff line number Diff line change
@@ -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<Route | HybridAppRoute | undefined>();

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;
107 changes: 107 additions & 0 deletions src/components/HybridAppMiddleware/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 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<Route | HybridAppRoute | undefined>();

const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false});
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});

// 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;
17 changes: 17 additions & 0 deletions src/hooks/useExitTo.ts
Original file line number Diff line number Diff line change
@@ -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 useExitTo() {
const activeRouteParams = useNavigationState<RootStackParamList, PublicScreensParamList[typeof SCREENS.TRANSITION_BETWEEN_APPS] | undefined>((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?.exitTo;
}
11 changes: 0 additions & 11 deletions src/hooks/useHybridAppMiddleware.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
}}
>
{/* HybridAppMiddleware needs to have access to navigation ref and SplashScreenHidden context */}
<HybridAppMiddleware>
<HybridAppMiddleware authenticated={authenticated}>
<AppNavigator authenticated={authenticated} />
</HybridAppMiddleware>
</NavigationContainer>
Expand Down
Loading