Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix show onboarding modal functions #44536

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3438e76
Fix show onboarding modal functions
filip-solecki Jun 27, 2024
0b0ece6
Add util function to check if screen is from onboarding flow
zfurtak Jun 27, 2024
4b7b93a
Added back navigation blocking from onboarding screens
zfurtak Jun 27, 2024
4a31c72
Fix TS errors
filip-solecki Jun 28, 2024
1ce3c51
Merge branch 'main' into filip-solecki/dismissing-onboarding
filip-solecki Jun 28, 2024
f916987
Fix lint
filip-solecki Jun 28, 2024
6a36309
Move error to onyx to handle back button errors
filip-solecki Jun 28, 2024
f4d9eb7
Merge branch 'main' into filip-solecki/dismissing-onboarding
filip-solecki Jul 1, 2024
f785748
Add translations
filip-solecki Jul 1, 2024
32084b6
generate onboarding state for initial state
adamgrzybowski Jul 2, 2024
ca9dfe9
improve redirecting to onboarding after logging in
adamgrzybowski Jul 2, 2024
db1d22a
prevent deeplinks if user should see onboarding after login
adamgrzybowski Jul 2, 2024
9422f48
Merge branch 'main' into filip-solecki/dismissing-onboarding
adamgrzybowski Jul 2, 2024
5e6f56a
move useDisableModalDismissOnEscape to top level onboarding navigator
adamgrzybowski Jul 4, 2024
1920968
Merge branch 'main' into filip-solecki/dismissing-onboarding
adamgrzybowski Jul 4, 2024
e2598f2
Add confirmed translation
filip-solecki Jul 4, 2024
4c35ff1
Fix lint
filip-solecki Jul 4, 2024
3079bc7
Fix jest tests
filip-solecki Jul 5, 2024
9eb7286
Merge branch 'main' into filip-solecki/dismissing-onboarding
filip-solecki Jul 5, 2024
063373d
apply CR suggestions
filip-solecki Jul 5, 2024
30344f3
Fix focus on Purpose page
filip-solecki Jul 8, 2024
fd1e2ec
Merge branch 'main' into filip-solecki/dismissing-onboarding
adamgrzybowski Jul 9, 2024
84075e4
Fix english text
filip-solecki Jul 9, 2024
ed75b69
Fix comments styling
filip-solecki Jul 9, 2024
7316347
add hasCompletedGuidedSetupFlowSelector
adamgrzybowski Jul 9, 2024
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
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5159,6 +5159,10 @@ const CONST = {
DATE: 'date',
LIST: 'dropdown',
},

NAVIGATION_ACTIONS: {
RESET: 'RESET',
},
} as const;

