import React, { useMemo, useRef, useState } from 'react'; import { useDirtyReport } from '../../../../hooks/useDirtyReport'; import { CheckboxControl } from '../../../../components/ui/checkbox-control'; import { TextControl } from '../../../../components/ui/text-control'; import { DismissibleAlert } from '../../../../components/ui/dismissible-alert'; import { Flex, FlexItem } from '../../../../components/ui/flex'; import { useAuthServiceContext } from '../../../../services/AuthService'; import { authSessionId } from '../../../../services/authStorage'; import { BAA_SIGNING_ATTESTATION, isMsaAcceptance, useBAAService } from '../../../../services/BAAService.tsx'; import { AgreementBlocks } from '../../../../components/FirstSignInGate/AgreementBlocks'; import { formatFhirHumanName } from '../../../../utils/fhirHumanName.tsx'; import { SaveRetiredError, isSaveRetired, isSaveTimedOut, useSaveScope } from '../../../../utils/saveOperation'; import { msaClosingLines, resolveMsaBlocks, type MsaPreview } from '../../../../utils/msaAgreement'; import type { BaaSignFormParts } from './BaaSignForm'; import { MSA_ACCEPTED_EVENT } from '../../../../hooks/useMsaOwnerEntry'; import { readHeldMsa as readHeld, writeHeldMsa as writeHeld } from '../../../../utils/msaRecovery'; import { MsaHeldForm } from './MsaRecovery'; interface MsaAcceptFormProps { preview: MsaPreview; /** Told once the acceptance is confirmed by the server, including one finished after a lost answer. */ onAccepted: () => void; /** Re-reads what the owner reviews, after the server says the agreement or its details changed. */ onRefresh: () => void; onDirtyChange?: (dirty: boolean) => void; children: (parts: BaaSignFormParts) => React.ReactNode; } const reasonOf = (error: unknown): string | undefined => { const body = (error as { body?: { reason?: unknown } } | null)?.body; return typeof body?.reason === 'string' ? body.reason : undefined; }; const RECORDING = 'Your acceptance could not be confirmed yet. Press Accept again to finish the same acceptance.'; const MsaAcceptFormOpening = ({ preview: currentPreview, onAccepted, onRefresh, onDirtyChange, children }: MsaAcceptFormProps) => { const { wpOrganization, currentUser, actingAsSessionRef } = useAuthServiceContext(); const { acceptMsa } = useBAAService(); const organizationId = wpOrganization.current?.id; const actorId = currentUser?.cognito?.sub; const scope = useSaveScope(organizationId); const [initial] = useState(() => organizationId && actorId ? readHeld(organizationId, actorId) : 'unavailable'); const [held, setHeld] = useState(initial === 'unavailable' ? undefined : initial); const heldRef = useRef(held); const preview = held?.preview ?? currentPreview; const operationId = useRef(held?.request.operationId ?? crypto.randomUUID()); const defaultSigner = currentUser?.fhir?.name?.[0] ? formatFhirHumanName(currentUser.fhir.name[0]) : ''; const [signerName, setSignerName] = useState(held?.request.signerName ?? defaultSigner); const [signerTitle, setSignerTitle] = useState(held?.request.signerTitle ?? ''); const [checkedRead, setCheckedRead] = useState(false); const [checkedBind, setCheckedBind] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(held ? RECORDING : null); const [blocked, setBlocked] = useState(initial === 'unavailable'); useDirtyReport(Boolean(signerTitle.trim()) || checkedRead || checkedBind, onDirtyChange); const renew = () => { if (organizationId && actorId && !writeHeld(organizationId, actorId, undefined, operationId.current)) return false; heldRef.current = undefined; setHeld(undefined); operationId.current = crypto.randomUUID(); return true; }; const start = () => { const operation = scope.start(); const session = authSessionId(); const viewer = actingAsSessionRef.current; const belongs = () => operation.belongs() && wpOrganization.current?.id === organizationId && authSessionId() === session && actingAsSessionRef.current === viewer; const admit = () => { if (!belongs()) { operation.retire(); throw new SaveRetiredError(); } operation.admit(); }; return { operation, admit, belongs, transport: { admit, signal: operation.transport.signal } }; }; const facts = useMemo(() => ({ ...preview.facts, signerName, signerTitle, signDate: preview.signDate }), [preview, signerName, signerTitle]); const blocks = useMemo(() => resolveMsaBlocks(preview.blocks, facts), [preview.blocks, facts]); const missing = [!signerName.trim() && 'Who is accepting', !signerTitle.trim() && 'Your title or role'].filter(Boolean) as string[]; const handleAccept = async () => { if (!organizationId || !actorId || blocked || busy || missing.length > 0 || !checkedRead || !checkedBind) return; const { operation, admit, belongs, transport } = start(); const practice = organizationId; const body = heldRef.current?.request ?? { operationId: operationId.current, signerName: signerName.trim(), signerTitle: signerTitle.trim(), documentVersion: preview.version, editionHash: preview.editionHash, signDate: preview.signDate, factsDigest: preview.factsDigest, signingAttestation: { statementVersion: BAA_SIGNING_ATTESTATION.version }, }; setBusy(true); setError(null); try { admit(); const frozen = heldRef.current ?? { organizationId: practice, actorId, request: body, preview }; if (!writeHeld(practice, actorId, frozen)) { const current = readHeld(practice, actorId); if (current && current !== 'unavailable') { heldRef.current = current; setHeld(current); } else setBlocked(true); setError(RECORDING); return; } heldRef.current = frozen; setHeld(frozen); const accepted = await operation.step(() => acceptMsa(body, transport, practice), true); admit(); if (!isMsaAcceptance(accepted, body, practice)) throw new Error('Unconfirmed acceptance'); if (!writeHeld(practice, actorId, undefined, body.operationId)) throw new SaveRetiredError(); window.dispatchEvent(new Event(MSA_ACCEPTED_EVENT)); onAccepted(); } catch (failure) { if (isSaveRetired(failure) || !belongs()) return; const reason = reasonOf(failure); if (isSaveTimedOut(failure) || reason === 'msa-unavailable') { setError(RECORDING); } else if (reason === 'msa-facts-changed' || reason === 'msa-stale-version') { if (!renew()) { operation.retire(); return; } setCheckedRead(false); setCheckedBind(false); setError(reason === 'msa-stale-version' ? 'A newer agreement was published while this was open. Read the current version, then accept again.' : 'Your organization details or the date changed while this was open. Check them below, then accept again.'); onRefresh(); } else if (reason === 'operation-mismatch') { setBlocked(true); setError('This acceptance reference holds other details. Contact Akashic support; its original reference has been kept.'); } else if (reason === 'msa-copy-conflict') { setBlocked(true); setError('The signed copy could not be confirmed as reviewed. Contact Akashic support; its original reference has been kept.'); } else { setError(heldRef.current ? RECORDING : 'Your recovery reference could not be saved. Check browser storage, then try again.'); } } finally { if (belongs()) setBusy(false); operation.retire(); } }; if (held && !busy) return {children}; if (initial === 'unavailable') return <>{children({ body:

Your previous acceptance could not be read safely. Contact Akashic support before starting another.

, actions: , wide: true, })}; const actions = ( {missing.length > 0 && (

To accept, fill in: {missing.join(', ')}.

)}
); const body = (
{error && ( setError(null)}> {error} )}

{preview.title}

{msaClosingLines(facts).map((line) =>

{line}

)}

The organization details come from your practice profile. It is accepted only when you click below.

); return <>{children({ body, actions, wide: true })}; }; const MsaAcceptForm = (props: MsaAcceptFormProps) => { const { wpOrganization, currentUser, actingAsSessionRef } = useAuthServiceContext(); const key = [wpOrganization.current?.id, currentUser?.cognito?.sub, currentUser?.fhir?.id, authSessionId(), actingAsSessionRef.current].join('|'); return ; }; export default MsaAcceptForm;