diff --git a/src/CONST.ts b/src/CONST.ts index 13d44ee883be..0f97e81658f0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5159,6 +5159,10 @@ const CONST = { DATE: 'date', LIST: 'dropdown', }, + + NAVIGATION_ACTIONS: { + RESET: 'RESET', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5d6b5492d15c..1fc6f61f023d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -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', @@ -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; diff --git a/src/languages/en.ts b/src/languages/en.ts index 543dfbf5e541..a1153ec64019 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -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', diff --git a/src/languages/es.ts b/src/languages/es.ts index e7f4faab2725..c19aa5c3aae4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -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.', [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', diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 29a2205b2e37..61adcd77da76 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -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'; @@ -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; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx index 8c531a918af8..2e1c4c012156 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx @@ -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'; @@ -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]); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts index a1768df5e0d6..5b3cefb63a2d 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts @@ -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'; @@ -97,6 +100,23 @@ function compareAndAdaptState(state: StackNavigationState) { } } +function shouldPreventReset(state: StackNavigationState, 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 + 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); @@ -107,6 +127,12 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); return state; }, + getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) { + if (shouldPreventReset(state, action)) { + return state; + } + return stackRouter.getStateForAction(state, action, configOptions); + }, }; } diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index db64aea7ffe8..c197fa702bfb 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -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 {useOnyx} from 'react-native-onyx'; import HybridAppMiddleware from '@components/HybridAppMiddleware'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; @@ -8,11 +9,14 @@ 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'; @@ -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( diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a60316fb7768..7da961f57c93 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1125,6 +1125,8 @@ type FullScreenName = keyof FullScreenNavigatorParamList; type CentralPaneName = keyof CentralPaneScreensParamList; +type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; + type SwitchPolicyIDParams = { policyID?: string; route?: Routes; @@ -1155,6 +1157,7 @@ export type { NewChatNavigatorParamList, NewTaskNavigatorParamList, OnboardingModalNavigatorParamList, + OnboardingFlowName, ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, ProfileNavigatorParamList, diff --git a/src/libs/NavigationUtils.ts b/src/libs/NavigationUtils.ts index 34fc0b971ef6..aa26268977a2 100644 --- a/src/libs/NavigationUtils.ts +++ b/src/libs/NavigationUtils.ts @@ -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, @@ -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; @@ -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) => { const stateCopy = cloneDeep(state); const bottomTabRoute = getTopmostBottomTabRoute(stateCopy); @@ -34,4 +44,4 @@ const removePolicyIDParamFromState = (state: State) => { return stateCopy; }; -export {isCentralPaneName, removePolicyIDParamFromState}; +export {isCentralPaneName, removePolicyIDParamFromState, isOnboardingFlowName}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 9870b561ad6f..45bdca8e0fc7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -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'; @@ -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'; @@ -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); + }); + }, }); }); }); diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index a90c386d02b6..b592424cfcdf 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -11,7 +11,8 @@ import ROUTES from '@src/ROUTES'; import type Onboarding from '@src/types/onyx/Onboarding'; import type TryNewDot from '@src/types/onyx/TryNewDot'; -let onboarding: Onboarding | [] | undefined; +type OnboardingData = Onboarding | [] | undefined; + let isLoadingReportData = true; let tryNewDotData: TryNewDot | undefined; @@ -30,8 +31,8 @@ let isServerDataReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); -let resolveOnboardingFlowStatus: (value?: Promise) => void | undefined; -let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { +let resolveOnboardingFlowStatus: (value?: OnboardingData) => void; +let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { resolveOnboardingFlowStatus = resolve; }); @@ -45,7 +46,7 @@ function onServerDataReady(): Promise { } function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOnboardingFlowProps) { - isOnboardingFlowStatusKnownPromise.then(() => { + isOnboardingFlowStatusKnownPromise.then((onboarding) => { if (Array.isArray(onboarding) || onboarding?.hasCompletedGuidedSetupFlow === undefined) { return; } @@ -102,23 +103,7 @@ function handleHybridAppOnboarding() { } /** - * Check that a few requests have completed so that the welcome action can proceed: - * - * - Whether we are a first time new expensify user - * - Whether we have loaded all policies the server knows about - * - Whether we have loaded all reports the server knows about - * Check if onboarding data is ready in order to check if the user has completed onboarding or not - */ -function checkOnboardingDataReady() { - if (onboarding === undefined) { - return; - } - - resolveOnboardingFlowStatus?.(); -} - -/** - * Check if user dismissed modal and if report data are loaded + * Check if report data are loaded */ function checkServerDataReady() { if (isLoadingReportData) { @@ -143,6 +128,10 @@ function setOnboardingPurposeSelected(value: OnboardingPurposeType) { Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, value ?? null); } +function setOnboardingErrorMessage(value: string) { + Onyx.set(ONYXKEYS.ONBOARDING_ERROR_MESSAGE, value ?? null); +} + function setOnboardingAdminsChatReportID(adminsChatReportID?: string) { Onyx.set(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, adminsChatReportID ?? null); } @@ -186,9 +175,7 @@ Onyx.connect({ return; } - onboarding = value; - - checkOnboardingDataReady(); + resolveOnboardingFlowStatus(value); }, }); @@ -213,10 +200,9 @@ function resetAllChecks() { isServerDataReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); - isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { + isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { resolveOnboardingFlowStatus = resolve; }); - onboarding = undefined; isLoadingReportData = true; } @@ -229,4 +215,5 @@ export { setOnboardingPolicyID, completeHybridAppOnboarding, handleHybridAppOnboarding, + setOnboardingErrorMessage, }; diff --git a/src/libs/hasCompletedGuidedSetupFlowSelector.ts b/src/libs/hasCompletedGuidedSetupFlowSelector.ts new file mode 100644 index 000000000000..83cde0a0be8c --- /dev/null +++ b/src/libs/hasCompletedGuidedSetupFlowSelector.ts @@ -0,0 +1,12 @@ +import type {OnyxValue} from 'react-native-onyx'; +import type ONYXKEYS from '@src/ONYXKEYS'; + +function hasCompletedGuidedSetupFlowSelector(onboarding: OnyxValue): boolean { + // onboarding is an array for old accounts and accounts created from olddot + if (Array.isArray(onboarding)) { + return true; + } + return onboarding?.hasCompletedGuidedSetupFlow ?? false; +} + +export default hasCompletedGuidedSetupFlowSelector; diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index f5bd14ed7aa1..52e2d817e6db 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -12,7 +12,6 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -46,7 +45,9 @@ function BaseOnboardingPersonalDetails({ const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false); const {accountID} = useSession(); - useDisableModalDismissOnEscape(); + useEffect(() => { + Welcome.setOnboardingErrorMessage(''); + }, []); const completeEngagement = useCallback( (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 03a4b790bc5f..7304c1822ae9 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; @@ -13,7 +13,6 @@ import MenuItemList from '@components/MenuItemList'; import OfflineIndicator from '@components/OfflineIndicator'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import Text from '@components/Text'; -import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useTheme from '@hooks/useTheme'; @@ -28,7 +27,8 @@ import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps} from './types'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import type {BaseOnboardingPurposeProps} from './types'; const menuIcons = { [CONST.ONBOARDING_CHOICES.EMPLOYER]: Illustrations.ReceiptUpload, @@ -38,15 +38,15 @@ const menuIcons = { [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: Illustrations.Binoculars, }; -function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, onboardingPurposeSelected}: BaseOnboardingPurposeProps) { +function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight}: BaseOnboardingPurposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useOnboardingLayout(); const [selectedPurpose, setSelectedPurpose] = useState(undefined); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const theme = useTheme(); - - useDisableModalDismissOnEscape(); + const [onboardingPurposeSelected, onboardingPurposeSelectedResult] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); + const [onboardingErrorMessage, onboardingErrorMessageResult] = useOnyx(ONYXKEYS.ONBOARDING_ERROR_MESSAGE); const PurposeFooterInstance = ; @@ -83,8 +83,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS); }, [selectedPurpose]); - const [errorMessage, setErrorMessage] = useState(''); - const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => { const translationKey = `onboarding.purpose.${choice}` as const; const isSelected = selectedPurpose === choice; @@ -103,7 +101,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on numberOfLinesTitle: 0, onPress: () => { Welcome.setOnboardingPurposeSelected(choice); - setErrorMessage(''); + Welcome.setOnboardingErrorMessage(''); }, }; }); @@ -111,15 +109,18 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on const handleOuterClick = useCallback(() => { if (!selectedPurpose) { - setErrorMessage(translate('onboarding.purpose.errorSelection')); + Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorSelection')); } else { - setErrorMessage(translate('onboarding.purpose.errorContinue')); + Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorContinue')); } - }, [selectedPurpose, setErrorMessage, translate]); + }, [selectedPurpose, translate]); const onboardingLocalRef = useRef(null); useImperativeHandle(isFocused ? OnboardingRefManager.ref : onboardingLocalRef, () => ({handleOuterClick}), [handleOuterClick]); + if (isLoadingOnyxValue(onboardingPurposeSelectedResult, onboardingErrorMessageResult)) { + return null; + } return ( {({safeAreaPaddingBottomStyle}) => ( @@ -148,14 +149,14 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on buttonText={translate('common.continue')} onSubmit={() => { if (!selectedPurpose) { - setErrorMessage(translate('onboarding.purpose.errorSelection')); + Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorSelection')); return; } - setErrorMessage(''); + Welcome.setOnboardingErrorMessage(''); saveAndNavigate(); }} - message={errorMessage} - isAlertVisible={!!errorMessage} + message={onboardingErrorMessage} + isAlertVisible={!!onboardingErrorMessage} containerStyles={[styles.w100, styles.mb5, styles.mh0, paddingHorizontal]} /> @@ -166,10 +167,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on BaseOnboardingPurpose.displayName = 'BaseOnboardingPurpose'; -export default withOnyx({ - onboardingPurposeSelected: { - key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, - }, -})(BaseOnboardingPurpose); +export default BaseOnboardingPurpose; export type {BaseOnboardingPurposeProps}; diff --git a/src/pages/OnboardingPurpose/types.ts b/src/pages/OnboardingPurpose/types.ts index 8c8f11503f1a..17970dbab9a6 100644 --- a/src/pages/OnboardingPurpose/types.ts +++ b/src/pages/OnboardingPurpose/types.ts @@ -1,20 +1,11 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import type {OnboardingPurposeType} from '@src/CONST'; - type OnboardingPurposeProps = Record; -type BaseOnboardingPurposeOnyxProps = { - /** Saved onboarding purpose selected by the user */ - onboardingPurposeSelected: OnyxEntry; -}; - -type BaseOnboardingPurposeProps = OnboardingPurposeProps & - BaseOnboardingPurposeOnyxProps & { - /* Whether to use native styles tailored for native devices */ - shouldUseNativeStyles: boolean; +type BaseOnboardingPurposeProps = OnboardingPurposeProps & { + /* Whether to use native styles tailored for native devices */ + shouldUseNativeStyles: boolean; - /** Whether to use the maxHeight (true) or use the 100% of the height (false) */ - shouldEnableMaxHeight: boolean; - }; + /** Whether to use the maxHeight (true) or use the 100% of the height (false) */ + shouldEnableMaxHeight: boolean; +}; -export type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps, OnboardingPurposeProps}; +export type {BaseOnboardingPurposeProps, OnboardingPurposeProps}; diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index 9b8824300d30..14f9223f6c67 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -9,7 +9,6 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -33,8 +32,6 @@ function BaseOnboardingWork({shouldUseNativeStyles, onboardingPurposeSelected, o const {isSmallScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useOnboardingLayout(); - useDisableModalDismissOnEscape(); - const completeEngagement = useCallback( (values: FormOnyxValues<'onboardingWorkForm'>) => { if (!onboardingPurposeSelected) { diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index b10cae2e7736..ca30eb10b065 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -39,6 +39,10 @@ jest.mock('../../src/components/ConfirmedRoute.tsx'); TestHelper.setupApp(); TestHelper.setupGlobalFetchMock(); +beforeEach(() => { + Onyx.set(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); +}); + function scrollUpToRevealNewMessagesBadge() { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); fireEvent.scroll(screen.getByLabelText(hintText), {