type Country = keyof typeof CONST.ALL_COUNTRIES;
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ const ONYXKEYS = {
/** Onboarding Purpose selected by the user during Onboarding flow */
ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected',

/** Onboarding error message to be displayed to the user */
ONBOARDING_ERROR_MESSAGE: 'onboardingErrorMessage',

/** Onboarding policyID selected by the user during Onboarding flow */
ONBOARDING_POLICY_ID: 'onboardingPolicyID',

Expand Down Expand Up @@ -782,6 +785,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
[ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string;
[ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string;
[ONYXKEYS.ONBOARDING_POLICY_ID]: string;
[ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string;
[ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,7 @@ export default {
title: 'What do you want to do today?',
errorSelection: 'Please make a selection to continue.',
errorContinue: 'Please press continue to get set up.',
errorBackButton: 'Please finish the setup questions to start using the app.',
[CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer',
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Track and budget expenses',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,7 @@ export default {
title: '¿Qué quieres hacer hoy?',
errorSelection: 'Por favor selecciona una opción para continuar.',
errorContinue: 'Por favor, haz click en continuar para configurar tu cuenta.',
errorBackButton: 'Por favor, finaliza las preguntas de configuración para empezar a utilizar la aplicación.',
filip-solecki marked this conversation as resolved.
Show resolved Hide resolved
mountiny marked this conversation as resolved.
Show resolved Hide resolved
[CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Cobrar de mi empresa',
[CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo',
[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Controlar y presupuestar gastos',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useOnboardingLayout from '@hooks/useOnboardingLayout';
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';
Expand All @@ -26,15 +28,11 @@ function OnboardingModalNavigator() {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useOnboardingLayout();
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: (onboarding) => {
// onboarding is an array for old accounts and accounts created from olddot
if (Array.isArray(onboarding)) {
return true;
}
return onboarding?.hasCompletedGuidedSetupFlow;
},
selector: hasCompletedGuidedSetupFlowSelector,
});

useDisableModalDismissOnEscape();

useEffect(() => {
if (!hasCompletedGuidedSetupFlow) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import * as Session from '@libs/actions/Session';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import Navigation from '@libs/Navigation/Navigation';
import linkingConfig from '@libs/Navigation/linkingConfig';
import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import type {RootStackParamList, State} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
Expand Down Expand Up @@ -53,7 +55,12 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
return;
}

Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_ROOT)});
Welcome.isOnboardingFlowCompleted({
onNotCompleted: () => {
const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config);
navigationRef.resetRoot(adaptedState);
},
});
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isLoadingApp]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type {RouterConfigOptions, StackNavigationState} from '@react-navigation/native';
import {getPathFromState, StackRouter} from '@react-navigation/native';
import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native';
import type {ParamListBase} from '@react-navigation/routers';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import * as Localize from '@libs/Localize';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import linkingConfig from '@libs/Navigation/linkingConfig';
import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
import {isCentralPaneName} from '@libs/NavigationUtils';
import {isCentralPaneName, isOnboardingFlowName} from '@libs/NavigationUtils';
import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import type {ResponsiveStackNavigatorRouterOptions} from './types';
Expand Down Expand Up @@ -97,6 +100,23 @@ function compareAndAdaptState(state: StackNavigationState<RootStackParamList>) {
}
}

function shouldPreventReset(state: StackNavigationState<ParamListBase>, action: CommonActions.Action | StackActionType) {
if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
return false;
}
const currentFocusedRoute = findFocusedRoute(state);
const targetFocusedRoute = findFocusedRoute(action?.payload);

// We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen
mountiny marked this conversation as resolved.
Show resolved Hide resolved
if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) {
Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
// We reset the URL as the browser sets it in a way that doesn't match the navigation state
// eslint-disable-next-line no-restricted-globals
history.replaceState({}, '', getPathFromState(state, linkingConfig.config));
return true;
}
}

function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
const stackRouter = StackRouter(options);

Expand All @@ -107,6 +127,12 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) {
const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
return state;
},
getStateForAction(state: StackNavigationState<ParamListBase>, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) {
if (shouldPreventReset(state, action)) {
return state;
}
return stackRouter.getStateForAction(state, action, configOptions);
},
};
}

Expand Down
46 changes: 31 additions & 15 deletions src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import type {NavigationState} from '@react-navigation/native';
import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native';
import React, {useContext, useEffect, useMemo, useRef} from 'react';
import {useOnyx} from 'react-native-onyx';
import HybridAppMiddleware from '@components/HybridAppMiddleware';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCurrentReportID from '@hooks/useCurrentReportID';
import useTheme from '@hooks/useTheme';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {FSPage} from '@libs/Fullstory';
import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import Log from '@libs/Log';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import AppNavigator from './AppNavigator';
import getPolicyIDFromState from './getPolicyIDFromState';
import linkingConfig from './linkingConfig';
Expand Down Expand Up @@ -77,25 +81,37 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
const {isSmallScreenWidth} = useWindowDimensions();
const {setActiveWorkspaceID} = useActiveWorkspace();

const initialState = useMemo(
() => {
if (!lastVisitedPath) {
return undefined;
}
const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
});

const path = initialUrl ? getPathFromURL(initialUrl) : null;
const initialState = useMemo(() => {
// If the user haven't completed the flow, we want to always redirect them to the onboarding flow.
if (!hasCompletedGuidedSetupFlow) {
const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config);
return adaptedState;
}

// For non-nullable paths we don't want to set initial state
if (path) {
return;
}
// If there is no lastVisitedPath, we can do early return. We won't modify the default behavior.
if (!lastVisitedPath) {
return undefined;
}

const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
return adaptedState;
},
const path = initialUrl ? getPathFromURL(initialUrl) : null;

// If the user opens the root of app "/" it will be parsed to empty string "".
// If the path is defined and different that empty string we don't want to modify the default behavior.
if (path) {
return;
}

// Otherwise we want to redirect the user to the last visited path.
const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config);
return adaptedState;

// The initialState value is relevant only on the first render.
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
[],
);
}, []);

