Skip to content

Commit f265e7a

Browse files
luacmartinsOSBotify
authored andcommitted
Merge pull request #89071 from software-mansion-labs/kuba_nowakowski/bugfix/tab_navi_blockers
fix onboarding modal from OD and bac button flow (cherry picked from commit 7c1f47e) (cherry-picked to staging by luacmartins)
1 parent fd7bed3 commit f265e7a

6 files changed

Lines changed: 55 additions & 17 deletions

File tree

src/hooks/useOnboardingFlow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ function useOnboardingFlowRouter() {
8282
}
8383
}
8484

85-
// Skip onboarding for migrated users or users who were invited/have workspace policies
8685
const isMigratedUser = hasBeenAddedToNudgeMigration ?? false;
8786
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
8887
const isInvitedOrGroupMember = (!CONFIG.IS_HYBRID_APP && (hasNonPersonalPolicy || wasInvitedToNewDot)) ?? false;
89-
if (isMigratedUser || isInvitedOrGroupMember) {
88+
// OD signup sets inviteType + creates a workspace, so invited/group members can still need NewDot onboarding.
89+
if (isMigratedUser || (isInvitedOrGroupMember && isOnboardingCompleted)) {
9090
return;
9191
}
9292

src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/index.ios.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import type {NavigationRoute, SplitNavigatorName} from '@libs/Navigation/types';
66
import NAVIGATORS from '@src/NAVIGATORS';
77
import ensureTabNavigatorRoutes from './ensureTabNavigatorRoutes';
88

9+
// TODO: deploy-blocker hotfix for #89006 — slicing tab state breaks rehydration on iOS swipe-back.
10+
// Skipping the slice until we exceed the threshold preserves the underlying TAB_NAVIGATOR's nested
11+
// state so the gesture restores it correctly. This is a temporary fix; the proper solution is to
12+
// rebuild the TAB_NAVIGATOR state from `preservedNavigatorStates` on rehydration so the magic
13+
// number can go away. We'll address this shortly — tracked in https://github.com/Expensify/App/issues/89179.
14+
const SKIP_SLICE_TAB_THRESHOLD = 4;
15+
916
// Swiping back on iOS does not work properly when the preloaded route has gestureEnabled set to false.
1017
// Therefore, on screens where swiping should work, preloadedRoutes will be an empty array during rendering to ensure swiping works properly.
1118
// Once this bug is fixed, this file should be deleted and index.android.ts renamed to index.native.ts.
@@ -35,12 +42,19 @@ function getShouldHidePreloadedRoutes(route?: NavigationRoute) {
3542
// This is an optimization to keep mounted only last few screens in the stack.
3643
// On native platforms, we store the last two routes to handle swiping back.
3744
export default function useCustomRootStackNavigatorState({state}: CustomStateHookProps) {
38-
const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name));
39-
const indexToSlice = Math.max(0, lastSplitIndex - 1);
40-
const slicedRoutes = state.routes.slice(indexToSlice, state.routes.length);
41-
const routesToRender = ensureTabNavigatorRoutes(slicedRoutes, indexToSlice, state.routes);
45+
const tabCount = state.routes.reduce((acc, route) => (route.name === NAVIGATORS.TAB_NAVIGATOR ? acc + 1 : acc), 0);
46+
47+
let stateToRender: typeof state;
48+
if (tabCount <= SKIP_SLICE_TAB_THRESHOLD) {
49+
stateToRender = state;
50+
} else {
51+
const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name));
52+
const indexToSlice = Math.max(0, lastSplitIndex - 1);
53+
const slicedRoutes = state.routes.slice(indexToSlice, state.routes.length);
54+
const routesToRender = ensureTabNavigatorRoutes(slicedRoutes, indexToSlice, state.routes);
55+
stateToRender = {...state, routes: routesToRender, index: routesToRender.length - 1};
56+
}
4257

43-
const stateToRender = {...state, routes: routesToRender, index: routesToRender.length - 1};
4458
if (getShouldHidePreloadedRoutes(stateToRender.routes.at(-1))) {
4559
return {...stateToRender, preloadedRoutes: []};
4660
}

