import { HttpError } from './HttpService.tsx'; import { writeThroughViewer } from '../utils/writeThroughHold'; /** * A guardian's add-a-child or invite-a-co-guardian request as the server answers it. The same id sent again finishes the * same request, never a second. Only `complete` is success; `overtaken` means a later decision on the link came first. */ export type GuardianProducerFlow = 'dependent' | 'co-guardian'; export type GuardianProducerStatus = 'in-progress' | 'complete' | 'overtaken'; export interface GuardianProducerAnswer { requestId: string; status: GuardianProducerStatus; phase?: string; /** Only once the server confirmed it exists. */ childPatientId?: string; relatedPersonId?: string; /** For an earlier invitation sent again: whether its email reached the queue. */ invitation?: string; } /** The server's own answer carried on a refusal or an unfinished request, with its code. */ export type GuardianProducerRefusal = Partial & { code?: string; message?: string }; export const guardianProducerAnswerOf = (value: unknown): GuardianProducerAnswer | undefined => { const answer = value as Partial | null | undefined; if (!answer || typeof answer.requestId !== 'string') return undefined; if (answer.status !== 'in-progress' && answer.status !== 'complete' && answer.status !== 'overtaken') return undefined; return answer as GuardianProducerAnswer; }; export const guardianProducerRefusalOf = (error: unknown): GuardianProducerRefusal | undefined => error instanceof HttpError && error.body && typeof error.body === 'object' ? error.body as GuardianProducerRefusal : undefined; // Only the request's opaque id is kept across reopening, for the viewer (account, acted-as link, practice) who made it. // Never the child's or the invitee's name, birth date or email. const STORE_KEY = 'akashic.guardian-request.v1'; type Kept = { viewer: string; flow: GuardianProducerFlow; childId: string; requestId: string }; let inMemory: Kept[] = []; const readKept = (): Kept[] => { try { const raw = window.sessionStorage.getItem(STORE_KEY); const parsed = raw ? JSON.parse(raw) : []; return Array.isArray(parsed) ? parsed.filter((kept: Kept) => typeof kept?.requestId === 'string' && typeof kept?.viewer === 'string') : inMemory; } catch { return inMemory; } }; const writeKept = (kept: Kept[]): void => { inMemory = kept; try { window.sessionStorage.setItem(STORE_KEY, JSON.stringify(kept)); } catch { // The in-memory copy still carries the request through this page. } }; /** The request this viewer left open for this flow and child, if any. */ export const keptGuardianRequest = (flow: GuardianProducerFlow, childId = ''): string | undefined => { const viewer = writeThroughViewer(); if (!viewer) return undefined; return readKept().find((kept) => kept.viewer === viewer && kept.flow === flow && kept.childId === childId)?.requestId; }; /** Kept before the request is sent, so an answer lost to a reload or a closed form still finds it. */ export const keepGuardianRequest = (flow: GuardianProducerFlow, childId: string, requestId: string): void => { const viewer = writeThroughViewer(); if (!viewer) return; writeKept([...readKept().filter((kept) => !(kept.viewer === viewer && kept.flow === flow && kept.childId === childId)), { viewer, flow, childId, requestId }]); }; /** Forgotten only once it is finished, overtaken, or known to have made nothing. */ export const forgetGuardianRequest = (flow: GuardianProducerFlow, childId: string, requestId: string): void => { writeKept(readKept().filter((kept) => !(kept.flow === flow && kept.childId === childId && kept.requestId === requestId))); }; export const newGuardianRequestId = (): string => crypto.randomUUID();