import { useEffect, useState } from "react";
import type { FormEvent } from "react";
import { ApiError, clinicianApi } from "../../api/client";
import type { ClinicianSession } from "../../api/types";
import { Button, Callout, cn, Field, Icon, Input, linkButtonClass } from "../kit";
import { Link, navigate, useLocation } from "../router";
import { errorText, Spinner } from "../ui";
import { AuthFrame, codeClass, StepLinks, StepTitle } from "./AuthFrame";
import { StepUp } from "./StepUp";
const safeNext = (next: string | null) => (next && next.startsWith("/app") && !next.startsWith("//") ? next : "/app/today");
/** ACC-7: work email, then a one-time code, then the second step. */
export default function SignIn() {
const { query } = useLocation();
const next = safeNext(query.get("next"));
const reason = query.get("reason");
const [stage, setStage] = useState<"checking" | "email" | "code" | "enrol" | "verify" | "pending">("checking");
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const [verification, setVerification] = useState<ClinicianSession["verification"]>("verified");
const afterSession = (s: ClinicianSession) => {
setVerification(s.verification);
if (s.verification !== "verified") return setStage("pending");
if (s.stepUp === "done") return navigate(next, { replace: true });
setStage(s.stepUp === "enrol" ? "enrol" : "verify");
};
useEffect(() => {
clinicianApi.session().then(afterSession, () => setStage("email"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const sendCode = async (e?: FormEvent) => {
e?.preventDefault();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.trim())) {
setError("Enter your work email.");
return;
}
setBusy(true);
setError("");
try {
await clinicianApi.sendCode(email.trim().toLowerCase());
setStage("code");
} catch (err) {
setError(errorText(err));
} finally {
setBusy(false);
}
};
const verify = async (e: FormEvent) => {
e.preventDefault();
if (!/^\d{6}$/.test(code.trim())) {
setError("Enter the 6-digit code from the email.");
return;
}
setBusy(true);
setError("");
try {
await clinicianApi.verifyCode(email.trim().toLowerCase(), code.trim());
afterSession(await clinicianApi.session());
} catch (err) {
setError(err instanceof ApiError && err.status === 401 ? "That code did not sign you in. Request a new one." : errorText(err));
} finally {
setBusy(false);
}
};
return (
<AuthFrame>
{reason === "expired" && stage === "email" && (
<Callout tone="amber" role="status" icon="time-line">
Your session ended after 12 hours. Sign in again.
</Callout>
)}
{reason === "signed-out" && stage === "email" && (
<Callout role="status" icon="logout-box-r-line">
You are signed out on every device.
</Callout>
)}
{stage === "checking" && <Spinner label="Checking your session" className="self-center py-6" />}
{stage === "email" && (
<form className="flex flex-col gap-5" onSubmit={sendCode} noValidate>
<StepTitle title="Sign in">We email you a one-time code. There is no password.</StepTitle>
<Field id="signin-email" label="Work email" error={error}>
<Input
id="signin-email"
type="email"
inputMode="email"
autoComplete="email"
value={email}
aria-invalid={error ? true : undefined}
aria-describedby={error ? "signin-email-error" : undefined}
onChange={(e) => setEmail(e.target.value)}
autoFocus
/>
</Field>
<Button type="submit" variant="primary" size="lg" className="w-full" disabled={busy}>
<Icon name="mail-send-line" size={16} />
{busy ? "Sending…" : "Email me a code"}
</Button>
<p className="m-0 text-[13px] leading-5 text-muted">
New to Context?{" "}
<Link to="/request-access" className="font-medium text-ink underline decoration-line underline-offset-4 hover:decoration-ink">
Request access
</Link>{" "}
or{" "}
<a href="/demo" className="font-medium text-ink underline decoration-line underline-offset-4 hover:decoration-ink">
try a fictional patient
</a>
.
</p>
</form>
)}
{stage === "code" && (
<form className="flex flex-col gap-5" onSubmit={verify} noValidate>
<StepTitle icon="mail-check-line" iconClass="text-green-icon" title="Check your email">
If <strong className="font-medium text-ink">{email.trim()}</strong> belongs to a verified clinician, a 6-digit code is on its way. It works for 10 minutes.
</StepTitle>
<Field id="signin-code" label="Code from the email" error={error}>
<Input
id="signin-code"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
placeholder="······"
className={codeClass}
value={code}
aria-invalid={error ? true : undefined}
aria-describedby={error ? "signin-code-error" : undefined}
onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
autoFocus
/>
</Field>
<Button type="submit" variant="primary" size="lg" className="w-full" disabled={busy}>
{busy ? "Checking…" : "Continue"}
</Button>
<StepLinks>
<button type="button" className={cn(linkButtonClass, "text-muted no-underline hover:text-ink")} onClick={() => { setStage("email"); setCode(""); setError(""); }}>
<Icon name="arrow-left-line" size={14} /> Use another email
</button>
<button type="button" className={linkButtonClass} disabled={busy} onClick={() => void sendCode()}>
Send a new code
</button>
</StepLinks>
</form>
)}
{(stage === "enrol" || stage === "verify") && <StepUp mode={stage} onDone={afterSession} />}
{stage === "pending" && (
<div className="flex flex-col gap-5">
<StepTitle icon={verification === "suspended" ? "forbid-line" : "hourglass-line"} iconClass={verification === "suspended" ? "text-red-icon" : "text-amber-icon"}
title={verification === "lapsed" ? "Your yearly licence check has run out" : verification === "suspended" ? "Your access is paused" : "Your access is being checked"}>
{verification === "lapsed"
? "Send your current licence to MARMAR to continue with patient records. Until it is checked again you can try Context with fictional patients."
: verification === "suspended"
? "Your access to patient records is paused. Contact MARMAR support. You can still try Context with fictional patients."
: "MARMAR verifies your licence before you can see real patients. We email you as soon as it is done. Until then you can try Context with fictional patients."}
</StepTitle>
<Button asChild variant="primary" size="lg" className="w-full no-underline">
<a href="/demo">
<Icon name="user-heart-line" size={16} /> Try a fictional patient
</a>
</Button>
</div>
)}
</AuthFrame>
);
}