From 0d11206e9beeee06b3d5a1176782b612409aeac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Rejdak?= Date: Thu, 30 Apr 2026 16:12:06 +0200 Subject: [PATCH 1/3] transition tracker in useOnboardingFlow --- src/hooks/useOnboardingFlow.ts | 120 +++++++++++++++++---------------- 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts index b523c2cfd3eb..2460132644c1 100644 --- a/src/hooks/useOnboardingFlow.ts +++ b/src/hooks/useOnboardingFlow.ts @@ -2,10 +2,10 @@ import {isSingleNewDotEntrySelector} from '@selectors/HybridApp'; import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector, wasInvitedToNewDotSelector} from '@selectors/Onboarding'; import {emailSelector} from '@selectors/Session'; import {useEffect} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager} from 'react-native'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import Navigation from '@libs/Navigation/Navigation'; +// eslint-disable-next-line no-restricted-imports +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {isLoggingInAsNewUser} from '@libs/SessionUtils'; import {startOnboardingFlow} from '@userActions/Welcome/OnboardingFlow'; import CONFIG from '@src/CONFIG'; @@ -47,68 +47,70 @@ function useOnboardingFlowRouter() { const isOnboardingCompleted = hasCompletedGuidedSetupFlowSelector(onboardingValues); // This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes - // eslint-disable-next-line @typescript-eslint/no-deprecated - const handle = InteractionManager.runAfterInteractions(() => { - // Prevent showing onboarding if we are logging in as a new user with short lived token - if (currentUrl?.includes(ROUTES.TRANSITION_BETWEEN_APPS) && isLoggingInAsNewSessionUser) { - return; - } - - if (isLoadingApp !== false || isOnboardingLoading) { - return; - } - - if (isLoadingOnyxValue(isOnboardingCompletedMetadata, tryNewDotMetadata, dismissedProductTrainingMetadata)) { - return; - } - - if (CONFIG.IS_HYBRID_APP && isLoadingOnyxValue(isSingleNewDotEntryMetadata)) { - return; - } - - if (currentUrl.endsWith('/r')) { - // Don't trigger onboarding if we are in the middle of a redirect to a report - return; - } - - if (CONFIG.IS_HYBRID_APP) { - // For single entries, such as using the Travel feature from OldDot, we don't want to show onboarding - if (isSingleNewDotEntry) { + + const handle = TransitionTracker.runAfterTransitions({ + callback: () => { + // Prevent showing onboarding if we are logging in as a new user with short lived token + if (currentUrl?.includes(ROUTES.TRANSITION_BETWEEN_APPS) && isLoggingInAsNewSessionUser) { return; } - // When user is transitioning from OldDot to NewDot, we usually show the explanation modal - if (isHybridAppOnboardingCompleted === false) { - Navigation.navigate(ROUTES.EXPLANATION_MODAL_ROOT); + if (isLoadingApp !== false || isOnboardingLoading) { + return; } - } - - const isMigratedUser = hasBeenAddedToNudgeMigration ?? false; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isInvitedOrGroupMember = (!CONFIG.IS_HYBRID_APP && (hasNonPersonalPolicy || wasInvitedToNewDot)) ?? false; - // OD signup sets inviteType + creates a workspace, so invited/group members can still need NewDot onboarding. - if (isMigratedUser || (isInvitedOrGroupMember && isOnboardingCompleted)) { - return; - } - - // Explicitly start the onboarding flow when onboarding is not completed. - // We use startOnboardingFlow (which calls resetRoot) instead of Navigation.navigate because - // navigate goes through the router where OnboardingGuard would block the navigation. - // waitForProtectedRoutes ensures navigation is ready, which is critical during fresh login. - // Skip when HybridApp explanation modal is active (OldDot-transitioning users). - if (isOnboardingCompleted === false && !(CONFIG.IS_HYBRID_APP && isHybridAppOnboardingCompleted === false)) { - Navigation.waitForProtectedRoutes().then(() => { - startOnboardingFlow({ - onboardingValuesParam: onboardingValues ?? undefined, - isUserFromPublicDomain: !!account?.isFromPublicDomain, - hasAccessiblePolicies: !!account?.hasAccessibleDomainPolicies, - currentOnboardingCompanySize: onboardingCompanySize, - currentOnboardingPurposeSelected: onboardingPurposeSelected, - onboardingInitialPath, - onboardingValues, + + if (isLoadingOnyxValue(isOnboardingCompletedMetadata, tryNewDotMetadata, dismissedProductTrainingMetadata)) { + return; + } + + if (CONFIG.IS_HYBRID_APP && isLoadingOnyxValue(isSingleNewDotEntryMetadata)) { + return; + } + + if (currentUrl.endsWith('/r')) { + // Don't trigger onboarding if we are in the middle of a redirect to a report + return; + } + + if (CONFIG.IS_HYBRID_APP) { + // For single entries, such as using the Travel feature from OldDot, we don't want to show onboarding + if (isSingleNewDotEntry) { + return; + } + + // When user is transitioning from OldDot to NewDot, we usually show the explanation modal + if (isHybridAppOnboardingCompleted === false) { + Navigation.navigate(ROUTES.EXPLANATION_MODAL_ROOT); + } + } + + const isMigratedUser = hasBeenAddedToNudgeMigration ?? false; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const isInvitedOrGroupMember = (!CONFIG.IS_HYBRID_APP && (hasNonPersonalPolicy || wasInvitedToNewDot)) ?? false; + // OD signup sets inviteType + creates a workspace, so invited/group members can still need NewDot onboarding. + if (isMigratedUser || (isInvitedOrGroupMember && isOnboardingCompleted)) { + return; + } + + // Explicitly start the onboarding flow when onboarding is not completed. + // We use startOnboardingFlow (which calls resetRoot) instead of Navigation.navigate because + // navigate goes through the router where OnboardingGuard would block the navigation. + // waitForProtectedRoutes ensures navigation is ready, which is critical during fresh login. + // Skip when HybridApp explanation modal is active (OldDot-transitioning users). + if (isOnboardingCompleted === false && !(CONFIG.IS_HYBRID_APP && isHybridAppOnboardingCompleted === false)) { + Navigation.waitForProtectedRoutes().then(() => { + startOnboardingFlow({ + onboardingValuesParam: onboardingValues ?? undefined, + isUserFromPublicDomain: !!account?.isFromPublicDomain, + hasAccessiblePolicies: !!account?.hasAccessibleDomainPolicies, + currentOnboardingCompanySize: onboardingCompanySize, + currentOnboardingPurposeSelected: onboardingPurposeSelected, + onboardingInitialPath, + onboardingValues, + }); }); - }); - } + } + }, }); return () => { From cdb0c6af0d23e590ae4d7e89fac80d6cde26ae81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Rejdak?= Date: Thu, 7 May 2026 15:06:21 +0200 Subject: [PATCH 2/3] transitiontracker - baseOnboardingInterestedFeatures --- .../BaseOnboardingInterestedFeatures.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index 7b3fa91fa36b..7eea56a62020 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -1,7 +1,6 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; import FixedFooter from '@components/FixedFooter'; @@ -32,6 +31,8 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/acti import Log from '@libs/Log'; import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; +// eslint-disable-next-line no-restricted-imports +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -238,11 +239,11 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin betas, }); - // Avoid creating new WS because onboardingPolicyID is cleared before unmounting - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - setOnboardingAdminsChatReportID(); - setOnboardingPolicyID(); + TransitionTracker.runAfterTransitions({ + callback: () => { + setOnboardingAdminsChatReportID(); + setOnboardingPolicyID(); + }, }); // We need to wait the policy is created before navigating out the onboarding flow From f77ef40612d788caedbab936e5c2dd43872f6874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Rejdak?= Date: Mon, 11 May 2026 17:27:28 +0200 Subject: [PATCH 3/3] add flag so transitionTracker runs after navigating --- .../BaseOnboardingInterestedFeatures.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index a5b0471944d8..04087edb88e3 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -246,6 +246,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin setOnboardingAdminsChatReportID(); setOnboardingPolicyID(); }, + waitForUpcomingTransition: true, }); // We need to wait the policy is created before navigating out the onboarding flow