import React, { useEffect, useRef, useState } from 'react'; 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 { Spacer } from '../../../../components/ui/spacer'; import { Modal } from '../../../../components/ui/wp-modal-shim'; import { useAuthServiceContext } from '../../../../services/AuthService'; import { BAA_SIGNING_ATTESTATION, BaaRecord, BaaUploadAnswer, UploadBaaRequest, useBAAService, } from '../../../../services/BAAService.tsx'; import { HttpError } from '../../../../services/HttpService.tsx'; import { relayServerFailure } from '../../../../utils/fetchFailureMessage'; import { isSaveRetired, startSaveOperation } from '../../../../utils/saveOperation'; import { viewerEpoch, writeThroughViewer } from '../../../../utils/writeThroughHold'; import { baaUploadInputsDigest, clearBaaUploadReference, fileSha256, newAttemptId, saveBaaUploadReference, type BaaUploadReference, type PendingDraftRef, type TransferResult, } from '../../../../utils/baaUploadAttempt'; import { formatPhoneNumber } from '../../../../utils/phoneNumber'; interface BaaUploadModalProps { onClose: () => void; onUploaded: (record: BaaRecord) => void; /** * Whether an agreement is already in force for this practice. * * IT CHANGES WHAT IS TRUE, not the tone. A practice replacing a live agreement keeps its portal * throughout; a practice with no agreement is waiting on this one to get a portal at all. One * sentence cannot be true for both, and the wrong one is worse than vague: told their portal * "will be activated once the review is complete", a practice whose portal is already serving * patients reads a threat. */ agreementInForce: boolean; /** The draft awaiting review as shown when this opened, or null: what a new attempt names as replaced. */ shownPending: PendingDraftRef | null; /** The admission count shown when this opened; null when it could not be read, so nothing is sent. */ pendingGeneration: number | null | undefined; /** The attempt reached a settled outcome the page must re-read to show; closes this window. */ onSettled: (note: string) => void; } /** This opening's attempt: frozen at its first send, and never retargeted by anything shown since. */ interface LiveAttempt { reference: BaaUploadReference; fileSha256: string; /** Every file send this attempt made, in order. One unknown send rules out automatic withdrawal for good. */ transfers: TransferResult[]; admitted?: PendingDraftRef; } const SETTLED_NOTES: Record, string> = { withdrawn: 'That submission was withdrawn.', replaced: 'That submission was replaced by a newer one.', executed: 'That submission was counter-signed and is now in force.', retired: 'That submission was retired before it arrived. You can send it again.', 'not-admitted': 'That submission never arrived. You can send it again.', }; const BaaUploadModal: React.FC = ({ onClose, onUploaded, agreementInForce, shownPending, pendingGeneration, onSettled, }) => { const { wpOrganization, currentUser } = useAuthServiceContext(); const { requestUpload, recoverUpload, uploadToS3, getBaa, withdrawPendingBaa } = useBAAService(); const mounted = useRef(true); useEffect(() => () => { mounted.current = false; }, []); const attempt = useRef(null); // Numbers each press, so a finished or retired press can only release what it set itself. const presses = useRef(0); const orgPhone = formatPhoneNumber(wpOrganization.current?.telecom?.find((t) => t.system === 'phone')?.value ?? ''); const fileInputRef = useRef(null); const [form, setForm] = useState>({ signerName: '', signerTitle: '', orgName: wpOrganization.current?.name ?? '', orgAddress: '', orgPhone, }); const [selectedFile, setSelectedFile] = useState(null); const [checkedAuth, setCheckedAuth] = useState(false); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(null); const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0] ?? null; if (file && file.type !== 'application/pdf') { setError('Only PDF files are accepted.'); setSelectedFile(null); return; } setSelectedFile(file); setError(null); }; const handleUpload = async () => { if (!form.signerName || !form.signerTitle || !form.orgName || !form.orgAddress || !form.orgPhone) { setError('All fields are required.'); return; } if (!selectedFile) { setError('Please select a PDF file to upload.'); return; } if (!checkedAuth) { setError('You must confirm you are authorized to submit this agreement.'); return; } const practiceId = wpOrganization.current?.id; const viewer = writeThroughViewer(); const actorUserId = currentUser?.cognito?.sub; if (!practiceId || !viewer || !actorUserId) { setError('We could not confirm who is submitting. Reload and try again.'); return; } // Everything this submit starts belongs to this press of this window, this practice and this viewer epoch, // all taken before the first await: a viewer that changes and changes back is a new epoch, not this one. const press = ++presses.current; const epoch = viewerEpoch(); const live = () => mounted.current && presses.current === press && viewerEpoch() === epoch && wpOrganization.current?.id === practiceId && writeThroughViewer() === viewer; const settle = (note: string) => { if (attempt.current) clearBaaUploadReference(practiceId, attempt.current.reference.attemptId); attempt.current = null; onSettled(note); }; setIsUploading(true); setError(null); let firstSend = false; try { const checksum = await fileSha256(selectedFile); if (!live()) return; const inputsDigest = await baaUploadInputsDigest({ ...form, fileName: selectedFile.name, fileSha256: checksum }); if (!live()) return; let current = attempt.current; if (current && current.reference.inputsDigest !== inputsDigest) { setError('Your earlier submission is not settled yet. Close this window to check it before sending a different one.'); return; } let answer: BaaUploadAnswer; if (current) { // The same attempt again: asked what became of it, never sent as a new admission. answer = await recoverUpload(current.reference, startSaveOperation(live).transport); } else { if (typeof pendingGeneration !== 'number') { setError('We could not check the agreement awaiting review. Close this window and try again.'); return; } const reference: BaaUploadReference = { schema: 1, practiceId, viewer, actorUserId, attemptId: newAttemptId(), inputsDigest, admissionGeneration: pendingGeneration, expectedDraft: shownPending ?? null, }; try { saveBaaUploadReference(reference); } catch { setError('This browser could not keep a record of the submission, so it was not sent.'); return; } current = attempt.current = { reference, fileSha256: checksum, transfers: [] }; firstSend = true; answer = await requestUpload({ ...form, fileName: selectedFile.name, fileSha256: checksum, // The checkbox above is the claim, and this is what carries it off the browser. A tick // that never leaves the page is not an attestation -- there is not even a stored true. // The server writes its own copy of this version's wording onto the agreement, so the // evidence is exactly the sentence that was displayed. signingAttestation: { statementVersion: BAA_SIGNING_ATTESTATION.version }, }, reference, startSaveOperation(live).transport); } if (!live()) return; firstSend = false; if (answer.outcome !== 'admitted') { settle(SETTLED_NOTES[answer.outcome]); return; } current.admitted = { documentKey: answer.documentKey as string, signedAt: answer.signedAt as string }; if (!answer.uploadUrl) { setError('Your submission was received, but the file could not be sent just now. Try again.'); return; } const transfer = await uploadToS3(answer.uploadUrl, selectedFile, current.fileSha256, startSaveOperation(live).transport); if (!live()) return; current.transfers.push(transfer); if (transfer === 'stored') { clearBaaUploadReference(practiceId, current.reference.attemptId); attempt.current = null; try { const record = await getBaa(startSaveOperation(live).transport); if (live()) onUploaded(record); } catch { // The file is known to have arrived; only the status read failed. if (live()) onSettled('Your agreement was sent. Its status could not be refreshed; reload to see it.'); } return; } if (transfer === 'refused' && current.transfers.every((sent) => sent === 'refused')) { // Storage refused every send of this attempt before keeping anything, so the draft has no // file. Exactly that draft is withdrawn; nothing newer can be touched. try { await withdrawPendingBaa(current.admitted, startSaveOperation(live).transport); if (live()) settle('Upload failed. Nothing is awaiting review. Please try again.'); } catch { if (live()) settle('Upload failed, and the submission could not be withdrawn. It is shown as awaiting review; withdraw it there.'); } return; } setError('We could not confirm your file arrived. Try again, or close this window: the submission is shown as awaiting review and can be withdrawn there.'); } catch (e: unknown) { if (!live() || isSaveRetired(e)) return; const code = e instanceof HttpError ? (e.body as { code?: string } | undefined)?.code : undefined; const definitelyRefused = firstSend && e instanceof HttpError && (e.status === 400 || e.status === 403 || code === 'pending-changed'); if (definitelyRefused || code === 'pending-changed') { // Refused before anything was admitted: the attempt is over, and the page reads the current state. settle(relayServerFailure(e, 'That submission was not accepted. Please try again.')); return; } if (code === 'admitted-url-unavailable' && attempt.current) { const body = (e as HttpError).body as PendingDraftRef; attempt.current.admitted = { documentKey: body.documentKey, signedAt: body.signedAt }; } setError(relayServerFailure(e, 'We could not confirm your submission was received. Try again to check it.')); } finally { if (mounted.current && presses.current === press) setIsUploading(false); } }; const set = (field: keyof typeof form) => (value: string) => setForm((prev) => ({ ...prev, [field]: value })); return ( {/* Cancel removed: the Modal's onRequestClose IS `onClose`, the same function reference this called. */} , ]} >
{error && ( <> setError(null)}> {error} )}

Upload your organization's own Business Associate Agreement. Akashic will review and counter-sign it.{' '} {agreementInForce // The same promise the replace screen made a click earlier and the pending // notice repeats a click later. Three screens, one sentence. ? 'Your current agreement stays in force while we review, so your patient portal keeps working throughout.' : 'Your patient portal will be activated once the review is complete.'}

{selectedFile && (

{selectedFile.name}

)}
); }; export default BaaUploadModal;