src/libs/Navigation/Navigation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,14 @@ function goUp(backToRoute: Route, options?: GoBackOptions) {
483483
return;
484484
}
485485

486+
// For TAB_NAVIGATOR targets, use POP_TO so the underlying tab's nested state is restored from the
487+
// payload — plain pop can leave the active tab pointing at Home instead of the intended target.
488+
// Issue: https://github.com/Expensify/App/issues/89006
489+
if ((minimalAction.payload as {name?: string} | undefined)?.name === NAVIGATORS.TAB_NAVIGATOR) {
490+
navigationRef.current.dispatch({...minimalAction, type: CONST.NAVIGATION.ACTION_TYPE.POP_TO, target: targetState.key});
491+
return;
492+
}
493+
486494
navigationRef.current.dispatch({...StackActions.pop(distanceToPop), target: targetState.key});
487495
}
488496

src/libs/Navigation/guards/OnboardingGuard.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,7 @@ const OnboardingGuard: NavigationGuard = {
184184
const isNavigatingWithReplace = isNavigatingToOnboardingFlowWithReplaceAction(action);
185185

186186
const shouldSkipOnboarding =
187-
skipOnboardingConfig ||
188-
isLoading ||
189-
isTransitioning ||
190-
isOnboardingCompleted ||
191-
isMigratedUser ||
192-
isSingleEntry ||
193-
needsExplanationModal ||
194-
isInvitedOrGroupMember ||
195-
isNavigatingWithReplace;
187+
skipOnboardingConfig || isLoading || isTransitioning || isOnboardingCompleted || isMigratedUser || isSingleEntry || needsExplanationModal || isNavigatingWithReplace;
196188

197189
if (shouldSkipOnboarding) {
198190
return {type: 'ALLOW'};

src/libs/Navigation/helpers/linkTo/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ export default function linkTo(navigation: NavigationContainerRef<RootNavigatorP
203203
}
204204

205205
// If we deep link to a RHP page, we want to make sure we have the correct full screen route under the overlay.
206-
if (shouldCheckFullScreenRouteMatching(action)) {
206+
// Skip when current top is already RHP — the underlying tab is already in place, and the extra dispatch
207+
// would corrupt the navigation state. Issue: https://github.com/Expensify/App/issues/89006
208+
if (shouldCheckFullScreenRouteMatching(action) && currentState.routes[currentState.index]?.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
207209
const newFocusedRoute = findFocusedRoute(stateFromPath);
208210
if (newFocusedRoute) {
209211
// getMatchingFullScreenRoute returns a TAB_NAVIGATOR wrapper; unwrap it to get the

tests/unit/Navigation/guards/OnboardingGuard.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,28 @@ describe('OnboardingGuard', () => {
362362
expect(result.type).toBe('REDIRECT');
363363
expect(result.route).toContain('onboarding');
364364
});
365+
366+
it('should redirect invited or group members when they have not completed onboarding', async () => {
367+
// Given an invited user from OD signup who has not completed the NewDot guided setup
368+
await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
369+
hasCompletedGuidedSetupFlow: false,
370+
});
371+
await Onyx.merge(ONYXKEYS.NVP_INTRO_SELECTED, {
372+
choice: CONST.INTRO_CHOICES.SUBMIT,
373+
});
374+
await Onyx.merge(ONYXKEYS.HAS_NON_PERSONAL_POLICY, true);
375+
await Onyx.merge(ONYXKEYS.ACCOUNT, {
376+
isFromPublicDomain: true,
377+
});
378+
await waitForBatchedUpdates();
379+
380+
// When the guard evaluates on a non-onboarding screen
381+
const result = OnboardingGuard.evaluate(mockState, mockAction, authenticatedContext) as {type: 'REDIRECT'; route: string};
382+
383+
// Then redirect to onboarding
384+
expect(result.type).toBe('REDIRECT');
385+
expect(result.route).toContain('onboarding');
386+
});
365387
});
366388

367389
describe('infinite loop prevention (APP-7FR)', () => {

0 commit comments

Comments
 (0)