Skip to content

Commit

Permalink
Merge pull request Expensify#49586 from software-mansion-labs/fix/onb…
Browse files Browse the repository at this point in the history
…oarding-offline

Refactor NewDot and HybridApp onboarding flow
  • Loading branch information
mountiny authored Sep 30, 2024
2 parents a9224f3 + 075af14 commit fbe4025
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 205 deletions.
10 changes: 0 additions & 10 deletions src/Expensify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {updateLastRoute} from './libs/actions/App';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
import {handleHybridAppOnboarding} from './libs/actions/Welcome';
import * as ActiveClientManager from './libs/ActiveClientManager';
import FS from './libs/Fullstory';
import * as Growl from './libs/Growl';
Expand Down Expand Up @@ -99,7 +98,6 @@ function Expensify({
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
const [tryNewDotData] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT);
const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);

useEffect(() => {
Expand All @@ -118,14 +116,6 @@ function Expensify({
setAttemptedToOpenPublicRoom(true);
}, [isCheckingPublicRoom]);

useEffect(() => {
if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN || tryNewDotData === undefined) {
return;
}

handleHybridAppOnboarding();
}, [splashScreenState, tryNewDotData]);

const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]);
const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]);

Expand Down
22 changes: 2 additions & 20 deletions src/components/ExplanationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,20 @@
import React, {useCallback} from 'react';
import React from 'react';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import * as Welcome from '@userActions/Welcome';
import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow';
import CONST from '@src/CONST';
import FeatureTrainingModal from './FeatureTrainingModal';

