import { useCallback, useEffect, useRef, useState } from 'react'; import { useAuthServiceContext } from '../services/AuthService'; import { isMsaAcceptance, isMsaOperationRead, useBAAService, type MsaOperationRead } from '../services/BAAService'; import { SaveRetiredError, isSaveRetired } from '../utils/saveOperation'; import { readHeldMsa, sameMsaRequest, writeHeldMsa, type HeldMsaOperation } from '../utils/msaRecovery'; import { MSA_ACCEPTED_EVENT, useMsaReadScope } from './useMsaOwnerEntry'; export type MsaRecoveryLocator = { organizationId: string; actorId: string; practitionerId: string; operationId: string; held?: HeldMsaOperation }; const CHECK = 'Your acceptance could not be confirmed yet. Check the original acceptance again before continuing.'; /** A hint opens only this act. Resume checks it again and sends only its server-frozen command. */ export const useMsaRecovery = (locator: MsaRecoveryLocator, onConfirmed?: () => void) => { const { wpOrganization } = useAuthServiceContext(); const { getMsaOperation, acceptMsa } = useBAAService(); const { key: viewerKey, start } = useMsaReadScope(locator.organizationId); const { organizationId, actorId, practitionerId, operationId, held } = locator; const key = [viewerKey, practitionerId, operationId, held ? JSON.stringify(held.request) : 'discovered'].join('|'); const rendered = useRef(key); rendered.current = key; const confirmed = useRef(onConfirmed); const settled = useRef(); const [state, setState] = useState<{ read?: MsaOperationRead; busy: boolean; error?: string }>({ busy: true }); const originalActor = useCallback((read: MsaOperationRead) => read.actorIsOriginal === true && read.actor?.userId === actorId && read.actor.practitionerId === practitionerId, [actorId, practitionerId]); const begin = useCallback(() => { const attempt = start(); const belongs = () => attempt.belongs() && rendered.current === key && wpOrganization.current?.id === organizationId; const admit = () => { if (!belongs()) { attempt.operation.retire(); throw new SaveRetiredError(); } if (held) { const current = readHeldMsa(organizationId, actorId); if (current === 'unavailable') throw new Error(CHECK); if (current && !sameMsaRequest(current.request, held.request)) { attempt.operation.retire(); throw new SaveRetiredError(); } } attempt.admit(); }; return { ...attempt, belongs, admit, transport: { ...attempt.transport, admit } }; }, [start, key, organizationId, actorId, held, wpOrganization]); const validate = useCallback((value: unknown): MsaOperationRead => { if (!isMsaOperationRead(value, operationId, organizationId)) throw new Error(CHECK); if (value.actorMayResume && (!originalActor(value) || !value.frozen || (held && !sameMsaRequest(value.frozen.request, held.request)))) throw new Error(CHECK); return value; }, [operationId, organizationId, originalActor, held]); const finish = useCallback((read: MsaOperationRead, admit: () => void) => { admit(); let warning: string | undefined; try { writeHeldMsa(organizationId, actorId, undefined, operationId); } catch { warning = 'The acceptance is confirmed. Its browser reference could not be cleared; check it again after reloading.'; } admit(); setState({ read, busy: false, error: warning }); if (settled.current !== key) { settled.current = key; window.dispatchEvent(new Event(MSA_ACCEPTED_EVENT)); admit(); confirmed.current?.(); } }, [organizationId, actorId, operationId, key]); const check = useCallback(async () => { const { operation, admit, belongs, transport } = begin(); try { admit(); setState({ busy: true }); const value = await operation.step(() => getMsaOperation(operationId, transport, organizationId)); admit(); const read = validate(value); if (read.state === 'accepted' && originalActor(read)) finish(read, admit); else setState({ read, busy: false }); } catch (error) { if (!isSaveRetired(error) && belongs()) setState({ busy: false, error: CHECK }); } finally { operation.retire(); } }, [begin, getMsaOperation, operationId, organizationId, validate, originalActor, finish]); useEffect(() => { void check(); }, [check]); const resume = async () => { if (state.busy) return; const { operation, admit, belongs, transport } = begin(); try { admit(); setState({ busy: true }); const read = validate(await operation.step(() => getMsaOperation(operationId, transport, organizationId))); admit(); if (read.state === 'accepted' && originalActor(read)) { finish(read, admit); return; } if (!read.actorMayResume || !originalActor(read) || !read.frozen?.preview) { setState({ read, busy: false, error: CHECK }); return; } const request = read.frozen.request; const answer = await operation.step(() => acceptMsa(request, transport, organizationId), true); admit(); if (!isMsaAcceptance(answer, request, organizationId)) throw new Error(CHECK); finish({ state: 'accepted', operationId, version: answer.version, acceptedAt: answer.acceptedAt, documentKey: answer.documentKey, actor: read.actor, actorMayResume: false, actorIsOriginal: true }, admit); } catch (error) { if (!isSaveRetired(error) && belongs()) setState({ busy: false, error: CHECK }); } finally { operation.retire(); } }; const read = state.read; const actorMatches = read ? originalActor(read) : false; const canResume = !state.busy && actorMatches && read?.state === 'admitted' && read.actorMayResume && Boolean(read.frozen?.preview); const canOpen = actorMatches && (read?.state === 'accepted' || (read?.state === 'admitted' && read.actorMayResume)); return { locator, ...state, actorMatches, canResume, canOpen, check, resume }; }; export type MsaRecoveryOpening = ReturnType;