Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e2fa574bd | |||
| d79da8cd6a | |||
| 10141806dc | |||
| 08ef85869f | |||
| 619ec4c9b1 | |||
| 66cd8953ba | |||
| 11931cbf6f | |||
| 43f9a3eb83 | |||
| f5ff7bf6ea | |||
| 3e79b3bf31 | |||
| e08fa357f7 | |||
| e37ef375eb | |||
| 66118ff63f | |||
| 6d0c8890d3 | |||
| e9ec33d412 | |||
| 2a653b807b |
@@ -41,3 +41,4 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
.vercel
|
||||
.env*.local
|
||||
|
||||
Generated
+780
-20
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -9,7 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abe-kap/appshell-sdk": "^0.2.3",
|
||||
"@abe-kap/appshell-sdk": "^0.2.4",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
|
||||
@@ -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 <main className="portal-main"><span className="portal-grid" /></main>;
|
||||
|
||||
return (
|
||||
<main className="portal-main">
|
||||
<span className="portal-grid" />
|
||||
|
||||
@@ -24,7 +24,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export function LoginFlow() {
|
||||
const router = useRouter();
|
||||
const { login, loginWithOAuth, completeOAuthLogin } = useAuth();
|
||||
const { login, loginWithOAuth, completeOAuthLogin, getUserEmail } = useAuth();
|
||||
const { ready, sdk } = useAppShell();
|
||||
const [step, setStep] = useState<Step>("identify");
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -86,13 +86,16 @@ export function LoginFlow() {
|
||||
registered = !!st?.registered;
|
||||
} catch { registered = false; }
|
||||
window.history.replaceState({}, "", "/portal/login");
|
||||
router.replace(registered ? "/dashboard" : "/portal/onboarding");
|
||||
if (registered) { router.replace("/dashboard"); return; }
|
||||
// Capture the verified email now (session is fresh) so onboarding prefills it.
|
||||
try { const em = await getUserEmail(); if (em) sessionStorage.setItem("onboard_email", em); } catch { /* ignore */ }
|
||||
router.replace("/portal/onboarding");
|
||||
})
|
||||
.catch(() => {
|
||||
setFlash("Google sign-in didn't complete. Please try again.");
|
||||
replace("identify");
|
||||
});
|
||||
}, [ready, completeOAuthLogin, router, sdk]);
|
||||
}, [ready, completeOAuthLogin, router, sdk, getUserEmail]);
|
||||
|
||||
/* ---- auth resolution ---- */
|
||||
function afterAuth(factor: "password" | "passkey" | "otp" | "totp" | "push" | "social") {
|
||||
|
||||
@@ -54,9 +54,11 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
|
||||
const [emailVerified, setEmailVerified] = useState(false);
|
||||
const [phoneVerified, setPhoneVerified] = useState(false);
|
||||
|
||||
// Onboarding: prefill the verified email from the Google session (the ACE omits it).
|
||||
// Onboarding: prefill the verified email — from the session-storage hint the login
|
||||
// page stashed at OAuth time, then confirmed via the live Supabase session.
|
||||
useEffect(() => {
|
||||
if (!onboard) return;
|
||||
try { const cached = sessionStorage.getItem("onboard_email"); if (cached) setEmail(cached); } catch { /* ignore */ }
|
||||
getUserEmail().then((e) => { if (e) setEmail(e); }).catch(() => { /* ignore */ });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onboard]);
|
||||
@@ -118,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");
|
||||
}
|
||||
|
||||
@@ -205,12 +209,13 @@ function StepAccount(p: {
|
||||
<h1>Complete your profile</h1>
|
||||
<p className="sub">Tell us a bit about you to set up your account.</p>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{p.sso ? (
|
||||
<div className="conn-banner conn-green"><Icon name="check" size={16} /> Connected with {cap(p.sso.provider)} · {p.sso.email}</div>
|
||||
) : (
|
||||
<div className="conn-banner conn-blue"><Icon name="mail" size={16} /> {p.onboard ? "Signed in as" : "Creating account for"} {p.email}</div>
|
||||
)}
|
||||
<div className="field" style={{ marginTop: 16 }}>
|
||||
<label className="label">Email address</label>
|
||||
<div className="input-wrap">
|
||||
<span className="input-ico"><Icon name="mail" size={17} /></span>
|
||||
<input className="input" type="email" value={p.email} disabled readOnly aria-label="Email address" />
|
||||
</div>
|
||||
<span className="faint" style={{ fontSize: 12 }}>{p.onboard ? "Verified with Google — this can't be changed." : "The email you're registering with."}</span>
|
||||
</div>
|
||||
|
||||
<div className="row gap-3" style={{ marginTop: 18, alignItems: "center" }}>
|
||||
|
||||
Reference in New Issue
Block a user