import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { useAuthServiceContext } from '../services/AuthService'; import { useDependentService } from '../services/DependentService'; import { authSessionId } from '../services/authStorage'; import { activationNotice, keepActivationReference, readActivationReference, unknownActivation, type ActivationAnswer } from '../services/activationContinuation'; import { onViewerChange, viewerEpoch } from '../utils/writeThroughHold'; import { relayServerFailure } from '../utils/fetchFailureMessage'; import { EMAIL_PATTERN } from '../utils/registerPatientRequirements'; export const ACTIVATION_SEND_DEADLINE_MS = 20_000; interface Options { open: boolean; childPatientId: string; onActivated(isCurrent: () => boolean): void; confirmEligible?: (signal: AbortSignal) => Promise<'allowed' | 'denied' | 'unknown'>; onSettled?: (text: string) => void; } interface Form { opening: number; email: string; pending: boolean; answer?: ActivationAnswer; error?: string; busy: boolean } interface Attempt { controller: AbortController; sent: boolean; expired: boolean } const storage = { getItem: (key: string) => sessionStorage.getItem(key), setItem: (key: string, value: string) => sessionStorage.setItem(key, value), removeItem: (key: string) => sessionStorage.removeItem(key) }; const referenceKey = (viewer: string, organization: string, actedFor: string, child: string) => `account-activation:${viewer}:${organization || 'neutral'}:${actedFor || 'self'}:${child}`; export function useHasActivationReference(childPatientId: string): boolean { const auth = useAuthServiceContext(), viewer = auth.currentUser?.fhir?.id; if (!viewer || !childPatientId) return false; const key = referenceKey(viewer, auth.wpOrganization?.current?.id ?? '', auth.actingAsRef?.current?.relatedPersonId ?? '', childPatientId); return readActivationReference(storage, key, childPatientId).kind !== 'none'; } export function useActivationContinuation(options: Options) { const auth = useAuthServiceContext(), { activateChildLogin } = useDependentService(); const viewer = auth.currentUser?.fhir?.id ?? '', organization = auth.wpOrganization.current?.id ?? ''; const actedFor = auth.actingAsRef.current?.relatedPersonId ?? ''; const key = referenceKey(viewer, organization, actedFor, options.childPatientId); const renderSession = authSessionId(), renderViewer = viewerEpoch(), renderActing = auth.actingAsSessionRef.current; const identity = JSON.stringify([key, renderSession, renderViewer, renderActing]); const active = useRef(null); const latestAttempt = useRef(null); const mounted = useRef(true); const lifetime = useRef({ identity, open: options.open, opening: 1 }); const live = useRef({ auth, identity, open: options.open }); live.current = { auth, identity, open: options.open }; if (lifetime.current.identity !== identity || lifetime.current.open !== options.open) { active.current?.controller.abort(); active.current = null; lifetime.current = { identity, open: options.open, opening: lifetime.current.opening + 1 }; } const opening = lifetime.current.opening; const initial = (): Form => ({ opening, email: '', pending: readActivationReference(storage, key, options.childPatientId).kind !== 'none', busy: false }); const [form, setForm] = useState
(initial); const shown = form.opening === opening ? form : initial(); const refreshed = useRef(new Set()), announced = useRef(new Set()); useEffect(() => { setForm(current => current.opening === opening ? current : initial()); }, [opening]); useEffect(() => { mounted.current = true; return () => { mounted.current = false; active.current?.controller.abort(); active.current = null; }; }, []); useEffect(() => onViewerChange(() => { active.current?.controller.abort(); active.current = null; setForm(current => ({ ...current, busy: false })); }), []); const openingAlive = () => mounted.current && live.current.open && lifetime.current.opening === opening && live.current.identity === identity && live.current.auth.actingAsSessionRef === auth.actingAsSessionRef && auth.actingAsSessionRef.current === renderActing && (live.current.auth.wpOrganization.current?.id ?? '') === organization && viewerEpoch() === renderViewer && authSessionId() === renderSession; const setEmail = (email: string) => { if (openingAlive() && !shown.busy) setForm(current => ({ ...(current.opening === opening ? current : initial()), email })); }; const retire = () => { if (!openingAlive()) return false; active.current?.controller.abort(); active.current = null; lifetime.current = { ...lifetime.current, opening: lifetime.current.opening + 1 }; return true; }; const startNew = () => { if (!openingAlive() || shown.busy || !shown.answer?.canStartNew) return; try { storage.removeItem(key); if (openingAlive()) setForm({ opening, email: shown.email, pending: false, busy: false }); } catch { if (openingAlive()) setForm({ ...shown, error: 'The earlier activation reference could not be cleared. Check status before trying again.' }); } }; const submit = async () => { if (!openingAlive() || !viewer || shown.busy || !EMAIL_PATTERN.test(shown.email.trim())) return; const reference = readActivationReference(storage, key, options.childPatientId); const check = shown.pending || reference.kind !== 'none'; const claimId = shown.answer?.claimId ?? (reference.kind === 'kept' ? reference.reference.claimId : undefined); const held = { child: options.childPatientId, email: shown.email.trim().toLowerCase(), organization, actingRef: auth.actingAsSessionRef, actingEpoch: auth.actingAsSessionRef.current, viewer: viewerEpoch(), session: authSessionId() }; const attempt: Attempt = { controller: new AbortController(), sent: false, expired: false }; active.current = attempt; latestAttempt.current = attempt; const owns = () => openingAlive() && active.current === attempt && live.current.auth.actingAsSessionRef === held.actingRef && held.actingRef.current === held.actingEpoch && live.current.auth.wpOrganization.current?.id === (held.organization || undefined) && viewerEpoch() === held.viewer && authSessionId() === held.session; const deadline = Date.now() + ACTIVATION_SEND_DEADLINE_MS; const admit = () => { if (!owns() || attempt.controller.signal.aborted) throw new DOMException('This activation was put aside.', 'AbortError'); if (Date.now() >= deadline) { attempt.expired = true; attempt.controller.abort(); throw new DOMException('Activation preparation expired.', 'AbortError'); } }; setForm({ ...shown, opening, busy: true, error: undefined }); const timer = setTimeout(() => { attempt.expired = true; attempt.controller.abort(); }, ACTIVATION_SEND_DEADLINE_MS); let abort!: () => void; const aborted = new Promise((_resolve, reject) => { abort = () => reject(new DOMException('Activation was cancelled.', 'AbortError')); attempt.controller.signal.addEventListener('abort', abort, { once: true }); }); aborted.catch(() => undefined); try { if (options.confirmEligible) { const standing = await Promise.race([options.confirmEligible(attempt.controller.signal), aborted]); admit(); if (standing !== 'allowed') { const text = standing === 'denied' ? 'You can no longer activate this login. Nothing was sent.' : 'Your authority could not be checked. Nothing was sent; your details are kept.'; if (owns()) setForm({ ...shown, opening, busy: false, error: text }); if (owns() && standing === 'denied') options.onSettled?.(text); return; } } admit(); const sending = activateChildLogin(held.child, held.email, { check, claimId, signal: attempt.controller.signal, ...(held.organization ? { organizationId: held.organization } : {}), admit, onSend: () => { admit(); keepActivationReference(storage, key, { version: 1, childPatientId: held.child, ...(claimId ? { claimId } : {}) }); attempt.sent = true; } }); sending.catch(() => undefined); const answer = await Promise.race([sending, aborted]); if (!owns() || attempt.controller.signal.aborted) return; const bound = answer?.childPatientId === held.child ? answer : unknownActivation(held.child, claimId); keepActivationReference(storage, key, { version: 1, childPatientId: held.child, ...(bound.claimId ? { claimId: bound.claimId } : {}) }); if (!owns()) return; setForm({ opening, email: held.email, pending: true, answer: bound, busy: false }); const saved = bound.account === 'created-observed' || bound.account === 'associated-proved'; const refreshKey = `${bound.claimId}:${bound.standing === 'producer-settled' ? 'complete' : 'associated'}`; if (saved && bound.claimId && !refreshed.current.has(refreshKey) && owns()) { refreshed.current.add(refreshKey); try { options.onActivated(() => openingAlive() && latestAttempt.current === attempt); } catch { /* The saved answer remains authoritative. */ } } if (bound.standing === 'producer-settled' && bound.claimId && !announced.current.has(bound.claimId) && owns()) { announced.current.add(bound.claimId); toast.success(activationNotice(bound)); } } catch (error) { if (!owns()) return; const pending = attempt.sent || check || readActivationReference(storage, key, held.child).kind !== 'none'; const text = attempt.expired && !attempt.sent ? 'This took too long to prepare. Nothing was sent; try again.' : relayServerFailure(error, pending ? 'The activation could not be confirmed. Check status using the original email.' : 'Nothing was sent. Please try again.'); setForm({ opening, email: held.email, pending, busy: false, error: text, ...(pending ? { answer: unknownActivation(held.child, claimId) } : {}) }); if (owns() && (error as { status?: number })?.status === 403) options.onSettled?.(text); } finally { clearTimeout(timer); attempt.controller.signal.removeEventListener('abort', abort); if (owns()) { active.current = null; setForm(current => current.opening === opening ? { ...current, busy: false } : current); } } }; return { email: shown.email, setEmail, submitting: shown.busy, error: shown.error, answer: shown.answer, notice: shown.answer ? activationNotice(shown.answer) : shown.pending ? 'An earlier activation needs checking. Enter the original email, then check its status.' : undefined, checking: shown.pending, canStartNew: shown.answer?.canStartNew === true, startNew, submit, retire, emailValid: EMAIL_PATTERN.test(shown.email.trim()) && Boolean(viewer) }; }