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:
tanweer919
2026-07-13 20:25:25 +05:30
parent cc71516278
commit be4bd5f41d
4 changed files with 126 additions and 48 deletions
+15 -6
View File
@@ -53,6 +53,13 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
// verify step (email is trusted without an OTP — see verifyValid below)
const [phoneVerified, setPhoneVerified] = useState(false);
// When arriving from a team invite, the email is fixed to the invited address.
const [invitedEmail, setInvitedEmail] = useState("");
useEffect(() => {
if (onboard) return;
try { const e = sessionStorage.getItem("invite_email"); if (e) { setEmail(e); setInvitedEmail(e); } } catch { /* ignore */ }
}, [onboard]);
// Onboarding: prefill the verified email — from the session-storage hint the login
// page stashed at OAuth time, then confirmed via the live Supabase session.
@@ -138,6 +145,7 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
if (inviteToken) {
await sdk.command("crm.team.invitation.accept", { token: inviteToken });
sessionStorage.removeItem("invite_token");
sessionStorage.removeItem("invite_email");
}
} catch { /* invitation expired/used — they can still be invited again */ }
}
@@ -154,7 +162,7 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
<>
{submitErr && <div style={{ marginBottom: 14 }}><FlashNote tone="error">{submitErr}</FlashNote></div>}
<StepAccount
onboard={onboard} creating={creating}
onboard={onboard} creating={creating} invited={!!invitedEmail}
sso={sso} setSso={setSso} email={email} setEmail={setEmail}
first={first} setFirst={setFirst} last={last} setLast={setLast}
cc={cc} setCc={setCc} phone={phone} setPhone={setPhone} phoneOk={phoneOk} country={country}
@@ -192,7 +200,7 @@ function StepAccount(p: {
cc: string; setCc: (v: string) => void; phone: string; setPhone: (v: string) => void; phoneOk: boolean; country: typeof countryCodes[number];
pw: string; setPw: (v: string) => void;
termsOk: boolean; setTermsOk: (v: boolean) => void; privacyOk: boolean; setPrivacyOk: (v: boolean) => void;
valid: boolean; onContinue: () => void; toLogin: () => void; onboard?: boolean; creating?: boolean;
valid: boolean; onContinue: () => void; toLogin: () => void; onboard?: boolean; creating?: boolean; invited?: boolean;
}) {
// Onboarding (OAuth) starts on the profile step — email is already known + verified.
const [phase, setPhase] = useState<"sso" | "profile">(p.onboard ? "profile" : "sso");
@@ -203,21 +211,22 @@ function StepAccount(p: {
return (
<div>
<StepBack onClick={p.toLogin} />
<h1>Create your account</h1>
<p className="sub">Enter your email to get started.</p>
<h1>{p.invited ? "Accept your invitation" : "Create your account"}</h1>
<p className="sub">{p.invited ? "You've been invited to the team. Set up your account to join." : "Enter your email to get started."}</p>
<div style={{ marginTop: 20 }}>
<form onSubmit={(e) => { e.preventDefault(); if (valid) { p.setSso(null); setPhase("profile"); } }}>
<div className="field">
<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} onChange={(e) => p.setEmail(e.target.value)} placeholder="you@example.com" autoFocus />
<input className="input" type="email" value={p.email} onChange={(e) => p.setEmail(e.target.value)} placeholder="you@example.com" autoFocus={!p.invited} disabled={p.invited} readOnly={p.invited} />
</div>
{p.invited && <span className="faint" style={{ fontSize: 12 }}>This is the address you were invited with.</span>}
</div>
<button className="btn btn-primary" style={{ marginTop: 12 }} disabled={!valid}>Continue <Icon name="arrowR" size={16} /></button>
</form>
</div>
<p className="foot-note">Already registered? <button className="link" onClick={p.toLogin}>Sign in</button></p>
{!p.invited && <p className="foot-note">Already registered? <button className="link" onClick={p.toLogin}>Sign in</button></p>}
</div>
);
}