From 5d827df08c19c38177119d7a6e42eb64c224ce5f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:17:22 +0000 Subject: [PATCH] Add onboarding bypass flag V2 version2 version 2 (#5151) ## Summary - add a shared hook that honors a `bypassOnboarding` query parameter and marks onboarding steps as completed for the session - block onboarding orchestrator and UI elements when the bypass flag is present so tours and popups stay hidden ## Testing - ./gradlew build ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_693059f866a8832891dd97f3d52ca5a0) --- .../core/components/onboarding/Onboarding.tsx | 6 ++ .../orchestrator/useOnboardingOrchestrator.ts | 5 +- .../onboarding/useBypassOnboarding.ts | 70 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 frontend/src/core/components/onboarding/useBypassOnboarding.ts diff --git a/frontend/src/core/components/onboarding/Onboarding.tsx b/frontend/src/core/components/onboarding/Onboarding.tsx index eb752ec6a..e851ee60b 100644 --- a/frontend/src/core/components/onboarding/Onboarding.tsx +++ b/frontend/src/core/components/onboarding/Onboarding.tsx @@ -6,6 +6,7 @@ import { isAuthRoute } from '@app/constants/routes'; import { dispatchTourState } from '@app/constants/events'; import { useOnboardingOrchestrator } from '@app/components/onboarding/orchestrator/useOnboardingOrchestrator'; import { markStepSeen } from '@app/components/onboarding/orchestrator/onboardingStorage'; +import { useBypassOnboarding } from '@app/components/onboarding/useBypassOnboarding'; import OnboardingTour, { type AdvanceArgs, type CloseArgs } from '@app/components/onboarding/OnboardingTour'; import OnboardingModalSlide from '@app/components/onboarding/OnboardingModalSlide'; import { @@ -29,6 +30,7 @@ export default function Onboarding() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); + const bypassOnboarding = useBypassOnboarding(); const { state, actions } = useOnboardingOrchestrator(); const serverExperience = useServerExperience(); const onAuthRoute = isAuthRoute(location.pathname); @@ -227,6 +229,10 @@ export default function Onboarding() { return modalSlides.findIndex((step) => step.id === currentStep.id); }, [activeFlow, currentStep]); + if (bypassOnboarding) { + return null; + } + if (onAuthRoute) { return null; } diff --git a/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts b/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts index 07888b7ce..45d1ef454 100644 --- a/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts +++ b/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts @@ -17,6 +17,7 @@ import { migrateFromLegacyPreferences, } from '@app/components/onboarding/orchestrator/onboardingStorage'; import { accountService } from '@app/services/accountService'; +import { useBypassOnboarding } from '@app/components/onboarding/useBypassOnboarding'; const AUTH_ROUTES = ['/login', '/signup', '/auth', '/invite']; const SESSION_TOUR_REQUESTED = 'onboarding::session::tour-requested'; @@ -142,6 +143,7 @@ export function useOnboardingOrchestrator( const serverExperience = useServerExperience(); const { config, loading: configLoading } = useAppConfig(); const location = useLocation(); + const bypassOnboarding = useBypassOnboarding(); const [runtimeState, setRuntimeState] = useState(() => getInitialRuntimeState(defaultState) @@ -213,7 +215,8 @@ export function useOnboardingOrchestrator( const isOnAuthRoute = AUTH_ROUTES.some((route) => location.pathname.startsWith(route)); const loginEnabled = config?.enableLogin === true; const isUnauthenticatedWithLoginEnabled = loginEnabled && !hasAuthToken(); - const shouldBlockOnboarding = isOnAuthRoute || configLoading || isUnauthenticatedWithLoginEnabled; + const shouldBlockOnboarding = + bypassOnboarding || isOnAuthRoute || configLoading || isUnauthenticatedWithLoginEnabled; const conditionContext = useMemo(() => ({ ...serverExperience, diff --git a/frontend/src/core/components/onboarding/useBypassOnboarding.ts b/frontend/src/core/components/onboarding/useBypassOnboarding.ts new file mode 100644 index 000000000..8e7d9b14f --- /dev/null +++ b/frontend/src/core/components/onboarding/useBypassOnboarding.ts @@ -0,0 +1,70 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { ONBOARDING_STEPS } from '@app/components/onboarding/orchestrator/onboardingConfig'; +import { markStepSeen } from '@app/components/onboarding/orchestrator/onboardingStorage'; + +const SESSION_KEY = 'onboarding::bypass-all'; +const PARAM_KEY = 'bypassOnboarding'; + +function isTruthy(value: string | null): boolean { + return value?.toLowerCase() === 'true'; +} + +function readStoredBypass(): boolean { + if (typeof window === 'undefined') return false; + try { + return sessionStorage.getItem(SESSION_KEY) === 'true'; + } catch { + return false; + } +} + +function setStoredBypass(enabled: boolean): void { + if (typeof window === 'undefined') return; + try { + if (enabled) { + sessionStorage.setItem(SESSION_KEY, 'true'); + } else { + sessionStorage.removeItem(SESSION_KEY); + } + } catch { + // Ignore storage errors to avoid blocking the bypass flow + } +} + +/** + * Detects the `bypassOnboarding` query parameter and stores it in session storage + * so that onboarding remains disabled while the app is open. Also marks all steps + * as seen to ensure any dependent UI elements remain hidden. + */ +export function useBypassOnboarding(): boolean { + const location = useLocation(); + const [bypassOnboarding, setBypassOnboarding] = useState(() => readStoredBypass()); + const stepsMarkedRef = useRef(false); + + const shouldBypassFromSearch = useMemo(() => { + try { + const params = new URLSearchParams(location.search); + return isTruthy(params.get(PARAM_KEY)); + } catch { + return false; + } + }, [location.search]); + + useEffect(() => { + const fromStorage = readStoredBypass(); + const nextBypass = shouldBypassFromSearch || fromStorage; + setBypassOnboarding(nextBypass); + if (nextBypass) { + setStoredBypass(true); + } + }, [shouldBypassFromSearch]); + + useEffect(() => { + if (!bypassOnboarding || stepsMarkedRef.current) return; + stepsMarkedRef.current = true; + ONBOARDING_STEPS.forEach((step) => markStepSeen(step.id)); + }, [bypassOnboarding]); + + return bypassOnboarding; +}