forked from Goutam/lynkeduppro-crm
103f3ae3a1
- Registration no longer asks for a role/persona (removed the role select + allottee block); crm.account.register sends no persona. - New users have no permissions → the sidebar now shows only Dashboard + Profile for them, gated on crm.account.me (membership + permissions). Members see the areas their permissions allow. - Add /portal/invite?token=… : accepts the invite for a signed-in registered user, or routes an unregistered invitee through register/onboarding, which redeems the stashed token on completion (granting the invited role). - Team Management: 'Copy link' on pending invites builds the invite link from the invitation token (no email delivery yet).
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
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";
|
|
import { CookieBanner, Spinner } from "@/components/portal/bits";
|
|
import { isShellConfigured } from "@/lib/appshell";
|
|
|
|
/**
|
|
* Team invitation landing page (/portal/invite?token=…). Redeems the invite:
|
|
* - not signed in → send to registration; it redeems the stashed token on finish.
|
|
* - signed in but no CRM profile → send to onboarding; it redeems on finish.
|
|
* - signed in + registered → accept now (creates the membership with the invited role).
|
|
* The token (a 128-hex secret emailed to the invitee) is the authorization.
|
|
*/
|
|
export default function InvitePage() {
|
|
const router = useRouter();
|
|
const { status, getUserEmail } = useAuth();
|
|
const { ready, sdk } = useAppShell();
|
|
const [error, setError] = useState("");
|
|
|
|
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; }
|
|
try { sessionStorage.setItem("invite_token", token); } catch { /* ignore */ }
|
|
|
|
// No Shell (local/mock) → just go register.
|
|
if (!isShellConfigured()) { router.replace("/portal/register"); return; }
|
|
if (!ready) return;
|
|
|
|
let cancelled = false;
|
|
(async () => {
|
|
if (status === "unauthenticated") {
|
|
router.replace("/portal/register"); // sign up first; finish() redeems the token
|
|
return;
|
|
}
|
|
// Authenticated: registered users accept immediately; profile-less users onboard.
|
|
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"); } catch { /* ignore */ }
|
|
router.replace("/dashboard");
|
|
} catch {
|
|
if (!cancelled) setError("We couldn't accept this invitation — it may have expired or already been used.");
|
|
}
|
|
})();
|
|
return () => { cancelled = true; };
|
|
}, [ready, status, router, sdk, getUserEmail]);
|
|
|
|
return (
|
|
<main className="portal-main">
|
|
<span className="portal-grid" />
|
|
<div className="portal-split anim-in">
|
|
<PortalAside />
|
|
<section className="portal-panel">
|
|
<div style={{ width: "100%", maxWidth: 420 }}>
|
|
<PanelBrand />
|
|
<div className="card anim-fade-up" style={{ textAlign: "center" }}>
|
|
{error ? (
|
|
<>
|
|
<h1>Invitation problem</h1>
|
|
<p className="sub">{error}</p>
|
|
<button className="btn btn-primary" style={{ marginTop: 16 }} onClick={() => router.replace("/portal/login")}>
|
|
Go to sign in
|
|
</button>
|
|
</>
|
|
) : (
|
|
<div className="interstitial">
|
|
<Spinner lg />
|
|
<h1 style={{ fontSize: 20, marginTop: 8 }}>Accepting your invitation…</h1>
|
|
<p className="sub">Setting up your team access.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<CookieBanner />
|
|
</main>
|
|
);
|
|
}
|