// https://reactnavigation.org/docs/themes
const navigationTheme = useMemo(
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,8 @@ type FullScreenName = keyof FullScreenNavigatorParamList;

type CentralPaneName = keyof CentralPaneScreensParamList;

type OnboardingFlowName = keyof OnboardingModalNavigatorParamList;

type SwitchPolicyIDParams = {
policyID?: string;
route?: Routes;
Expand Down Expand Up @@ -1155,6 +1157,7 @@ export type {
NewChatNavigatorParamList,
NewTaskNavigatorParamList,
OnboardingModalNavigatorParamList,
OnboardingFlowName,
ParticipantsNavigatorParamList,
PrivateNotesNavigatorParamList,
ProfileNavigatorParamList,
Expand Down
14 changes: 12 additions & 2 deletions src/libs/NavigationUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cloneDeep from 'lodash/cloneDeep';
import SCREENS from '@src/SCREENS';
import getTopmostBottomTabRoute from './Navigation/getTopmostBottomTabRoute';
import type {CentralPaneName, RootStackParamList, State} from './Navigation/types';
import type {CentralPaneName, OnboardingFlowName, RootStackParamList, State} from './Navigation/types';

const CENTRAL_PANE_SCREEN_NAMES = new Set([
SCREENS.SETTINGS.WORKSPACES,
Expand All @@ -17,6 +17,8 @@ const CENTRAL_PANE_SCREEN_NAMES = new Set([
SCREENS.REPORT,
]);

const ONBOARDING_SCREEN_NAMES = new Set([SCREENS.ONBOARDING.PERSONAL_DETAILS, SCREENS.ONBOARDING.PURPOSE, SCREENS.ONBOARDING.WORK, SCREENS.ONBOARDING_MODAL.ONBOARDING]);

function isCentralPaneName(screen: string | undefined): screen is CentralPaneName {
if (!screen) {
return false;
Expand All @@ -25,6 +27,14 @@ function isCentralPaneName(screen: string | undefined): screen is CentralPaneNam
return CENTRAL_PANE_SCREEN_NAMES.has(screen as CentralPaneName);
}

function isOnboardingFlowName(screen: string | undefined): screen is OnboardingFlowName {
if (!screen) {
return false;
}

return ONBOARDING_SCREEN_NAMES.has(screen as OnboardingFlowName);
}

const removePolicyIDParamFromState = (state: State<RootStackParamList>) => {
const stateCopy = cloneDeep(state);
const bottomTabRoute = getTopmostBottomTabRoute(stateCopy);
Expand All @@ -34,4 +44,4 @@ const removePolicyIDParamFromState = (state: State<RootStackParamList>) => {
return stateCopy;
};

export {isCentralPaneName, removePolicyIDParamFromState};
export {isCentralPaneName, removePolicyIDParamFromState, isOnboardingFlowName};
68 changes: 45 additions & 23 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {findFocusedRoute} from '@react-navigation/native';
import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz';
import {Str} from 'expensify-common';
import isEmpty from 'lodash/isEmpty';
Expand Down Expand Up @@ -55,11 +56,13 @@ import {prepareDraftComment} from '@libs/DraftCommentUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as Environment from '@libs/Environment/Environment';
import * as ErrorUtils from '@libs/ErrorUtils';
import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import isPublicScreenRoute from '@libs/isPublicScreenRoute';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import {registerPaginationConfig} from '@libs/Middleware/Pagination';
import Navigation from '@libs/Navigation/Navigation';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {isOnboardingFlowName} from '@libs/NavigationUtils';
import type {NetworkStatus} from '@libs/NetworkConnection';
import LocalNotification from '@libs/Notification/LocalNotification';
import Parser from '@libs/Parser';
Expand Down Expand Up @@ -2549,28 +2552,47 @@ function openReportFromDeepLink(url: string) {
// Navigate to the report after sign-in/sign-up.
InteractionManager.runAfterInteractions(() => {
Session.waitForUserSignIn().then(() => {
Navigation.waitForProtectedRoutes().then(() => {
if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) {
Session.signOutAndRedirectToSignIn(true);
return;
}

// 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 (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) {
return;
}

if (shouldSkipDeepLinkNavigation(route)) {
return;
}

if (isAuthenticated) {
return;
}

Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
Onyx.connect({
key: ONYXKEYS.NVP_ONBOARDING,
callback: (onboarding) => {
Navigation.waitForProtectedRoutes().then(() => {
if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) {
Session.signOutAndRedirectToSignIn(true);
return;
}

// 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 (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) {
return;
}

if (shouldSkipDeepLinkNavigation(route)) {
return;
}

const state = navigationRef.getRootState();
const currentFocusedRoute = findFocusedRoute(state);
const hasCompletedGuidedSetupFlow = hasCompletedGuidedSetupFlowSelector(onboarding);

// We need skip deeplinking if the user hasn't completed the guided setup flow.
if (!hasCompletedGuidedSetupFlow) {
return;
}

if (isOnboardingFlowName(currentFocusedRoute?.name)) {
Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
return;
}

if (isAuthenticated) {
return;
}

Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
});
},
});
});
});
Expand Down
Loading
Loading