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
28 changes: 16 additions & 12 deletions src/Expensify.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Audio} from 'expo-av';
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {NativeEventSubscription} from 'react-native';
import {AppState, Linking} from 'react-native';
import {AppState, Linking, NativeModules} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx, {withOnyx} from 'react-native-onyx';
import ConfirmModal from './components/ConfirmModal';
Expand Down Expand Up @@ -77,9 +77,12 @@ type ExpensifyOnyxProps = {

type ExpensifyProps = ExpensifyOnyxProps;

type SplashScreenHiddenContextType = {isSplashHidden?: boolean};
// HybridApp needs access to SetStateAction in order to properly hide SplashScreen when React Native was booted before.
type SplashScreenHiddenContextType = {isSplashHidden?: boolean; setIsSplashHidden: React.Dispatch<React.SetStateAction<boolean>>};

const SplashScreenHiddenContext = React.createContext<SplashScreenHiddenContextType>({});
const SplashScreenHiddenContext = React.createContext<SplashScreenHiddenContextType>({
setIsSplashHidden: () => {},
});

function Expensify({
isCheckingPublicRoom = true,
Expand Down Expand Up @@ -109,13 +112,6 @@ function Expensify({
const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]);
const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]);

const contextValue = useMemo(
() => ({
isSplashHidden,
}),
[isSplashHidden],
);

const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom;
const shouldHideSplash = shouldInit && !isSplashHidden;

Expand All @@ -139,6 +135,14 @@ function Expensify({
Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
}, []);

const contextValue = useMemo(
() => ({
isSplashHidden,
setIsSplashHidden,
}),
[isSplashHidden, setIsSplashHidden],
);

useLayoutEffect(() => {
// Initialize this client as being an active client
ActiveClientManager.init();
Expand Down Expand Up @@ -259,8 +263,8 @@ function Expensify({
/>
</SplashScreenHiddenContext.Provider>
)}

{shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
{/* HybridApp has own middleware to hide SplashScreen */}
{!NativeModules.HybridAppModule && shouldHideSplash && <SplashScreenHider onHide={onSplashHide} />}
</DeeplinkWrapper>
);
}
Expand Down
108 changes: 108 additions & 0 deletions src/components/HybridAppMiddleware.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {useNavigation} from '@react-navigation/native';
import type {StackNavigationProp} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {NativeModules} from 'react-native';
import useSplashScreen from '@hooks/useSplashScreen';
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 Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';

type HybridAppMiddlewareProps = {
children: React.ReactNode;
};

type HybridAppMiddlewareContextType = {
navigateToExitUrl: (exitUrl: Route) => void;
showSplashScreenOnNextStart: () => void;
};
const HybridAppMiddlewareContext = React.createContext<HybridAppMiddlewareContextType>({
navigateToExitUrl: () => {},
showSplashScreenOnNextStart: () => {},
});

/*
* HybridAppMiddleware is responsible for handling BootSplash visibility correctly.
* It is crucial to make transitions between OldDot and NewDot look smooth.
*/
function HybridAppMiddleware(props: HybridAppMiddlewareProps) {
const {isSplashHidden, setIsSplashHidden} = useSplashScreen();
const [startedTransition, setStartedTransition] = useState(false);
const [finishedTransition, setFinishedTransition] = useState(false);
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();

/*
* 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);
}

Navigation.navigate(exitUrl);
}, []);

/**
* 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(() => {
setIsSplashHidden(false);
setStartedTransition(false);
setFinishedTransition(false);
}, [setIsSplashHidden]);

useEffect(() => {
if (!finishedTransition || isSplashHidden) {
return;
}

Log.info('[HybridApp] Finished transtion', true);
BootSplash.hide().then(() => {
setIsSplashHidden(true);
Log.info('[HybridApp] Handling onboarding flow', true);
Welcome.handleHybridAppOnboarding();
});
}, [finishedTransition, isSplashHidden, setIsSplashHidden]);

useEffect(() => {
if (!startedTransition) {
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);

const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => {
clearTimeout(timeout);
setFinishedTransition(true);
});

return () => {
clearTimeout(timeout);
unsubscribeTransitionEnd();
};
}, [navigation, startedTransition]);

const contextValue = useMemo(
() => ({
navigateToExitUrl,
showSplashScreenOnNextStart,
}),
[navigateToExitUrl, showSplashScreenOnNextStart],
);

return <HybridAppMiddlewareContext.Provider value={contextValue}>{props.children}</HybridAppMiddlewareContext.Provider>;
}

HybridAppMiddleware.displayName = 'HybridAppMiddleware';

export default HybridAppMiddleware;
export type {HybridAppMiddlewareContextType};
export {HybridAppMiddlewareContext};
24 changes: 0 additions & 24 deletions src/components/TestToolMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ApiUtils from '@libs/ApiUtils';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import * as Network from '@userActions/Network';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Network as NetworkOnyx, User as UserOnyx} from '@src/types/onyx';
import Button from './Button';
import {withNetwork} from './OnyxProvider';
Expand All @@ -35,7 +30,6 @@ const USER_DEFAULT: UserOnyx = {shouldUseStagingServer: undefined, isSubscribedT
function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
const shouldUseStagingServer = user?.shouldUseStagingServer ?? ApiUtils.isUsingStagingApi();
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();

return (
Expand Down Expand Up @@ -94,24 +88,6 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
onPress={() => Session.invalidateCredentials()}
/>
</TestToolRow>
{/* Navigate to the Explanation Modal. This button is temporary to test Explanation Modal flow without HybridApp native module. */}
<TestToolRow title="Explanation modal">
<Button
small
text="Navigate"
onPress={() => {
Navigation.dismissModal();
if (isSmallScreenWidth) {
Navigation.navigate(ROUTES.HOME);
} else {
Report.navigateToConciergeChat();
}
setTimeout(() => {
Navigation.navigate(ROUTES.EXPLANATION_MODAL_ROOT);
}, variables.welcomeVideoDelay);
}}
/>
</TestToolRow>
</>
);
}
Expand Down
11 changes: 11 additions & 0 deletions src/hooks/useHybridAppMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {useContext} from 'react';
import {HybridAppMiddlewareContext} from '@components/HybridAppMiddleware';

