Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c6190d16a | |||
| 6ba00bfbf9 | |||
| 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
|
next-env.d.ts
|
||||||
|
|
||||||
.vercel
|
.vercel
|
||||||
|
.env*.local
|
||||||
|
|||||||
Generated
+780
-20
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abe-kap/appshell-sdk": "^0.2.3",
|
"@abe-kap/appshell-sdk": "^0.2.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.21.0",
|
"lucide-react": "^1.21.0",
|
||||||
"next": "16.2.9",
|
"next": "16.2.9",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react";
|
import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react";
|
||||||
import { PortalAside, PanelBrand } from "@/components/portal/parts";
|
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
|
* Post-OAuth onboarding — a first-time Google user completes their CRM profile
|
||||||
* (everything except email, which Google already verified). Only reachable while
|
* (everything except email, which Google already verified).
|
||||||
* authenticated; unauthenticated visitors are sent back to sign in.
|
*
|
||||||
|
* 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() {
|
export default function OnboardingPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status } = useAuth();
|
const { status } = useAuth();
|
||||||
const { ready } = useAppShell();
|
const { ready } = useAppShell();
|
||||||
|
const [allowed, setAllowed] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
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]);
|
}, [ready, status, router]);
|
||||||
|
|
||||||
|
if (!allowed) return <main className="portal-main"><span className="portal-grid" /></main>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="portal-main">
|
<main className="portal-main">
|
||||||
<span className="portal-grid" />
|
<span className="portal-grid" />
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|||||||
|
|
||||||
export function LoginFlow() {
|
export function LoginFlow() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { login, loginWithOAuth, completeOAuthLogin } = useAuth();
|
const { login, loginWithOAuth, completeOAuthLogin, getUserEmail } = useAuth();
|
||||||
const { ready, sdk } = useAppShell();
|
const { ready, sdk } = useAppShell();
|
||||||
const [step, setStep] = useState<Step>("identify");
|
const [step, setStep] = useState<Step>("identify");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
@@ -86,13 +86,16 @@ export function LoginFlow() {
|
|||||||
registered = !!st?.registered;
|
registered = !!st?.registered;
|
||||||
} catch { registered = false; }
|
} catch { registered = false; }
|
||||||
window.history.replaceState({}, "", "/portal/login");
|
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(() => {
|
.catch(() => {
|
||||||
setFlash("Google sign-in didn't complete. Please try again.");
|
setFlash("Google sign-in didn't complete. Please try again.");
|
||||||
replace("identify");
|
replace("identify");
|
||||||
});
|
});
|
||||||
}, [ready, completeOAuthLogin, router, sdk]);
|
}, [ready, completeOAuthLogin, router, sdk, getUserEmail]);
|
||||||
|
|
||||||
/* ---- auth resolution ---- */
|
/* ---- auth resolution ---- */
|
||||||
function afterAuth(factor: "password" | "passkey" | "otp" | "totp" | "push" | "social") {
|
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 [emailVerified, setEmailVerified] = useState(false);
|
||||||
const [phoneVerified, setPhoneVerified] = 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(() => {
|
useEffect(() => {
|
||||||
if (!onboard) return;
|
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 */ });
|
getUserEmail().then((e) => { if (e) setEmail(e); }).catch(() => { /* ignore */ });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [onboard]);
|
}, [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.
|
// 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");
|
router.push("/dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,12 +209,13 @@ function StepAccount(p: {
|
|||||||
<h1>Complete your profile</h1>
|
<h1>Complete your profile</h1>
|
||||||
<p className="sub">Tell us a bit about you to set up your account.</p>
|
<p className="sub">Tell us a bit about you to set up your account.</p>
|
||||||
|
|
||||||
<div style={{ marginTop: 16 }}>
|
<div className="field" style={{ marginTop: 16 }}>
|
||||||
{p.sso ? (
|
<label className="label">Email address</label>
|
||||||
<div className="conn-banner conn-green"><Icon name="check" size={16} /> Connected with {cap(p.sso.provider)} · {p.sso.email}</div>
|
<div className="input-wrap">
|
||||||
) : (
|
<span className="input-ico"><Icon name="mail" size={17} /></span>
|
||||||
<div className="conn-banner conn-blue"><Icon name="mail" size={16} /> {p.onboard ? "Signed in as" : "Creating account for"} {p.email}</div>
|
<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>
|
||||||
|
|
||||||
<div className="row gap-3" style={{ marginTop: 18, alignItems: "center" }}>
|
<div className="row gap-3" style={{ marginTop: 18, alignItems: "center" }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user