import { useCallback } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; // Patient-initiated change of the login (sign-in) email. Three sequential server steps: // request -> a code is emailed to the CURRENT address (proves nothing yet; opens the change) // verify-current -> that code proves control of the current credential (the security gate) // verify-new -> a code emailed to the NEW address confirms it, then the server commits // The server owns all verification and the Cognito write; this is a thin transport, like // DuplicateMatchService. fhirBaseApi already ends in /R4, so paths are appended bare. const API_URL = environment.fhirIdentityApi; export type LoginEmailStage = 'pending-current' | 'pending-new' | 'committing' | 'committed' | 'refused' | 'expired' | 'none'; /** The opening a request belongs to: admit throws once it is retired, and signal cancels what was already sent. */ export interface LoginEmailTransport { admit: () => void; signal: AbortSignal; } export interface LoginEmailChangeRequestResponse { pendingChangeId: string; maskedCurrentEmail: string; stage: LoginEmailStage; replayed?: boolean; } export interface LoginEmailChangeVerifyCurrentResponse { maskedNewEmail: string; stage: LoginEmailStage; } export interface LoginEmailChangeVerifyNewResponse { status: 'changed'; maskedNewEmail: string; // false => the sign-in email changed in Cognito but the Patient tag failed to re-sync. // The change still succeeded; surface a softer note rather than an error. tagSynced?: boolean; // 'pending' => the account changed but its receipt is still being recorded; nothing more is owed by the patient. receipt?: 'pending'; } export interface LoginEmailChangeCancelResponse { status: 'cancelled'; pendingChangeId: string; } export interface LoginEmailChangeResendResponse { // Whichever address the server actually re-sent to - the current one while the change is still // proving control of it, the new one after that. The client is told, never asked. maskedEmail: string; stage: LoginEmailStage; } export interface LoginEmailChangeStatusResponse { pendingChangeId: string; stage: LoginEmailStage; maskedCurrentEmail?: string; maskedNewEmail?: string; checking?: boolean; tagSynced?: boolean; } const optionsOf = (transport?: LoginEmailTransport) => (transport ? { admit: transport.admit, signal: transport.signal } : {}); export const useLoginEmailChangeService = () => { const { post } = useHttpService(); // operationId names this one change: a repeat with the same id and address answers with the same change. const requestChange = useCallback((newEmail: string, operationId: string, transport?: LoginEmailTransport): Promise => { return post(`${API_URL}/patient-login-email/request`, { newEmail, operationId }, optionsOf(transport)); }, [post]); const verifyCurrent = useCallback((pendingChangeId: string, code: string, transport?: LoginEmailTransport): Promise => { return post(`${API_URL}/patient-login-email/verify-current`, { pendingChangeId, code }, optionsOf(transport)); }, [post]); // Without a code this continues a change already admitted for this account; it never starts one. const verifyNew = useCallback((pendingChangeId: string, code: string | undefined, transport?: LoginEmailTransport): Promise => { return post(`${API_URL}/patient-login-email/verify-new`, code ? { pendingChangeId, code } : { pendingChangeId }, optionsOf(transport)); }, [post]); // Cancels exactly the named change, and only while it is still unproven; a change being applied is refused. const cancelChange = useCallback((pendingChangeId: string, transport?: LoginEmailTransport): Promise => { return post(`${API_URL}/patient-login-email/cancel`, { pendingChangeId }, optionsOf(transport)); }, [post]); // Another copy of the code for the change already in flight. NOT a step: the server re-issues // against the record's current status, so this never advances the flow, and the request carries no // address - which address to mail is the server's decision, not the caller's. const resendCode = useCallback((pendingChangeId: string, transport?: LoginEmailTransport): Promise => { return post(`${API_URL}/patient-login-email/resend`, { pendingChangeId }, optionsOf(transport)); }, [post]); // Reads where one of the caller's own changes stands. It changes nothing. const changeStatus = useCallback((pendingChangeId: string, transport?: LoginEmailTransport): Promise => { return post(`${API_URL}/patient-login-email/status`, { pendingChangeId }, optionsOf(transport)); }, [post]); return { requestChange, verifyCurrent, verifyNew, cancelChange, resendCode, changeStatus, }; };