type SplashScreenHiddenContextType = {isSplashHidden: boolean};

export default function useHybridAppMiddleware() {
const {navigateToExitUrl, showSplashScreenOnNextStart} = useContext(HybridAppMiddlewareContext);
return {navigateToExitUrl, showSplashScreenOnNextStart};
}

export type {SplashScreenHiddenContextType};
11 changes: 0 additions & 11 deletions src/hooks/useIsSplashHidden.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/hooks/useSplashScreen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {useContext} from 'react';
import {SplashScreenHiddenContext} from '@src/Expensify';

type SplashScreenHiddenContextType = {isSplashHidden: boolean; setIsSplashHidden: React.Dispatch<React.SetStateAction<boolean>>};

export default function useSplashScreen() {
const {isSplashHidden, setIsSplashHidden} = useContext(SplashScreenHiddenContext) as SplashScreenHiddenContextType;
return {isSplashHidden, setIsSplashHidden};
}

export type {SplashScreenHiddenContextType};
6 changes: 5 additions & 1 deletion src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {NavigationState} from '@react-navigation/native';
import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native';
import React, {useContext, useEffect, useMemo, useRef} from 'react';
import HybridAppMiddleware from '@components/HybridAppMiddleware';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCurrentReportID from '@hooks/useCurrentReportID';
Expand Down Expand Up @@ -152,7 +153,10 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
enabled: false,
}}
>
<AppNavigator authenticated={authenticated} />
{/* HybridAppMiddleware needs to have access to navigation ref and SplashScreenHidden context */}
<HybridAppMiddleware>
<AppNavigator authenticated={authenticated} />
</HybridAppMiddleware>
</NavigationContainer>
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/pages/LogInWithShortLivedAuthTokenPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ 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';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {PublicScreensParamList} from '@libs/Navigation/types';
import * as Session from '@userActions/Session';
import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
Expand All @@ -35,6 +35,7 @@ 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(() => {
Expand Down Expand Up @@ -66,8 +67,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA
if (exitTo) {
Navigation.isNavigationReady().then(() => {
const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : (exitTo as Route);
Navigation.navigate(url);
Welcome.handleHybridAppOnboarding();
navigateToExitUrl(url);
});
}
// The only dependencies of the effect are based on props.route
Expand Down
7 changes: 4 additions & 3 deletions src/pages/LogOutPreviousUserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ 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';
import * as SessionActions from '@userActions/Session';
import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand All @@ -33,6 +33,8 @@ 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;
const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL;
Expand Down Expand Up @@ -81,8 +83,7 @@ function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPrevio
// remove this screen and navigate to exit route
const exitUrl = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo;
Navigation.goBack();
Navigation.navigate(exitUrl);
Welcome.handleHybridAppOnboarding();
navigateToExitUrl(exitUrl);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
3 changes: 3 additions & 0 deletions src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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';
Expand All @@ -35,6 +36,7 @@ type ExitSurveyConfirmPageProps = ExitSurveyConfirmPageOnyxProps & StackScreenPr

function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitSurveyConfirmPageProps) {
const {translate} = useLocalize();
const {showSplashScreenOnNextStart} = useHybridAppMiddleware();
const {isOffline} = useNetwork();
const styles = useThemeStyles();

Expand Down Expand Up @@ -87,6 +89,7 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS
ExitSurvey.switchToOldDot().then(() => {
if (NativeModules.HybridAppModule) {
Navigation.resetToHome();
showSplashScreenOnNextStart();
NativeModules.HybridAppModule.closeReactNativeApp();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {ImageSourcePropType} from 'react-native';
import Reanimated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg';
import MobileBackgroundImage from '@assets/images/home-background--mobile-new.svg';
import useIsSplashHidden from '@hooks/useIsSplashHidden';
import useSplashScreen from '@hooks/useSplashScreen';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
Expand All @@ -25,7 +25,7 @@ function BackgroundImage({width, transitionDuration, isSmallScreen = false}: Bac
});
}

const isSplashHidden = useIsSplashHidden();
const {isSplashHidden} = useSplashScreen();
// Prevent rendering the background image until the splash screen is hidden.
// See issue: https://github.com/Expensify/App/issues/34696
if (!isSplashHidden) {
Expand Down
Loading