// (related-person financial disclosure), client Task 7 - "may whoever is driving right now see / // pay the displayed patient's invoices?" // // WHY A NEW HOOK: there was no RP-side accessor for the authority grant at all. `GET // /R4/related-person-authority` is PATIENTS-only and self-scoped - a patient reads the grants they have // GIVEN - so a related person acting for someone else cannot read their own grant through it. The // per-subject flags therefore ride along on `GET /my-proxies` (the "accounts I can switch into" list), // which is already caller-scoped and already fetched by the act-as flow. // // THIS IS A UX GATE, NOT A BOUNDARY. The server enforces independently in Invoice/get.ts, // PaymentReconciliation/get.ts and Payment/{checkout,session}.ts. Hiding a surface here is about not // showing someone a page whose every request would 403; it is never what keeps the data safe. import { useAuthServiceContext } from '../services/AuthService.tsx'; import { usePatientServiceContext } from '../services/PatientService.tsx'; import { useCallback, useEffect, useState } from 'react'; import { PROXY_REREAD_DEADLINE_MS, useMyProxies } from './useMyProxies.tsx'; import { resolveBillingAccess } from './billingAccess.ts'; import type { BillingAccess } from './billingAccess.ts'; export type { BillingAccess } from './billingAccess.ts'; export interface CurrentBillingAccess extends BillingAccess { isLoading: boolean; /** Whether the rights shown are known. Only an answered "no" is a denial; before that nothing is granted. */ answered: boolean; /** The helper's rights could not be read, or took too long: say so and offer `retry`, never a denial. */ failed: boolean; retry: () => void; } export const useCurrentBillingAccess = (): CurrentBillingAccess => { const { currentUser } = useAuthServiceContext(); const { patientState } = usePatientServiceContext(); // useMyProxies self-gates on "logged in as a plain Patient" and sends bypassActingAs, so this stays // MY list even while switched into someone else - which is exactly the list we need to look the // displayed patient up in. const { proxies, isLoading, listAnswered, error, refetch } = useMyProxies(); const loggedInPatientId = currentUser?.fhir?.resourceType === 'Patient' ? currentUser.fhir.id : undefined; const displayedPatientId = patientState?.id; // Self is answered on the first render, before the list is read; so are no displayed patient and a session // the list does not serve. A helper's rights are known only once this account's own list has answered: // `listAnswered` is false on a fresh mount and for another account, so an empty or older list never reads as // a "no". Until then access stays withheld, the same as before, but is reported as not yet known. const forSomeoneElse = !!displayedPatientId && !!loggedInPatientId && displayedPatientId !== loggedInPatientId; const answered = !forSomeoneElse || listAnswered; const [timedOutFor, setTimedOutFor] = useState(); useEffect(() => { if (answered || !displayedPatientId) return undefined; const timer = setTimeout(() => setTimedOutFor(displayedPatientId), PROXY_REREAD_DEADLINE_MS); return () => clearTimeout(timer); }, [answered, displayedPatientId, timedOutFor]); const access = resolveBillingAccess(displayedPatientId, loggedInPatientId, answered ? proxies : undefined); const failed = !answered && ((!isLoading && !!error) || timedOutFor === displayedPatientId); const retry = useCallback(() => { setTimedOutFor(undefined); void refetch(undefined, PROXY_REREAD_DEADLINE_MS); }, [refetch]); return { ...access, isLoading, answered, failed, retry }; };