| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- "use client";
- import { useRouter } from "next/navigation";
- import { useCallback, useRef, useState } from "react";
- import { Button } from "@/components/ui/button";
- import { Input } from "@/components/ui/input";
- import { getBrivenAuth } from "@/lib/auth";
- import { establishPortalSession } from "@/lib/portal-session";
- /** Most Briven email OTPs are 6 digits — verify as soon as that many land. */
- const OTP_LEN = 6;
- /**
- * Email OTP + Konnos OAuth (magic link off — use Briven Auth Providers).
- * After OTP success we mint a first-party portal session so the dashboard sticks.
- */
- export function BrivenPasswordlessSignIn({
- redirectTo = "/portal/dashboard",
- }: {
- redirectTo?: string;
- }) {
- const router = useRouter();
- const auth = getBrivenAuth();
- const [email, setEmail] = useState("");
- const [otp, setOtp] = useState("");
- const [otpSent, setOtpSent] = useState(false);
- const [busy, setBusy] = useState(false);
- const [message, setMessage] = useState<string | null>(null);
- const [error, setError] = useState<string | null>(null);
- const verifyingRef = useRef(false);
- const lastTriedRef = useRef("");
- const finishLogin = useCallback(
- async (userId: string, loginEmail: string) => {
- await establishPortalSession({ userId, email: loginEmail });
- window.dispatchEvent(new Event("krypco-auth-change"));
- router.push(redirectTo);
- router.refresh();
- },
- [redirectTo, router],
- );
- const verifyOtp = useCallback(
- async (code: string) => {
- if (!auth) return;
- const cleaned = code.replace(/\s/g, "").trim();
- if (cleaned.length < 4) return;
- if (verifyingRef.current) return;
- if (lastTriedRef.current === cleaned) return;
- verifyingRef.current = true;
- lastTriedRef.current = cleaned;
- setError(null);
- setBusy(true);
- setMessage("Checking code…");
- try {
- const loginEmail = email.trim().toLowerCase();
- const r = await auth.signIn.otpVerify({
- email: loginEmail,
- otp: cleaned,
- });
- if (!r.ok) {
- setError(r.message || r.code);
- setMessage(null);
- lastTriedRef.current = "";
- return;
- }
- const userId =
- ("userId" in r && typeof r.userId === "string" && r.userId) ||
- `user_${loginEmail}`;
- setMessage("Signed in — opening dashboard…");
- await finishLogin(userId, loginEmail);
- } catch (e) {
- setError(e instanceof Error ? e.message : "Could not verify code");
- setMessage(null);
- lastTriedRef.current = "";
- } finally {
- setBusy(false);
- verifyingRef.current = false;
- }
- },
- [auth, email, finishLogin],
- );
- if (!auth) {
- return (
- <p className="text-sm text-muted-foreground">
- Briven Auth key missing in environment.
- </p>
- );
- }
- async function sendOtp() {
- if (!auth) return;
- setError(null);
- setMessage(null);
- setBusy(true);
- try {
- const r = await auth.signIn.otpRequest({
- email: email.trim(),
- redirectTo,
- });
- if (!r.ok) {
- setError(r.message || r.code);
- return;
- }
- setOtpSent(true);
- setOtp("");
- lastTriedRef.current = "";
- setMessage("Check your email for a one-time code — paste it below.");
- } catch (e) {
- setError(e instanceof Error ? e.message : "Could not send code");
- } finally {
- setBusy(false);
- }
- }
- function onOtpChange(raw: string) {
- const digits = raw.replace(/\D/g, "").slice(0, OTP_LEN);
- setOtp(digits);
- setError(null);
- if (digits.length === OTP_LEN) {
- void verifyOtp(digits);
- }
- }
- function startKonnos() {
- if (!auth) return;
- setError(null);
- const { redirectUrl } = auth.signIn.social({
- provider: "konnos",
- redirectTo:
- typeof window !== "undefined"
- ? `${window.location.origin}${redirectTo}`
- : redirectTo,
- });
- window.location.assign(redirectUrl);
- }
- return (
- <div className="space-y-5">
- <div className="space-y-2">
- <label htmlFor="briven-email" className="text-sm font-medium">
- Email
- </label>
- <Input
- id="briven-email"
- type="email"
- autoComplete="email"
- placeholder="you@company.com"
- value={email}
- onChange={(e) => setEmail(e.target.value)}
- disabled={busy}
- />
- </div>
- <Button
- type="button"
- className="w-full"
- disabled={busy || !email.includes("@")}
- onClick={() => void sendOtp()}
- >
- {otpSent ? "Resend email code" : "Send email code"}
- </Button>
- {otpSent ? (
- <div className="flex flex-col items-center border-t border-border pt-6 text-center">
- <label
- htmlFor="briven-otp"
- className="mb-3 text-sm font-medium text-foreground"
- >
- Code from email
- </label>
- <Input
- id="briven-otp"
- inputMode="numeric"
- autoComplete="one-time-code"
- autoFocus
- placeholder="······"
- maxLength={OTP_LEN}
- value={otp}
- onChange={(e) => onOtpChange(e.target.value)}
- disabled={busy}
- aria-label="Six-digit code from your email"
- className="mx-auto h-14 w-full max-w-[16rem] text-center font-mono text-2xl tracking-[0.35em] tabular-nums"
- />
- <p className="mt-3 max-w-xs text-xs text-muted-foreground leading-relaxed">
- Paste or type all {OTP_LEN} digits — we check the code and open the
- dashboard automatically.
- </p>
- {busy && otp.length === OTP_LEN ? (
- <p className="mt-2 text-sm text-muted-foreground" role="status">
- Signing you in…
- </p>
- ) : null}
- {otp.length >= 4 && otp.length < OTP_LEN ? (
- <Button
- type="button"
- className="mt-4"
- disabled={busy}
- onClick={() => void verifyOtp(otp)}
- >
- Verify and continue
- </Button>
- ) : null}
- </div>
- ) : null}
- <div className="relative py-2">
- <div className="absolute inset-0 flex items-center">
- <span className="w-full border-t border-border" />
- </div>
- <div className="relative flex justify-center text-xs uppercase tracking-wide text-muted-foreground">
- <span className="bg-card px-2">or</span>
- </div>
- </div>
- <Button
- type="button"
- variant="outline"
- className="inline-flex w-full items-center justify-center gap-2"
- disabled={busy}
- onClick={startKonnos}
- >
- {/* eslint-disable-next-line @next/next/no-img-element */}
- <img
- src="/konnos.svg"
- alt=""
- width={20}
- height={20}
- className="h-5 w-5 shrink-0 object-contain"
- aria-hidden
- />
- Continue with Konnos
- </Button>
- {message ? (
- <p className="text-center text-sm text-proof" role="status">
- {message}
- </p>
- ) : null}
- {error ? (
- <p className="text-center text-sm text-pending" role="alert">
- {error}
- </p>
- ) : null}
- <p className="text-xs text-muted-foreground leading-relaxed">
- Sign in with a one-time email code. Magic links are off. MFA (if
- enrolled) is handled by Briven after the first factor.
- </p>
- </div>
- );
- }
|