import { useCallback } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import type { SaveTransport } from '../utils/saveOperation'; import { decodePracticeInvitation, type PracticeInvitation } from '../utils/practiceInvitationRecovery'; export type { PracticeInvitation } from '../utils/practiceInvitationRecovery'; export type PracticeInvitationOutcome = PracticeInvitation; const API_URL = environment.orgAuthorizationApi; const object = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value); export const usePracticeInvitationService = () => { const { get, post } = useHttpService(); const fetchMyPracticeInvitations = useCallback(async (transport?: SaveTransport): Promise => { const invitations: PracticeInvitation[] = []; const cursors = new Set(); const identities = new Set(); let cursor: string | undefined; for (let page = 0; page < 8; page++) { transport?.admit(); const response = await get(`${API_URL}/my-practice-invitations${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''}`, { ...transport, bypassActingAs: true, bypassOrganization: true }); transport?.admit(); if (!object(response) || !Array.isArray(response.invitations)) throw new Error('Invitation discovery is unavailable'); const legacy = response.version === undefined; if (!legacy && (response.version !== 1 || typeof response.complete !== 'boolean')) throw new Error('Invitation discovery proof is unavailable'); if (!legacy && response.complete === true && response.cursor !== undefined) throw new Error('Invitation discovery has contradictory page proof'); const decoded = response.invitations.map(value => decodePracticeInvitation(value, legacy)); for (const invitation of decoded) { const key = invitation.organizationId + ':' + (invitation.instance ?? invitation.createdAt); if (identities.has(key)) throw new Error('Invitation discovery is inconsistent'); identities.add(key); invitations.push(invitation); } transport?.admit(); if (legacy || response.complete === true) return invitations; if (typeof response.cursor !== 'string' || !/^org#[A-Za-z0-9.-]{1,64}$/.test(response.cursor) || cursors.has(response.cursor)) throw new Error('Invitation discovery is incomplete'); cursor = response.cursor; cursors.add(cursor); } throw new Error('Invitation discovery is incomplete'); }, [get]); const respondToPracticeInvitation = useCallback(async (organizationId: string, decision: 'accept' | 'decline' | 'continue', original?: Pick, transport?: SaveTransport): Promise => { if (!/^[A-Za-z0-9.-]{1,64}$/.test(organizationId) || decision === 'continue' && (!original?.instance || !original.operationId)) throw new Error('Original invitation is unavailable'); transport?.admit(); const response = await post(`${API_URL}/my-practice-invitations/${organizationId}/respond`, { decision, ...(original?.instance ? { instance: original.instance } : {}), ...(original?.operationId ? { operationId: original.operationId } : {}) }, { ...transport, bypassActingAs: true, bypassOrganization: true }); transport?.admit(); const result = decodePracticeInvitation(response); if (result.organizationId !== organizationId || original?.instance !== undefined && result.instance !== original.instance || decision === 'continue' && result.operationId !== original?.operationId || decision === 'decline' && result.status !== 'declined' || decision !== 'decline' && (result.status !== 'accepted' || !result.operationId)) throw new Error('Original invitation answer is unconfirmed'); transport?.admit(); return result; }, [post]); return { fetchMyPracticeInvitations, respondToPracticeInvitation }; };