feat(team+auth): live multi-role team management + email/SMS OTP login

- team-management: new useTeamData() data layer serves live be-crm data door
  (crm.team.member.search/role.list/invitation.list + set-roles/perm/invite
  commands) or the mock fallback, gated on isShellConfigured(). A member can
  hold MANY roles: role chips + multi-select picker in Edit/Invite modals.
- login OTP: OtpVerify wired to real Supabase email + SMS OTP (passwordless)
  via appshell sendEmailOtp/verifyEmailOtp + sendPhoneOtp/verifyPhoneOtp.
- bump @abe-kap/appshell-sdk ^0.2.0 (phone OTP).
This commit is contained in:
tanweer919
2026-07-11 04:44:54 +05:30
parent 1d37593102
commit 565019a75c
5 changed files with 462 additions and 217 deletions
+66 -18
View File
@@ -166,7 +166,7 @@ export function LoginFlow() {
)}
{step === "otp" && account && (
<OtpVerify account={account} remember={remember} setRemember={setRemember} onBack={back} onVerified={() => afterAuth("otp")} />
<OtpVerify account={account} email={email} remember={remember} setRemember={setRemember} onBack={back} onVerified={() => afterAuth("otp")} onAuthenticated={() => router.replace("/dashboard")} />
)}
{step === "another" && account && (
@@ -364,35 +364,83 @@ function Password({ account, email, login, onAuthenticated, flash, onBack, onFor
);
}
function OtpVerify({ account, remember, setRemember, onBack, onVerified }: {
account: Account; remember: boolean; setRemember: (v: boolean) => void; onBack: () => void; onVerified: () => void;
function OtpVerify({ account, email, remember, setRemember, onBack, onVerified, onAuthenticated }: {
account: Account; email: string; remember: boolean; setRemember: (v: boolean) => void; onBack: () => void; onVerified: () => void; onAuthenticated: () => void;
}) {
const { sendEmailOtp, verifyEmailOtp, sendPhoneOtp, verifyPhoneOtp } = useAuth();
const shell = isShellConfigured();
// In Shell mode only email + SMS are real Supabase OTP channels; hide WhatsApp.
const [channel, setChannel] = useState<"email" | "sms" | "wa">("email");
const [error, setError] = useState(false);
const target = channel === "email" ? account.maskedEmail : channel === "sms" ? account.maskedPhone : account.maskedWa;
function complete(code: string) {
if (code === "000000") { setError(true); return; }
setError(false); onVerified();
const [phone, setPhone] = useState("");
const [sent, setSent] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>("");
const target = channel === "email" ? account.maskedEmail : channel === "sms" ? (phone || account.maskedPhone) : account.maskedWa;
const PHONE_RE = /^\+[1-9]\d{6,14}$/;
// Real Supabase OTP: auto-send the email code when this screen opens.
useEffect(() => {
if (shell && channel === "email" && !sent) void send();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shell, channel]);
async function send() {
setError("");
try {
if (channel === "email") { await sendEmailOtp(email); setSent(true); }
else if (channel === "sms") {
if (!PHONE_RE.test(phone)) { setError("Enter your phone in international format, e.g. +14155550100."); return; }
await sendPhoneOtp(phone); setSent(true);
}
} catch (e) { setError((e as Error).message); }
}
async function complete(code: string) {
if (!shell) { if (code === "000000") { setError("bad"); return; } setError(""); onVerified(); return; }
setBusy(true); setError("");
try {
if (channel === "email") await verifyEmailOtp(email, code);
else await verifyPhoneOtp(phone, code);
onAuthenticated();
} catch { setError("Incorrect or expired code. Please try again."); }
finally { setBusy(false); }
}
const showBoxes = !shell || sent;
return (
<div>
<StepBack onClick={onBack} />
<h1>2-step verification</h1>
<p className="sub">Enter the 6-digit code we sent you to finish signing in.</p>
<h1>{shell ? "One-time passcode" : "2-step verification"}</h1>
<p className="sub">{shell ? "We'll text or email you a 6-digit code to sign in." : "Enter the 6-digit code we sent you to finish signing in."}</p>
<div className="seg" style={{ margin: "16px 0 14px" }}>
<button className={channel === "email" ? "on" : ""} onClick={() => { setChannel("email"); setError(false); }}><Icon name="mail" size={15} /> Email</button>
<button className={channel === "sms" ? "on" : ""} onClick={() => { setChannel("sms"); setError(false); }}><Icon name="sms" size={15} /> SMS</button>
<button className={channel === "wa" ? "on" : ""} onClick={() => { setChannel("wa"); setError(false); }}><Icon name="whatsapp" size={15} /> WhatsApp</button>
<button className={channel === "email" ? "on" : ""} onClick={() => { setChannel("email"); setError(""); setSent(false); }}><Icon name="mail" size={15} /> Email</button>
<button className={channel === "sms" ? "on" : ""} onClick={() => { setChannel("sms"); setError(""); setSent(false); }}><Icon name="sms" size={15} /> SMS</button>
{!shell && <button className={channel === "wa" ? "on" : ""} onClick={() => { setChannel("wa"); setError(""); }}><Icon name="whatsapp" size={15} /> WhatsApp</button>}
</div>
<p className="faint" style={{ fontSize: 12.5, marginBottom: 12 }}>Code sent to {target}</p>
<OtpBoxes onComplete={complete} error={error} />
{error && <div style={{ marginTop: 12 }}><FlashNote tone="error">Incorrect code. Please try again.</FlashNote></div>}
{shell && channel === "sms" && !sent ? (
<form onSubmit={(e) => { e.preventDefault(); void send(); }}>
<div className="field">
<label className="label" htmlFor="otpphone">Phone number</label>
<input id="otpphone" className="input" type="tel" autoFocus placeholder="+1 415 555 0100" value={phone} onChange={(e) => setPhone(e.target.value)} />
</div>
<button className="btn btn-primary" style={{ marginTop: 12 }} disabled={busy}>Send code <Icon name="arrowR" size={16} /></button>
</form>
) : (
<>
<p className="faint" style={{ fontSize: 12.5, marginBottom: 12 }}>Code sent to {target}</p>
{showBoxes && <OtpBoxes onComplete={(c) => void complete(c)} error={!!error} />}
</>
)}
{error && <div style={{ marginTop: 12 }}><FlashNote tone="error">{error === "bad" ? "Incorrect code. Please try again." : error}</FlashNote></div>}
<div className="row between" style={{ margin: "14px 0" }}>
<ResendLink seconds={30} />
{shell ? <button className="link" onClick={() => void send()} disabled={busy}>Resend code</button> : <ResendLink seconds={30} />}
<span />
</div>
<RememberDevice checked={remember} onChange={setRemember} note="2-step is still required at each sign-in." />
<p className="demo-hint">Demo: any 6 digits verify; 000000 shows an error.</p>
{!shell && <p className="demo-hint">Demo: any 6 digits verify; 000000 shows an error.</p>}
</div>
);
}