import { createContext, useCallback, useContext, ReactNode } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; /** * - inviting the practice's own patients to the portal. * * DELIBERATELY NOT PART OF THE CAMPAIGN SERVICE. Spencer, 2026-08-14: *"it should be an option * separate from the marketing campaign feature since it's a core needed aspect of onboarding."* A portal * invitation is OPERATIONAL - the practice this person already attends telling them their own record is * online - and a campaign is PROMOTIONAL, needing opt-in and suppression. Conflating them breaks both * ways: invitations get blocked by an opt-in nobody gave, or marketing inherits an exemption it must * never have. */ // The invitation routes are declared in `02-fhir-base.yaml`, the same stack as the sending-identity // routes OrganizationService already calls - so the same gateway. Getting this wrong does not 404 // visibly: an undeclared path answers the CORS preflight without an allow-origin header, so it fails as // `net::ERR_FAILED` with no status at all. const API_URL = environment.fhirIdentityApi; export type SendingDomainState = 'verified' | 'pending' | 'failed' | 'not-configured'; export interface InvitationStatus { sendingDomain: SendingDomainState; running: boolean; startedAt?: string; sent: number; } export const usePatientPortalInviteService = () => { const { get, post } = useHttpService(); /** * How the practice's invitation run is going - and, first, whether it may invite at all. * * It answers even when no run exists, because the screen's opening question is about the sending * DOMAIN rather than about a run. A UI that had to start one to find out would make Spencer's * sequence impossible: import *"we can invite these patients once your email domain is set up"* * invite. */ const fetchInvitationStatus = useCallback( async (): Promise => get(`${API_URL}/patient-portal-invitations`), [get], ); /** * Start inviting. * * IT RETURNS AS SOON AS THE RUN IS STARTED, not when the invitations are sent. A practice with * 2,000 patients is paced over days to protect its sending reputation, so there is no "done" to * wait for here - the caller polls `fetchInvitationStatus`. */ const startInvitations = useCallback( async (): Promise => post(`${API_URL}/patient-portal-invitations`, {}), [post], ); return { fetchInvitationStatus, startInvitations }; }; const PatientPortalInviteServiceContext = createContext< ReturnType | undefined >(undefined); export const PatientPortalInviteServiceProvider = ({ children }: { children: ReactNode }) => ( {children} ); export const usePatientPortalInviteServiceContext = () => { const context = useContext(PatientPortalInviteServiceContext); if (context === undefined) { throw new Error( 'usePatientPortalInviteServiceContext must be used within a PatientPortalInviteServiceProvider', ); } return context; };