import { useEffect, useState } from 'react'; import { AlertTriangle, ShieldCheck } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../../../components/ui/dialog.tsx'; import { Input } from '../../../../components/ui/input.tsx'; import { Label } from '../../../../components/ui/label.tsx'; import { useCredentialRecoveryService } from '../../../../services/CredentialRecoveryService.tsx'; import { relayServerFailure } from '../../../../utils/fetchFailureMessage'; // - THE SECOND DOOR OUT OF A LOCKED ACCOUNT, and the only one with a human in it. // // locks an account when a patient says "this wasn't me". Its single exit is a Stripe Identity // webhook landing; when that does not happen the patient retries forever against a byte-identical 403, // locked out of their own health record with nobody able to help. This dialog is the other way out. // // SPENCER RULED 2026-08-05: "If the staff member is willing to attest that they have verified the // patient's identity then they should be able to unlock it for them. They should have a notice in that // UI flow saying that doing this is recorded and can be audited, and that this is really the person that // owns the account." // THE NOTICE BELOW IS PART OF THE RULING, NOT DECORATION. It is the only thing between a receptionist // and treating this as a routine convenience, on the one route where they end a lock somebody // deliberately placed. Reword it honestly if you must; do not remove it. Pinned by meaning in // StaffUnlockDialog.test.tsx. // // WE RECORD WHAT WAS CHECKED, NEVER AN IMAGE OF THE ID. Do not add an upload here - the // server rejects an image field outright rather than ignoring it, and a test asserts there is no file // input on this surface. // // THIS DIALOG CHANGES NO ADDRESS, and that is what makes a single attester sufficient. The server // re-enables the account at the address on its own lock record, so an override grants the attester // nothing - it removes a lock, it does not hand over a mailbox. There is deliberately no email field // here; if one ever appears, that property is gone. const ID_TYPE_OPTIONS = [ { value: '', label: 'Select…' }, { value: 'drivers-license', label: "Driver's license" }, { value: 'state-id', label: 'State ID card' }, { value: 'passport', label: 'Passport' }, { value: 'military-id', label: 'Military ID' }, { value: 'other', label: 'Other government-issued ID' }, ]; const SELECT_CLASSNAME = 'border-input dark:bg-input/30 h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs ' + 'outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-ring/50 ' + 'focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50'; // handlers/CredentialRecovery/staff-unlock.ts crafts a specific, actionable message for every refusal - // the two 403s (no affiliation / no patient access) and, most importantly for this caller, the 409 that // says the account was not locked in the first place. That last one is the refusal a receptionist is // most likely to see and the one generic copy would render useless. One rule: show what the server said, // verbatim, and fall back only when there is nothing to show. Same idiom as RecoverLoginEmailDialog. const serverMessage = (err: unknown): string | undefined => { const raw = relayServerFailure(err, '').trim(); return raw.length > 0 ? raw : undefined; }; export interface StaffUnlockDialogProps { open: boolean; /** Named explicitly - staff act for someone else; there is no "caller's own patient" here. */ patientId: string; /** * WHO THIS IS ABOUT, and it is the reason this prop exists rather than a nicety. * * THIS DIALOG SITS ONE CLICK OFF A LIST ROW, so the failure mode is acting on the WRONG ROW - and * a title reading *"this patient's account"* is equally true of every row on the screen. Consequence * copy, however good, cannot catch a wrong-subject error; only naming the subject can. * * OPTIONAL, AND THE FALLBACK IS DELIBERATE. A patient with no recorded name must not produce * *"Change Patient's login email"* - a placeholder rendered in the possessive reads as a real name and * is worse than the generic sentence it replaced. Absent the original copy, unchanged. */ patientName?: string; onOpenChange: (open: boolean) => void; /** Fired once the account is actually unlocked, so the parent can refresh around it. */ onUnlocked: () => void; } const StaffUnlockDialog = ({ open, patientId, patientName, onOpenChange, onUnlocked }: StaffUnlockDialogProps) => { const { staffUnlockAccount } = useCredentialRecoveryService(); const [idType, setIdType] = useState(''); const [idState, setIdState] = useState(''); const [idLast4, setIdLast4] = useState(''); const [idExpiry, setIdExpiry] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(); const [unlocked, setUnlocked] = useState(false); // A prior patient's typed identity fields must never survive into a new session, and a prior success // screen must never bleed into the next patient. useEffect(() => { if (open) { setIdType(''); setIdState(''); setIdLast4(''); setIdExpiry(''); setSubmitting(false); setError(undefined); setUnlocked(false); } }, [open]); // ALL FOUR fields, mirroring the server's own all-or-nothing check. An attestation naming no document // is an attestation to nothing, and with one low-privilege attester and no second approver it is the // only control on the grant itself. const canSubmit = Boolean(idType) && Boolean(idState.trim()) && Boolean(idLast4.trim()) && Boolean(idExpiry) && !submitting; const handleSubmit = async () => { if (!canSubmit) return; setSubmitting(true); setError(undefined); try { await staffUnlockAccount({ patientId, verifiedIdType: idType, verifiedIdState: idState.trim(), verifiedIdLast4: idLast4.trim(), verifiedIdExpiry: idExpiry, }); setUnlocked(true); onUnlocked(); } catch (err) { console.error('Failed to unlock the account on a staff attestation:', err); setError(serverMessage(err) ?? 'Could not unlock this account. Please try again.'); } finally { setSubmitting(false); } }; return ( {patientName ? `Unlock ${patientName}'s account` : "Unlock this patient's account"} For staff use with the patient present in office, when they cannot complete identity verification themselves. {unlocked ? (

Account unlocked. They can sign in again with the email address they used before — their password has not changed.

) : (
{/* SPENCER'S RULING, VERBATIM IN SUBSTANCE — see the header. */}

Use this only for an account that was locked because someone reported it was taken over; an account that is not locked stays as it is. By unlocking it you are attesting that you have checked this person's photo ID in person and that they are the person who owns this account. This action is recorded against your name and can be audited.

setIdState(e.target.value)} disabled={submitting} />
setIdLast4(e.target.value)} disabled={submitting} />
setIdExpiry(e.target.value)} disabled={submitting} />
{error && (

{error}

)}
)}
); }; export default StaffUnlockDialog;