import { useCallback, useEffect, useRef, useState } from "react"; import { Button } from "../../../components/ui/button"; import { Checkbox } from "../../../components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../../components/ui/dialog"; import { QrCode } from "../../../components/QrCode/QrCode"; import { HttpError } from "../../../services/HttpService"; import { useShareAccessService, type ShareAccessService, type ShareStatus, type ShownCode, type SignedAuthorization, } from "../../../services/shareAccessService"; import "./ShareAccessDialog.css"; const POLL_MS = 3000; const DOT = " \u00b7 "; type Phase = | { kind: "intro" } | { kind: "showing"; shown: ShownCode } | { kind: "deciding"; status: ShareStatus } | { kind: "finishing"; status: ShareStatus } | { kind: "approved"; teamId?: string; name: string; offerRef: string } | { kind: "ended"; message: string }; const ENDED: Record = { declined: "You declined. Nothing was shared.", cancelled: "The code was withdrawn. Nothing was shared.", expired: "The code expired before anyone used it. Nothing was shared.", none: "That code is no longer waiting. Nothing was shared.", }; const refusalOf = (error: unknown, fallback: string) => error instanceof HttpError && typeof (error.body as { message?: unknown })?.message === "string" ? (error.body as { message: string }).message : fallback; // Where the practitioner types the code is wherever the server's own claim link points, per environment. export const claimHost = (claimUrl: string): string | undefined => { try { return new URL(claimUrl).host || undefined; } catch { return undefined; } }; export const remainingLabel = (expiresAt: string, now: number) => { const seconds = Math.max(0, Math.ceil((Date.parse(expiresAt) - now) / 1000)); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; }; interface ShareAccessDialogProps { open: boolean; onOpenChange: (open: boolean) => void; service: ShareAccessService; onOpenTeam: (teamId: string) => void; } // Share ongoing access by code: show a code, wait for a practitioner to claim it, then approve or // decline the claimant the server describes. Approval signs the release document shown here. export const ShareAccessDialog = ({ open, onOpenChange, service, onOpenTeam }: ShareAccessDialogProps) => { const { showCode, readStatus, decide, readAuthorizations } = service; const [phase, setPhase] = useState({ kind: "intro" }); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [agreed, setAgreed] = useState(false); const [showDocument, setShowDocument] = useState(false); const [signedCopy, setSignedCopy] = useState(null); const [copies, setCopies] = useState([]); const [openCopy, setOpenCopy] = useState(null); const [copiesFailed, setCopiesFailed] = useState(false); const [availability, setAvailability] = useState(null); const [showCopies, setShowCopies] = useState(false); const loadCopies = useCallback(() => { readAuthorizations() .then((signed) => { setCopies(signed.authorizations); setAvailability(typeof signed.availability === 'string' ? signed.availability : null); setCopiesFailed(false); }) .catch(() => setCopiesFailed(true)); }, [readAuthorizations]); const [now, setNow] = useState(() => Date.now()); const phaseRef = useRef(phase); phaseRef.current = phase; // Reopening picks up a claim already waiting for the patient; anything else starts from the start. useEffect(() => { if (!open) return; setPhase({ kind: "intro" }); setError(null); setAgreed(false); setShowDocument(false); setSignedCopy(null); setOpenCopy(null); setShowCopies(false); loadCopies(); readStatus() .then((status) => { if (phaseRef.current.kind !== "intro") return; if (status.state === "claimed") setPhase({ kind: "deciding", status }); if (status.state === "approving") setPhase({ kind: "finishing", status }); }) .catch(() => undefined); }, [open, readStatus, loadCopies]); const showing = phase.kind === "showing"; useEffect(() => { if (!open || !showing) return; const tick = window.setInterval(() => setNow(Date.now()), 1000); const poll = window.setInterval(async () => { try { const status = await readStatus(); if (phaseRef.current.kind !== "showing") return; if (status.state === "claimed") setPhase({ kind: "deciding", status }); else if (status.state !== "minted") setPhase({ kind: "ended", message: ENDED[status.state] ?? ENDED.none }); } catch { // A missed poll is retried on the next tick; the countdown still runs. } }, POLL_MS); return () => { window.clearInterval(tick); window.clearInterval(poll); }; }, [open, showing, readStatus]); useEffect(() => { if (phase.kind === "showing" && Date.parse(phase.shown.expiresAt) <= now) { setPhase({ kind: "ended", message: ENDED.expired }); } }, [phase, now]); const start = useCallback(async () => { setBusy(true); setError(null); try { const shown = await showCode(); setNow(Date.now()); setPhase({ kind: "showing", shown }); } catch (failure) { setError(refusalOf(failure, "The code could not be shown. Try again.")); } finally { setBusy(false); } }, [showCode]); const act = useCallback(async (run: () => Promise<{ state: string; teamId?: string }>, name = "", offerRef = "") => { setBusy(true); setError(null); try { const result = await run(); setPhase(result.state === "approved" ? { kind: "approved", teamId: result.teamId, name, offerRef } : { kind: "ended", message: ENDED[result.state] ?? ENDED.none }); if (result.state === "approved") loadCopies(); } catch (failure) { // This offer has moved on elsewhere: its controls go, and nothing is retried against a newer one. if (failure instanceof HttpError && failure.status === 409) { setAgreed(false); setPhase({ kind: "ended", message: refusalOf(failure, ENDED.none) }); loadCopies(); return; } setError(refusalOf(failure, "That did not go through. Try again.")); } finally { setBusy(false); } }, [loadCopies]); // A code nobody has claimed is withdrawn when the screen closes, so it cannot be used unseen. const close = (next: boolean) => { const current = phaseRef.current; if (!next && current.kind === "showing") decide({ decision: "cancel", offerRef: current.shown.offerRef }).catch(() => undefined); onOpenChange(next); }; const claimant = phase.kind === "deciding" ? phase.status.claimant : undefined; const document = phase.kind === "deciding" ? phase.status.document : undefined; const refs = phase.kind === "deciding" || phase.kind === "finishing" ? { offerRef: phase.status.offerRef ?? "", claimRef: phase.status.claimRef ?? "" } : undefined; const toggleSignedCopy = async (offerRef: string) => { if (signedCopy !== null) return setSignedCopy(null); try { const copy = (await readAuthorizations()).authorizations.find((signed) => signed.offerRef === offerRef); setSignedCopy(copy ? copy.signedText : "Your signed copy could not be found. Try again in a moment."); } catch (failure) { setError(refusalOf(failure, "Your signed copy could not be opened. Try again.")); } }; return ( Share access with a practitioner {phase.kind === "deciding" ? "Someone used your code. Check who they are before you decide." : "For a practitioner outside your practices who should follow your care."} {phase.kind === "intro" && (
  • They see your name and can reach you straight away. They see a structured summary of your clinical information, including what is added later, until you stop sharing. Notes, documents, images and attachments are not shared.
  • They can message you and join case conferences about your care. They cannot book or start visits.
  • Substance use treatment records, psychotherapy notes and other specially protected records are not shared.
  • Nothing is shared until you see who used the code and approve them.
)} {/* Every signed copy stays reachable whatever the current offer is doing; only a code on screen hides them. */} {phase.kind !== "showing" && (copies.length > 0 || copiesFailed) && (
{availability &&

{availability}

} {copiesFailed && (

Your signed authorizations could not be loaded.{" "}

)} {showCopies &&
    {copies.map((copy) => (
  • {[copy.recipient.registryName, `NPI ${copy.recipient.npi}`, `signed ${copy.signedAt.slice(0, 10)}`].join(DOT)} {copy.state === "approving" && " (not finished)"} {copy.state === "approved" && ( )} {openCopy === copy.offerRef &&
    {copy.signedText}
    }
  • ))}
}
)} {phase.kind === "showing" && (

{phase.shown.code}

The practitioner scans this or types the code{claimHost(phase.shown.claimUrl) ? ` at ${claimHost(phase.shown.claimUrl)}` : ""}. It expires in{" "} {remainingLabel(phase.shown.expiresAt, now)}.

)} {phase.kind === "deciding" && claimant && document && (
National NPI registry
{[[claimant.registryName, claimant.registryTaxonomy].filter(Boolean).join(", "), `NPI ${claimant.npi}`].join(DOT)}
Signed in as
{[claimant.accountName || "No name given", claimant.accountEmail].join(DOT)}
{!claimant.namesAgree && (

The name on this account does not match the registry. Make sure this is the person you meant.

)}

Akashic checked that this NPI belongs to an active individual practitioner and that this email belongs to the account. It cannot confirm that the person in front of you is that practitioner.

{showDocument &&
{document.text}
}
)} {phase.kind === "deciding" && !document && (

This claim cannot be shown, so it cannot be approved. Decline it and show a new code.

)} {phase.kind === "finishing" && (

You approved sharing with {phase.status.claimant?.registryName ?? "this practitioner"}, but it did not finish. Finish it with the authorization you already signed.

)} {phase.kind === "approved" && (

{phase.name} can now message you and join case conferences about your care. You can stop sharing from the team at any time.

{availability &&

{availability}

} {signedCopy !== null &&
{signedCopy}
}
)} {phase.kind === "ended" &&

{phase.message}

} {error &&

{error}

} {(phase.kind === "intro" || phase.kind === "ended") && ( )} {phase.kind === "showing" && ( )} {phase.kind === "finishing" && refs && phase.status.version !== undefined && phase.status.documentHash && ( )} {phase.kind === "deciding" && refs && phase.status.version !== undefined && ( <> {claimant && document && ( )} )} {phase.kind === "approved" && phase.teamId && ( )}
); }; /** The dialog with its service, mounted only while open so a closed dialog costs the page nothing. */ export const ConnectedShareAccessDialog = (props: Omit) => { const service = useShareAccessService(); return ; }; export default ShareAccessDialog;