import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Building2 } from 'lucide-react'; import { usePracticeInvitationService, type PracticeInvitation } from '../../services/PracticeInvitationService.tsx'; import { useAuthServiceContext } from '../../services/AuthService'; import { authSessionId } from '../../services/authStorage'; import { environment } from '../../environments/environment'; import { startSaveOperation, isSaveRetired, SaveRetiredError, type SaveOperation, type SaveTransport } from '../../utils/saveOperation'; import { decodePracticeInvitation, readInvitationMarker, retainedInvitationMarkers, writeInvitationMarker, type InvitationMarker } from '../../utils/practiceInvitationRecovery'; const keyOf = (invitation: PracticeInvitation) => invitation.organizationId + ':' + (invitation.instance ?? invitation.createdAt); interface OpeningProps { accountId: string; installation: string; belongs(): boolean; fetchMyPracticeInvitations: ReturnType['fetchMyPracticeInvitations']; respondToPracticeInvitation: ReturnType['respondToPracticeInvitation']; } const InvitationOpening: React.FC = ({ accountId, installation, belongs, fetchMyPracticeInvitations, respondToPracticeInvitation }) => { const [opening] = useState(() => crypto.randomUUID()); const [invitations, setInvitations] = useState([]); const [busy, setBusy] = useState>({}); const [messages, setMessages] = useState>({}); const [settled, setSettled] = useState>({}); const live = useRef(true); const operations = useRef(new Set()); const busyRef = useRef(new Map()); const rowsRef = useRef(invitations); rowsRef.current = invitations; const marker = (invitation: PracticeInvitation) => invitation.instance ? readInvitationMarker(accountId, installation, invitation.organizationId, invitation.instance) : 'unavailable'; const checkKnownMembership = (value: PracticeInvitation) => { const previous = rowsRef.current.find(row => keyOf(row) === keyOf(value)); const held = marker(value); const knownOperation = previous?.operationId ?? (held && held !== 'unavailable' ? held.operationId : undefined); if (knownOperation && knownOperation !== value.operationId) throw new Error('Original invitation operation changed'); const known = previous?.membership ?? (held && held !== 'unavailable' && held.operationId === value.operationId ? held.membership : undefined); if (known && value.membership && (known.practitionerId !== value.membership.practitionerId || known.practitionerRoleId !== value.membership.practitionerRoleId || known.platformManagerRoleId !== value.membership.platformManagerRoleId)) throw new Error('Original membership proof changed'); }; const begin = useCallback(() => { const operation = startSaveOperation(() => live.current && belongs()); operations.current.add(operation); const admit = () => { if (!live.current || !belongs()) throw new SaveRetiredError(); operation.admit(); }; return { operation, admit, transport: { admit, signal: operation.transport.signal } as SaveTransport }; }, [belongs]); useEffect(() => { live.current = true; return () => { live.current = false; operations.current.forEach(operation => operation.retire()); operations.current.clear(); }; }, []); const merge = (fresh: PracticeInvitation[]) => { fresh.forEach(checkKnownMembership); const byKey = new Map(rowsRef.current.filter(value => value.operationId || marker(value)).map(value => [keyOf(value), value])); const receipts: Record = {}; for (const value of fresh) { const key = keyOf(value), previous = byKey.get(key); const held = marker(value); if (value.status === 'declined' || value.outcome === 'completed') { if (held === 'unavailable') throw new Error('Original recovery storage is unavailable'); if (held && held.state !== 'settled') { if (!writeInvitationMarker({ ...held, state: 'settled', ...(value.operationId ? { operationId: value.operationId } : {}), ...(value.membership ? { membership: value.membership } : {}) }, held.attempt)) throw new Error('Original recovery changed'); } if (previous || held && held.state !== 'settled') receipts[key] = value.status === 'declined' ? `${value.organizationName} was turned down. Nothing was added to your account.` : `Your original acceptance at ${value.organizationName} is complete. Sign out and back in to check current access.`; byKey.delete(key); continue; } const observed = held && held !== 'unavailable' && held.operationId === value.operationId ? held.membership : undefined; byKey.set(keyOf(value), previous?.membership && !value.membership ? { ...previous, currentStanding: 'unavailable' } : observed && !value.membership ? { ...value, membership: observed, outcome: 'membership-confirmed', currentStanding: 'unavailable' } : value); } rowsRef.current = [...byKey.values()]; setInvitations(rowsRef.current); if (Object.keys(receipts).length) setSettled(current => ({ ...current, ...receipts })); }; const load = useCallback(async () => { const { operation, admit, transport } = begin(); try { const fresh = await operation.step(() => fetchMyPracticeInvitations(transport)); admit(); const decoded = fresh.map(value => decodePracticeInvitation(value, true)); admit(); merge(decoded); } catch (error) { if (isSaveRetired(error) || !operation.belongs()) return; // Known progress remains reachable when its later read is unavailable. if (rowsRef.current.length || retainedInvitationMarkers(accountId, installation).length) setMessages(current => ({ ...current, discovery: 'Progress could not be refreshed. Your original invitation is retained.' })); } finally { operations.current.delete(operation); operation.retire(); } }, [begin, fetchMyPracticeInvitations]); useEffect(() => { void load(); }, [load]); const answer = async (invitation: PracticeInvitation, decision: 'accept' | 'decline' | 'continue') => { const key = keyOf(invitation); if (busyRef.current.has(key) || !invitation.instance) return; const { operation, admit, transport } = begin(); const attempt = crypto.randomUUID(); busyRef.current.set(key, attempt); setBusy(current => ({ ...current, [key]: attempt })); setMessages(current => ({ ...current, [key]: '' })); let held: InvitationMarker | undefined; let priorMarker: InvitationMarker | undefined; let retained = false; const ownMarker = () => { const current = marker(invitation); return current && current !== 'unavailable' && current.state !== 'settled' && current.attempt === attempt; }; try { admit(); let selected = invitation; const prior = marker(invitation); if (prior === 'unavailable') throw new Error('Invitation recovery storage is unavailable'); priorMarker = prior; if (decision === 'accept' && prior && prior.state !== 'unsent') throw new Error('Check the original invitation before continuing'); if (decision === 'continue') { const fresh = await operation.step(() => fetchMyPracticeInvitations(transport)); admit(); const found = fresh.map(value => decodePracticeInvitation(value)).find(value => value.organizationId === invitation.organizationId && value.instance === invitation.instance); if (!found?.operationId || found.operationId !== invitation.operationId || prior?.operationId && prior.operationId !== found.operationId) throw new Error('The original operation could not be confirmed'); checkKnownMembership(found); if (prior && !prior.operationId && !writeInvitationMarker({ ...prior, operationId: found.operationId, state: 'known', ...(found.membership ? { membership: found.membership } : {}) }, prior.attempt)) throw new Error('Original admission recovery changed'); selected = found; } const knownMembership = selected.membership ?? invitation.membership ?? prior?.membership; held = { version: 1, accountId, installation, organizationId: invitation.organizationId, instance: invitation.instance, opening, attempt, ...(selected.operationId ? { operationId: selected.operationId } : {}), state: selected.operationId ? 'known' : 'unsent' }; if (knownMembership) held.membership = knownMembership; admit(); if (!writeInvitationMarker(held, prior?.attempt)) throw new Error('Invitation recovery could not be retained'); retained = true; const ownedTransport: SaveTransport = { ...transport, admit: () => { admit(); if (!ownMarker() || busyRef.current.get(key) !== attempt) throw new SaveRetiredError(); }, onSend: () => { admit(); if (!held || !ownMarker()) throw new SaveRetiredError(); if (!writeInvitationMarker({ ...held, state: 'unconfirmed' }, attempt)) throw new Error('Invitation dispatch could not be retained'); } }; const result = await operation.step(() => respondToPracticeInvitation(invitation.organizationId, decision, { instance: selected.instance, ...(selected.operationId ? { operationId: selected.operationId } : {}) }, ownedTransport), true); admit(); if (!ownMarker()) throw new SaveRetiredError(); const decoded = decodePracticeInvitation(result); checkKnownMembership(decoded); if (decoded.organizationId !== invitation.organizationId || decoded.instance !== invitation.instance || decision === 'continue' && decoded.operationId !== selected.operationId) throw new Error('Original invitation answer is unconfirmed'); admit(); if (decision === 'decline' && decoded.status !== 'declined' || decision !== 'decline' && (decoded.status !== 'accepted' || !decoded.operationId)) throw new Error('Invitation decision is unconfirmed'); const done = decoded.status === 'declined' || decoded.outcome === 'completed'; if (!writeInvitationMarker({ ...held, ...(decoded.operationId ? { operationId: decoded.operationId } : {}), ...(decoded.membership ? { membership: decoded.membership } : {}), state: done ? 'settled' : 'known' }, attempt)) throw new Error('Original recovery marker changed'); admit(); if (done) { setInvitations(current => current.filter(value => keyOf(value) !== key)); setSettled(current => ({ ...current, [key]: decoded.status === 'declined' ? `${invitation.organizationName} was turned down. Nothing was added to your account.` : `Your original acceptance at ${invitation.organizationName} is complete. Sign out and back in to check current access.` })); } else merge([decoded]); } catch (error) { const currentMarker = marker(invitation); const failedBeforeRetention = !retained && (!currentMarker || currentMarker === 'unavailable' || currentMarker.attempt === priorMarker?.attempt); if (!isSaveRetired(error) && operation.belongs() && (failedBeforeRetention || ownMarker())) setMessages(current => ({ ...current, [key]: 'The result is not confirmed. Check the original invitation before doing anything further.' })); } finally { if (operation.belongs() && busyRef.current.get(key) === attempt) { busyRef.current.delete(key); setBusy(current => { if (current[key] !== attempt) return current; const next = { ...current }; Reflect.deleteProperty(next, key); return next; }); } operations.current.delete(operation); operation.retire(); } }; const undiscovered = retainedInvitationMarkers(accountId, installation).filter(held => !invitations.some(row => row.organizationId === held.organizationId && row.instance === held.instance)); if (!invitations.length && !Object.keys(settled).length && !undiscovered.length) return null; return
{undiscovered.map(held =>

{held.membership ? 'Your original administrator membership is recorded. Current access has not been checked. Some setup may still need to finish.' : 'The original invitation is retained. Check its progress before continuing.'}

)} {invitations.map(invitation => { const key = keyOf(invitation), held = marker(invitation), waiting = held && held !== 'unavailable' && held.state !== 'unsent' && (held.state !== 'settled' || invitation.status === 'pending'); const uncertain = invitation.status !== 'pending' || waiting || invitation.outcome === 'held'; const canContinue = invitation.operationId && invitation.outcome !== 'completed' && (!held || held !== 'unavailable' && held.state !== 'settled' && (!held.operationId || held.operationId === invitation.operationId)); return

{invitation.organizationName} was set up with your email address

{invitation.status === 'pending' && !uncertain ?

Someone created this practice with your email address and named you its administrator, responsible for its staff, settings, and billing. Accept to take this on. Decline and nothing changes; your account stays exactly as it is.

:

{invitation.membership ? 'Your original administrator membership is recorded. Current access has not been checked. Some setup may still need to finish.' : 'The original invitation is retained. Check its progress before continuing.'}

}
{!uncertain && <> } {canContinue && }
{invitation.reason === 'legacy-assistance' &&

This earlier acceptance needs assistance to confirm its original work.

} {messages[key] &&

{messages[key]}

}
; })} {messages.discovery &&

{messages.discovery}

} {Object.entries(settled).map(([key, message]) =>

{message}

)}
; }; const PracticeInvitationNotice: React.FC = () => { const { currentUser, currentOrganization, wpOrganization, actingAsSessionRef } = useAuthServiceContext(); const { fetchMyPracticeInvitations, respondToPracticeInvitation } = usePracticeInvitationService(); const service = useRef({ fetchMyPracticeInvitations, respondToPracticeInvitation, generation: 0 }); if (service.current.fetchMyPracticeInvitations !== fetchMyPracticeInvitations || service.current.respondToPracticeInvitation !== respondToPracticeInvitation) { service.current = { fetchMyPracticeInvitations, respondToPracticeInvitation, generation: service.current.generation + 1 }; } const accountId = currentUser?.cognito?.sub; const session = authSessionId(); const epoch = actingAsSessionRef.current; const practice = wpOrganization.current?.id; const organization = currentOrganization?.id; const storageScope = () => JSON.stringify([window.location.origin, environment.credentialStack, environment.sessionPortal, environment.orgAuthorizationApi, session, practice, organization]); const installation = storageScope(); const scope = [accountId, session, epoch, practice, organization, installation, service.current.generation].join('|'); const rendered = useRef(scope); rendered.current = scope; const belongs = useCallback(() => rendered.current === scope && authSessionId() === session && actingAsSessionRef.current === epoch && wpOrganization.current?.id === practice && storageScope() === installation, [scope, session, epoch, practice, installation, actingAsSessionRef, wpOrganization]); return accountId && session ? : null; }; export default PracticeInvitationNotice;