forked from Goutam/lynkeduppro-crm
fix(invite): look up the invitation and route correctly (register vs sign in)
The invite link is token-only, so the landing page couldn't tell the invited email or whether an account existed — it dumped everyone on an empty register screen. Now it calls a public lookup (/api/invite/lookup → be-crm) and: - first-time invitee → register with the email prefilled + locked; - email already registered → sign in with the email prefilled; - signed-in + registered → accept immediately; signed-in + no profile → onboarding. Login now redeems the pending invite after sign-in (password/OTP/OAuth), so an existing user's invite is accepted on login (not only when already logged in).
This commit is contained in:
@@ -34,6 +34,25 @@ export function LoginFlow() {
|
||||
const [remember, setRemember] = useState(false);
|
||||
const [flash, setFlash] = useState<string>("");
|
||||
const [otpChannel, setOtpChannel] = useState<"email" | "sms">("email");
|
||||
const [invited, setInvited] = useState(false);
|
||||
|
||||
// Arriving from a team invite for an already-registered email → prefill it.
|
||||
useEffect(() => {
|
||||
try { const e = sessionStorage.getItem("invite_email"); if (e) { setEmail(e); setInvited(true); } } catch { /* ignore */ }
|
||||
}, []);
|
||||
|
||||
// After a successful sign-in, redeem a pending team invite (if any), then go to the dashboard.
|
||||
async function acceptPendingInviteThenDashboard() {
|
||||
try {
|
||||
const t = sessionStorage.getItem("invite_token");
|
||||
if (t) {
|
||||
await sdk.command("crm.team.invitation.accept", { token: t });
|
||||
sessionStorage.removeItem("invite_token");
|
||||
sessionStorage.removeItem("invite_email");
|
||||
}
|
||||
} catch { /* invite expired/used — proceed to the dashboard anyway */ }
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
|
||||
// Minimal account stand-in for passwordless entry points (Shell mode).
|
||||
function blankAccount(): Account {
|
||||
@@ -87,7 +106,7 @@ export function LoginFlow() {
|
||||
registered = !!st?.registered;
|
||||
} catch { registered = false; }
|
||||
window.history.replaceState({}, "", "/portal/login");
|
||||
if (registered) { router.replace("/dashboard"); return; }
|
||||
if (registered) { await acceptPendingInviteThenDashboard(); 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");
|
||||
@@ -154,7 +173,7 @@ export function LoginFlow() {
|
||||
{step === "identify" && (
|
||||
<>
|
||||
{flash && <div style={{ marginBottom: 16 }}><FlashNote tone="error">{flash}</FlashNote></div>}
|
||||
<Identify email={email} setEmail={setEmail} onSocial={onSocial} onEmail={identifyEmail} onPhone={startPhoneLogin} toRegister={() => router.push("/portal/register")} />
|
||||
<Identify email={email} setEmail={setEmail} onSocial={onSocial} onEmail={identifyEmail} onPhone={startPhoneLogin} toRegister={() => router.push("/portal/register")} invited={invited} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -201,11 +220,11 @@ export function LoginFlow() {
|
||||
)}
|
||||
|
||||
{step === "password" && account && (
|
||||
<Password account={account} email={email} login={login} onAuthenticated={() => router.replace("/dashboard")} flash={flash} onBack={back} onForgot={() => push("fp_confirm")} onOtp={() => { setOtpChannel("email"); replace("otp"); }} onLocked={() => setFlash("This account is temporarily locked.")} onOk={() => afterAuth("password")} />
|
||||
<Password account={account} email={email} login={login} onAuthenticated={acceptPendingInviteThenDashboard} flash={flash} onBack={back} onForgot={() => push("fp_confirm")} onOtp={() => { setOtpChannel("email"); replace("otp"); }} onLocked={() => setFlash("This account is temporarily locked.")} onOk={() => afterAuth("password")} />
|
||||
)}
|
||||
|
||||
{step === "otp" && account && (
|
||||
<OtpVerify account={account} email={email} initialChannel={otpChannel} remember={remember} setRemember={setRemember} onBack={back} onVerified={() => afterAuth("otp")} onAuthenticated={() => router.replace("/dashboard")} />
|
||||
<OtpVerify account={account} email={email} initialChannel={otpChannel} remember={remember} setRemember={setRemember} onBack={back} onVerified={() => afterAuth("otp")} onAuthenticated={acceptPendingInviteThenDashboard} />
|
||||
)}
|
||||
|
||||
{step === "another" && account && (
|
||||
@@ -269,17 +288,17 @@ export function LoginFlow() {
|
||||
|
||||
/* ============================ screens ============================ */
|
||||
|
||||
function Identify({ email, setEmail, onSocial, onEmail, onPhone, toRegister }: {
|
||||
function Identify({ email, setEmail, onSocial, onEmail, onPhone, toRegister, invited }: {
|
||||
email: string; setEmail: (v: string) => void;
|
||||
onSocial: (p: "google") => void; onEmail: () => void; onPhone: () => void; toRegister: () => void;
|
||||
onSocial: (p: "google") => void; onEmail: () => void; onPhone: () => void; toRegister: () => void; invited?: boolean;
|
||||
}) {
|
||||
const [showEmail, setShowEmail] = useState(false);
|
||||
const [showEmail, setShowEmail] = useState(!!invited);
|
||||
const valid = EMAIL_RE.test(email);
|
||||
return (
|
||||
<div>
|
||||
<div className="kicker">Welcome back</div>
|
||||
<h1>Sign in to LynkedUp</h1>
|
||||
<p className="sub">Drone inspections, AI estimates and insurance-ready reports — all in one place.</p>
|
||||
<div className="kicker">{invited ? "You're invited" : "Welcome back"}</div>
|
||||
<h1>{invited ? "Sign in to join the team" : "Sign in to LynkedUp"}</h1>
|
||||
<p className="sub">{invited ? "You already have an account — sign in to accept your invitation." : "Drone inspections, AI estimates and insurance-ready reports — all in one place."}</p>
|
||||
|
||||
<div style={{ marginTop: 22 }}>
|
||||
<SocialButtons onPick={onSocial} />
|
||||
|
||||
Reference in New Issue
Block a user