function ExplanationModal() {
const {translate} = useLocalize();

const onClose = useCallback(() => {
Welcome.completeHybridAppOnboarding();

// We need to check if standard NewDot onboarding is completed.
Welcome.isOnboardingFlowCompleted({
onNotCompleted: () => {
setTimeout(() => {
Navigation.isNavigationReady().then(() => {
OnboardingFlow.startOnboardingFlow();
});
}, variables.welcomeVideoDelay);
},
});
}, []);

return (
<FeatureTrainingModal
title={translate('onboarding.explanationModal.title')}
description={translate('onboarding.explanationModal.description')}
secondaryDescription={translate('onboarding.explanationModal.secondaryDescription')}
confirmText={translate('footer.getStarted')}
videoURL={CONST.WELCOME_VIDEO_URL}
onClose={onClose}
onClose={Welcome.completeHybridAppOnboarding}
/>
);
}
Expand Down
11 changes: 3 additions & 8 deletions src/components/LHNOptionsList/OptionRowLHN.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
import DomUtils from '@libs/DomUtils';
import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import Performance from '@libs/Performance';
Expand All @@ -47,7 +47,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`);
const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER);
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
});
const [shouldHideGBRTooltip] = useOnyx(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, {initialValue: true});
Expand Down Expand Up @@ -171,12 +171,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
>
<EducationalTooltip
shouldRender={
isFirstTimeNewExpensifyUser &&
!shouldHideGBRTooltip &&
hasCompletedGuidedSetupFlow &&
isScreenFocused &&
shouldUseNarrowLayout &&
ReportUtils.isConciergeChatReport(report)
isFirstTimeNewExpensifyUser && !shouldHideGBRTooltip && isOnboardingCompleted && isScreenFocused && shouldUseNarrowLayout && ReportUtils.isConciergeChatReport(report)
}
renderTooltipContent={renderGBRTooltip}
anchorAlignment={{
Expand Down
46 changes: 46 additions & 0 deletions src/hooks/useOnboardingFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {useEffect} from 'react';
import {NativeModules} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Navigation from '@libs/Navigation/Navigation';
import {hasCompletedGuidedSetupFlowSelector, hasCompletedHybridAppOnboardingFlowSelector} from '@libs/onboardingSelectors';
import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

/**
* Hook to handle redirection to the onboarding flow based on the user's onboarding status
*
* Warning: This hook should be used only once in the app
*/
function useOnboardingFlowRouter() {
const [isOnboardingCompleted] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
});
const [isHybridAppOnboardingCompleted] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {
selector: hasCompletedHybridAppOnboardingFlowSelector,
});

useEffect(() => {
if (NativeModules.HybridAppModule) {
// When user is transitioning from OldDot to NewDot, we usually show the explanation modal
if (isHybridAppOnboardingCompleted === false) {
Navigation.navigate(ROUTES.EXPLANATION_MODAL_ROOT);
}

// But if the hybrid app onboarding is completed, but NewDot onboarding is not completed, we start NewDot onboarding flow
// This is a special case when user created an account from NewDot without finishing the onboarding flow and then logged in from OldDot
if (isHybridAppOnboardingCompleted === true && isOnboardingCompleted === false) {
OnboardingFlow.startOnboardingFlow();
}
}

// If the user is not transitioning from OldDot to NewDot, we should start NewDot onboarding flow if it's not completed yet
if (!NativeModules.HybridAppModule && isOnboardingCompleted === false) {
OnboardingFlow.startOnboardingFlow();
}
}, [isOnboardingCompleted, isHybridAppOnboardingCompleted]);

return {isOnboardingCompleted, isHybridAppOnboardingCompleted};
}

export default useOnboardingFlowRouter;
27 changes: 15 additions & 12 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import OptionsListContextProvider from '@components/OptionListContextProvider';
import {SearchContextProvider} from '@components/Search/SearchContext';
import SearchRouter from '@components/Search/SearchRouter/SearchRouter';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useOnboardingFlowRouter from '@hooks/useOnboardingFlow';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
Expand Down Expand Up @@ -237,7 +238,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
);
const modal = useRef<OnyxTypes.Modal>({});
const [didPusherInit, setDidPusherInit] = useState(false);

const {isOnboardingCompleted} = useOnboardingFlowRouter();
let initialReportID: string | undefined;
const isInitialRender = useRef(true);
if (isInitialRender.current) {
Expand Down Expand Up @@ -526,17 +527,19 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
options={onboardingModalScreenOptions}
component={WelcomeVideoModalNavigator}
/>
<RootStack.Screen
name={NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR}
options={onboardingScreenOptions}
component={OnboardingModalNavigator}
listeners={{
focus: () => {
Modal.setDisableDismissOnEscape(true);
},
beforeRemove: () => Modal.setDisableDismissOnEscape(false),
}}
/>
{isOnboardingCompleted === false && (
<RootStack.Screen
name={NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR}
options={onboardingScreenOptions}
component={OnboardingModalNavigator}
listeners={{
focus: () => {
Modal.setDisableDismissOnEscape(true);
},
beforeRemove: () => Modal.setDisableDismissOnEscape(false),
}}
/>
)}
<RootStack.Screen
name={SCREENS.WORKSPACE_JOIN_USER}
options={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import {createStackNavigator} from '@react-navigation/stack';
import React, {useCallback, useEffect} from 'react';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import OnboardingModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions';
import Navigation from '@libs/Navigation/Navigation';
import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types';
import OnboardingRefManager from '@libs/OnboardingRefManager';
import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails';
import OnboardingPurpose from '@pages/OnboardingPurpose';
import OnboardingWork from '@pages/OnboardingWork';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import Overlay from './Overlay';

Expand All @@ -27,29 +21,6 @@ const Stack = createStackNavigator<OnboardingModalNavigatorParamList>();
function OnboardingModalNavigator() {
const styles = useThemeStyles();
const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
});
const {shouldUseNarrowLayout} = useResponsiveLayout();

useEffect(() => {
if (!hasCompletedGuidedSetupFlow) {
return;
}
Navigation.isNavigationReady().then(() => {
// On small screens, pop all navigation states and go back to HOME.
// On large screens, need to go back to previous route and then redirect to Concierge,
// otherwise going back on Concierge will go to onboarding and then redirected to Concierge again
if (shouldUseNarrowLayout) {
Navigation.setShouldPopAllStateOnUP(true);
Navigation.goBack(ROUTES.HOME, true, true);
} else {
Navigation.goBack();
Report.navigateToConciergeChat();
}
});
}, [hasCompletedGuidedSetupFlow, shouldUseNarrowLayout]);

const outerViewRef = React.useRef<View>(null);

const handleOuterClick = useCallback(() => {
Expand All @@ -58,9 +29,6 @@ function OnboardingModalNavigator() {

useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, handleOuterClick, {shouldBubble: true});

if (hasCompletedGuidedSetupFlow) {
return null;
}
return (
<NoDropZone>
<Overlay />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {useNavigation} from '@react-navigation/native';
import React, {memo, useCallback, useEffect, useState} from 'react';
import {NativeModules, View} from 'react-native';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
Expand All @@ -11,11 +10,9 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Session from '@libs/actions/Session';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as SearchUtils from '@libs/SearchUtils';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
Expand All @@ -24,10 +21,7 @@ import navigationRef from '@navigation/navigationRef';
import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
import variables from '@styles/variables';
import * as Welcome from '@userActions/Welcome';
import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -69,38 +63,14 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const navigation = useNavigation();
const {activeWorkspaceID} = useActiveWorkspace();
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const transactionViolations = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [chatTabBrickRoad, setChatTabBrickRoad] = useState<BrickRoad>(getChatTabBrickRoad(activeWorkspaceID));

useEffect(() => {
setChatTabBrickRoad(getChatTabBrickRoad(activeWorkspaceID));
}, [activeWorkspaceID, transactionViolations]);

useEffect(() => {
const navigationState = navigation.getState() as State<RootStackParamList> | undefined;
const routes = navigationState?.routes;
const currentRoute = routes?.[navigationState?.index ?? 0];
// When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method.
// To prevent this, the value of the bottomTabRoute?.name is checked here
if (!!(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && !isCentralPaneName(currentRoute.name)) || Session.isAnonymousUser()) {
return;
}

// HybridApp has own entry point when we decide whether to display onboarding and explanation modal.
if (NativeModules.HybridAppModule) {
return;
}

Welcome.isOnboardingFlowCompleted({
onNotCompleted: () => OnboardingFlow.startOnboardingFlow(),
});

// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isLoadingApp]);

const navigateToChats = useCallback(() => {
if (selectedTab === SCREENS.HOME) {
return;
Expand Down
6 changes: 3 additions & 3 deletions src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import Firebase from '@libs/Firebase';
import {FSPage} from '@libs/Fullstory';
import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import Log from '@libs/Log';
import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
import {updateOnboardingLastVisitedPath} from '@userActions/Welcome';
Expand Down Expand Up @@ -92,7 +92,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh
const {setActiveWorkspaceID} = useActiveWorkspace();
const [user] = useOnyx(ONYXKEYS.USER);

const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
});

Expand All @@ -103,7 +103,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh

// If the user haven't completed the flow, we want to always redirect them to the onboarding flow.
// We also make sure that the user is authenticated.
if (!NativeModules.HybridAppModule && !hasCompletedGuidedSetupFlow && authenticated && !shouldShowRequire2FAModal) {
if (!NativeModules.HybridAppModule && !isOnboardingCompleted && authenticated && !shouldShowRequire2FAModal) {
const {adaptedState} = getAdaptedStateFromPath(getOnboardingInitialPath(), linkingConfig.config);
return adaptedState;
}
Expand Down
10 changes: 10 additions & 0 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3580,6 +3580,11 @@ function completeOnboarding(
key: ONYXKEYS.NVP_INTRO_SELECTED,
value: {choice: engagementChoice},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_ONBOARDING,
value: {hasCompletedGuidedSetupFlow: true},
},
);

const successData: OnyxUpdate[] = [...tasksForSuccessData];
Expand Down Expand Up @@ -3635,6 +3640,11 @@ function completeOnboarding(
key: ONYXKEYS.NVP_INTRO_SELECTED,
value: {choice: null},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_ONBOARDING,
value: {hasCompletedGuidedSetupFlow: false},
},
);

const guidedSetupData: GuidedSetupData = [
Expand Down
Loading

0 comments on commit fbe4025

Please sign in to comment.