From d79da8cd6a7fe91cefd5b9966fc3daa91b216d4f Mon Sep 17 00:00:00 2001 From: tanweer919 Date: Mon, 13 Jul 2026 04:01:01 +0530 Subject: [PATCH] fix(onboarding): gate direct access behind the OAuth handoff /portal/onboarding is only a step in the Google sign-in flow. Redirect to login unless the login page's onboard_email handoff hint is present (and the session is authenticated), instead of showing an empty form on direct/typed URL access. Hold rendering until the check passes to avoid a form flash, and clear the hint once onboarding completes so it can't be reused. --- src/app/portal/onboarding/page.tsx | 24 ++++++++++++++++++++---- src/components/portal/register-flow.tsx | 2 ++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/portal/onboarding/page.tsx b/src/app/portal/onboarding/page.tsx index 1d7516e..712098d 100644 --- a/src/app/portal/onboarding/page.tsx +++ b/src/app/portal/onboarding/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react"; import { PortalAside, PanelBrand } from "@/components/portal/parts"; @@ -10,18 +10,34 @@ import { isShellConfigured } from "@/lib/appshell"; /** * Post-OAuth onboarding — a first-time Google user completes their CRM profile - * (everything except email, which Google already verified). Only reachable while - * authenticated; unauthenticated visitors are sent back to sign in. + * (everything except email, which Google already verified). + * + * This screen is ONLY a step in the OAuth → onboarding handoff: the login page + * stashes the verified email in `sessionStorage` right before routing here. Any + * other way in — a typed URL, a fresh tab, a lost session — has no handoff hint + * (and possibly no session), so we bounce back to sign in rather than showing an + * empty form. */ export default function OnboardingPage() { const router = useRouter(); const { status } = useAuth(); const { ready } = useAppShell(); + const [allowed, setAllowed] = useState(false); useEffect(() => { - if (isShellConfigured() && ready && status === "unauthenticated") router.replace("/portal/login"); + if (!isShellConfigured()) { setAllowed(true); return; } // local dev without the Shell + if (!ready) return; // wait for session restore + let hasHandoff = false; + try { hasHandoff = !!sessionStorage.getItem("onboard_email"); } catch { /* ignore */ } + if (status === "unauthenticated" || !hasHandoff) { + router.replace("/portal/login"); + return; + } + setAllowed(true); }, [ready, status, router]); + if (!allowed) return
; + return (
diff --git a/src/components/portal/register-flow.tsx b/src/components/portal/register-flow.tsx index 762457e..c498991 100644 --- a/src/components/portal/register-flow.tsx +++ b/src/components/portal/register-flow.tsx @@ -120,6 +120,8 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa // register mode: non-fatal — the auth account exists; the profile can be filled in later. } } + // Consume the OAuth→onboarding handoff so a stale hint can't re-open onboarding. + try { sessionStorage.removeItem("onboard_email"); } catch { /* ignore */ } router.push("/dashboard"); } -- 2.52.0