import { useEffect, useRef } from 'react'; import type { Patient } from 'fhir/r4'; import { toast } from 'sonner'; import { useAuthServiceContext, usePatientServiceContext } from '../../services'; import { useFirstSignInGateStatus } from '../FirstSignInGate/firstSignInGateStatus'; import { missingDetailsPatch, takeSignupDetailsFor, type SignupDetails } from '../../utils/signupDetailsHandoff'; import { isSaveRetired, outcomeUnknown, useSaveScope } from '../../utils/saveOperation'; import { isRetryableFailure } from '../../utils/fetchFailureMessage'; import { olderThan } from '../../utils/versionOrder'; const RECOVERY_TOAST = 'signup-details-recovery'; /** * Fills in what an invited patient typed at signup and their already-existing account never saved. Runs once, for * the signed-in patient's own record only (never while acting for someone else), after the first sign-in agreements * are answered: the home record is read fresh, and only what it lacks is written, over the version read. * * What it reads and what it saves become the signed-in record at once, so My details, Home and Messages show a date * of birth saved at the gate and a phone saved here without a reload. Nothing is resent on its own. A read that * fails, or a save whose answer is lost, says so and offers Try again, which reads the record first and writes only * what it still lacks; a save that was refused says where to add the phone instead. */ export const SignupDetailsCompletion = () => { const gate = useFirstSignInGateStatus(); const { currentUser, actingAs, setCurrentUser } = useAuthServiceContext(); const { fetchPatient, updatePatient } = usePatientServiceContext(); const fhir = currentUser?.fhir as { resourceType?: string; id?: string; birthDate?: string } | undefined; const ownPatientId = fhir?.resourceType === 'Patient' && !actingAs ? fhir.id : undefined; const email = currentUser?.cognito?.email; // Behind what is stored only after a save at the gate (the date of birth it asks for) or one made here. const missingBirthDate = Boolean(ownPatientId) && !fhir?.birthDate; // Bound to this patient and the person signed in: a change of either retires a completion under way. const scope = useSaveScope(ownPatientId); // The details handed over for this patient, kept only for a Try again in this visit. const handedOver = useRef<{ patientId: string; details?: SignupDetails } | undefined>(undefined); useEffect(() => () => { toast.dismiss(RECOVERY_TOAST); }, [ownPatientId]); useEffect(() => { if (gate !== 'clear' || !ownPatientId) return; // Handed over once: a later run finds nothing, so nothing is written twice on its own. const details = email ? takeSignupDetailsFor(email) : undefined; if (!details && !missingBirthDate) return; handedOver.current = { patientId: ownPatientId, details }; // Only the same signed-in Patient, and never an older version over a newer one. const install = (record: Patient | undefined) => { if (!record?.id) return; setCurrentUser((signedIn) => (signedIn?.fhir?.resourceType === 'Patient' && signedIn.fhir.id === record.id && !olderThan(record, signedIn.fhir) ? { ...signedIn, fhir: record } : signedIn)); }; const complete = async () => { const held = handedOver.current; if (!held || held.patientId !== ownPatientId) return; const operation = scope.start(); let saving = false; try { const stored = await operation.step(() => fetchPatient(ownPatientId, false, undefined, true, operation.transport)); install(stored); const patched = held.details && stored?.meta?.versionId ? missingDetailsPatch(stored, held.details) : undefined; if (patched) { saving = true; install(await operation.step(() => updatePatient(patched, stored.meta?.versionId as string, operation.transport), true)); } toast.dismiss(RECOVERY_TOAST); } catch (error) { if (isSaveRetired(error)) return; if (saving && !outcomeUnknown(error) && !isRetryableFailure(error)) { toast.error('Your phone number from sign-up was not saved. You can add it in My details.', { id: RECOVERY_TOAST, duration: Infinity }); return; } toast.error(saving ? 'We could not confirm your phone number was saved.' : 'Your details could not be loaded just now.', { id: RECOVERY_TOAST, duration: Infinity, action: { label: 'Try again', onClick: () => { void complete(); } }, }); } }; void complete(); // `missingBirthDate` is read when the gate clears, not watched: installing the record must not start another run. // eslint-disable-next-line react-hooks/exhaustive-deps }, [gate, ownPatientId, email, scope, fetchPatient, updatePatient, setCurrentUser]); return null; }; export default SignupDetailsCompletion;