"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth, useAppShell } from "@abe-kap/appshell-sdk/react"; import { PortalAside, PanelBrand } from "@/components/portal/parts"; import { CookieBanner, Spinner } from "@/components/portal/bits"; import { isShellConfigured } from "@/lib/appshell"; interface InviteInfo { ok: boolean; status?: string; email?: string; roleNames?: string[]; hasAccount?: boolean } /** * Team invitation landing page (/portal/invite?token=…). It looks the token up * (public, pre-auth) to learn the invited email and whether an account exists, then: * - signed in + registered → accept now (creates the membership with the invited role); * - signed in, no CRM profile → onboarding, which redeems the token on finish; * - not signed in + email already has an account → sign in (email prefilled); * - not signed in + first-time invitee → register (email prefilled + locked). * The register/login/onboarding flows redeem the stashed token on completion. */ export default function InvitePage() { const router = useRouter(); const { status, getUserEmail } = useAuth(); const { ready, sdk } = useAppShell(); const [error, setError] = useState(""); const started = useRef(false); useEffect(() => { let token = ""; try { token = new URLSearchParams(window.location.search).get("token") ?? ""; } catch { /* ignore */ } if (!token) { setError("This invitation link is invalid or incomplete."); return; } if (!isShellConfigured()) { try { sessionStorage.setItem("invite_token", token); } catch { /* ignore */ } router.replace("/portal/register"); return; } if (!ready || started.current) return; started.current = true; (async () => { try { sessionStorage.setItem("invite_token", token); } catch { /* ignore */ } // Look the invitation up (pre-auth) to get the email + account state. let info: InviteInfo = { ok: false }; try { const res = await fetch(`/api/invite/lookup?token=${encodeURIComponent(token)}`, { cache: "no-store" }); info = await res.json(); } catch { /* treat as unavailable below */ } if (!info.ok) { const msg = info.status === "expired" ? "This invitation has expired. Ask for a new one." : info.status === "accepted" ? "This invitation has already been used." : info.status === "revoked" ? "This invitation was revoked." : "This invitation link is invalid."; setError(msg); try { sessionStorage.removeItem("invite_token"); } catch { /* ignore */ } return; } if (info.email) { try { sessionStorage.setItem("invite_email", info.email); } catch { /* ignore */ } } // Signed in already: accept if registered, else finish onboarding first. if (status === "authenticated") { try { const st = await sdk.query<{ registered: boolean }>("crm.account.registrationStatus"); if (!st?.registered) { try { const em = await getUserEmail(); if (em) sessionStorage.setItem("onboard_email", em); } catch { /* ignore */ } router.replace("/portal/onboarding"); return; } } catch { /* fall through to accept */ } try { await sdk.command("crm.team.invitation.accept", { token }); try { sessionStorage.removeItem("invite_token"); sessionStorage.removeItem("invite_email"); } catch { /* ignore */ } router.replace("/dashboard"); } catch { setError("We couldn't accept this invitation — it may have expired or already been used."); } return; } // Not signed in: existing account → sign in; first-time invitee → register. router.replace(info.hasAccount ? "/portal/login" : "/portal/register"); })(); }, [ready, status, router, sdk, getUserEmail]); return (
{error ? ( <>

Invitation problem

{error}

) : (

Checking your invitation…

One moment while we set things up.

)}
); }