import { useEffect, useState, type ReactNode } from "react"; import { useNavigate } from "react-router-dom"; import { useAuthServiceContext } from "../../../services"; import { authSessionId, credentials } from "../../../services/authStorage"; import { usePatientConnectionsService } from "../../../services/PatientConnectionsService.tsx"; import type { RecordsIssuer } from "../../../services/RecordsConnectionsService.tsx"; import { environment } from "../../../environments/environment"; import { personalConnectionAttempts, sessionOriginOf } from "../../../utils/connectionReturn"; import { isSaveRetired, useSaveScope } from "../../../utils/saveOperation"; import { systemName } from "../RecordsPage/connectionWords.ts"; import { accountOfAccessToken, putOffRecordImportOffer, recordImportOfferKey, recordImportOfferPutOff } from "./recordImportOfferMarker.ts"; export const CONNECTIONS_PATH = "/dashboard/my-records/connections"; const RETURN_PATH = "/dashboard/my-records/connections/return"; /** What is known of the patient's own connections: none open, one underway, or a list that did not say. */ type Recovery = "loading" | "none" | "underway" | "unresolved"; type Issuers = { state: "loading" } | { state: "failed" } | { state: "ready"; issuers: RecordsIssuer[] }; // Only navigates: the patient starts any connection on Connections, so nothing here authorizes or imports. // New choices need the patient's own connections read to the end with none open; a further page or a failed // read proves nothing, so it routes to Connections too. Issuer discovery is a separate read whose failure // never hides recovery. const RecordImportOffer = ({ onLater }: { onLater: () => void }) => { const navigate = useNavigate(); const { currentUser } = useAuthServiceContext(); const me = currentUser?.fhir?.resourceType === "Patient" && currentUser.fhir.id ? "Patient/" + currentUser.fhir.id : undefined; const { listPatientIssuers, personalConnectionsPage } = usePatientConnectionsService({ personal: true }); // A change of viewer or sign-in retires every read started for the one before. const owner = (me ?? "") + "|" + (authSessionId() ?? "") + "|" + environment.sessionPortal + "|" + window.location.origin; const lives = useSaveScope(owner); const [recovery, setRecovery] = useState("loading"); const [listing, setListing] = useState({ state: "loading" }); const [tries, setTries] = useState(0); useEffect(() => { if (!me) return; // An attempt this sign-in started is known without asking anyone. const owned = startedHere(me); setRecovery(owned ? "underway" : "loading"); setListing({ state: "loading" }); const life = lives.start(); const scope = { admit: life.admit, signal: life.transport.signal, organizationId: "" }; void (async () => { try { const [issuers, page] = await life.step(() => Promise.allSettled([listPatientIssuers(scope), personalConnectionsPage(scope)])); if (!life.belongs()) return; setListing(issuers.status === "fulfilled" ? { state: "ready", issuers: issuers.value } : { state: "failed" }); if (owned) return; if (page.status === "rejected") setRecovery("unresolved"); else if (page.value.connections.some((row) => row.status !== "disconnected")) setRecovery("underway"); else setRecovery(page.value.cursor ? "unresolved" : "none"); } catch (failure) { if (isSaveRetired(failure) || !life.belongs()) return; setListing({ state: "failed" }); if (!owned) setRecovery("unresolved"); } })(); return () => life.retire(); }, [me, owner, tries, lives, listPatientIssuers, personalConnectionsPage]); const toConnections = ( ); return (

Bring your records into Akashic

If you use another health system's patient portal, you can bring the records it shares into Akashic. You sign in with that portal and choose to allow access. You do not need an account there to use Akashic.

{(recovery === "loading" || (recovery === "none" && listing.state === "loading")) && (

Checking which health systems you can connect…

)} {recovery === "underway" && (

You have already started connecting a health system. Check it in your connections.

)} {recovery === "unresolved" && (

Your connections could not all be checked here. Check them in your connections before adding another.

)} {recovery !== "loading" && listing.state === "failed" && (

The health systems you can connect could not be loaded.

)} {recovery === "none" && listing.state === "ready" && listing.issuers.length === 0 && (

No health system can be connected yet.

)}
{(recovery === "underway" || recovery === "unresolved") && toConnections} {recovery === "none" && listing.state === "ready" && listing.issuers.map((issuer) => ( ))} {recovery !== "loading" && listing.state === "failed" && ( )}
); function startedHere(actor: string): boolean { const view = credentials().current(); const epoch = authSessionId(); const sessionOrigin = view.state === "signed-in" ? sessionOriginOf(view.session.tuple.accessToken) : undefined; if (view.state !== "signed-in" || !epoch || !sessionOrigin) return false; return personalConnectionAttempts({ scope: { kind: "personal", patientId: actor.slice("Patient/".length) }, actor, epoch, sessionOrigin, portal: environment.sessionPortal, clientId: view.session.tuple.clientId, returnOrigin: window.location.origin, returnPath: RETURN_PATH, }).length > 0; } }; /** * The slot's records occupant: the offer, unless this account put it off here, or no account can be read to key * that to - then whatever the slot would otherwise hold. */ export const RecordImportOfferSlot = ({ otherwise }: { otherwise: ReactNode }) => { const { currentUser } = useAuthServiceContext(); const patientId = currentUser?.fhir?.resourceType === "Patient" ? currentUser.fhir.id : undefined; const view = credentials().current(); const account = view.state === "signed-in" ? accountOfAccessToken(view.session.tuple.accessToken) : undefined; const key = patientId && account ? recordImportOfferKey({ account, patientId, portal: environment.sessionPortal, origin: window.location.origin }) : undefined; const [putOffFor, setPutOffFor] = useState(); if (!key || putOffFor === key || recordImportOfferPutOff(key)) return <>{otherwise}; return { putOffRecordImportOffer(key); setPutOffFor(key); }} />; }; export default RecordImportOffer;