import { createContext, useContext, useMemo, useState, type ReactNode } from 'react'; // // - ONE ANSWER TO "IS THE MANDATORY AGREEMENT GATE STILL OUTSTANDING?", so that ordering is // decided by DATA and not by paint order. // // THE OBSERVED DEFECT, on the first screen every new patient meets: the Terms of Service gate and an // ORGANIZATION-authorization dialog were open at the same time, and the dismissible one was on top. // `elementFromPoint()` at the centre of the Terms gate's **Accept** returned the other dialog's overlay; // dismissing that dialog with "Not now" flipped it back. One variable, measured both ways. // // AND THE MECHANISM IS WHY A Z-INDEX FIX WOULD BE THE WRONG ONE: both dialogs carry `z-index: 1100`, so // the winner was decided by **DOM order alone**. That is a tie broken by accident, and it could invert on // any render change. Raising one z-index keeps the accident and merely picks a side. // // THE DEFECT IS THE SEQUENCE, NOT THE OVERLAP. A patient was asked to let an organization into their // health record before they had accepted - or even been able to READ - Akashic's own Terms. If they decline // the Terms they should not have an account at all, yet they had already been offered an authorization // decision. That is wrong independently of any stacking. // // SO THE RULE IS: the mandatory gate resolves FIRST, and nothing optional speaks until it has. This // module is the channel. `FirstSignInGate` publishes what its own fetch found; anything that would render a // competing dialog reads `useFirstSignInGateStatus()` and stays silent until it is `'clear'`. // // THE DEFAULT WITHOUT A PROVIDER IS `'clear'`, DELIBERATELY, AND THE WIRING IS PINNED SEPARATELY. The // alternative - defaulting to `'outstanding'` - fails safe in production but silently hides the org prompt // in every test and every surface that does not mount the provider, which is a worse failure to debug than // the one being fixed. Instead the App-level wiring is asserted against source in // `__tests__/theMandatoryGateGoesFirst.test.tsx`, exactly as the gate's own mount position already is. // Do not "simplify" this by removing that test: without it, this file is a mechanism nothing reaches. // // 'unknown' is a real, distinct state and must never be collapsed into 'clear'. It means the gate has not // finished asking - and something that mounts during that window is exactly what produced the stacked // dialogs, because "nothing is showing yet" and "nothing is owed" look identical for one render. export type FirstSignInGateStatus = 'unknown' | 'outstanding' | 'clear'; type ContextValue = { status: FirstSignInGateStatus; publish: (status: FirstSignInGateStatus) => void; }; const FirstSignInGateStatusContext = createContext(undefined); export const FirstSignInGateStatusProvider = ({ children }: { children: ReactNode }) => { const [status, setStatus] = useState('unknown'); const value = useMemo(() => ({ status, publish: setStatus }), [status]); return ( {children} ); }; /** * What a competing surface should do. Render nothing - and fetch nothing - unless this is `'clear'`. * * Returns `'clear'` when no provider is mounted, so an isolated component under test behaves exactly as * it did before this card. See the header for why that default is the right one and where the real wiring * is guarded. */ export const useFirstSignInGateStatus = (): FirstSignInGateStatus => useContext(FirstSignInGateStatusContext)?.status ?? 'clear'; /** * The status only where a gate is actually mounted: undefined without a provider, so a surface that needs the * real first-sign-in answer never reads the isolated default as "cleared". */ export const useMountedFirstSignInGateStatus = (): FirstSignInGateStatus | undefined => useContext(FirstSignInGateStatusContext)?.status; /** * The gate's own side of the channel. A no-op when no provider is mounted, so the gate's 16 existing tests * render it standalone unchanged. */ export const usePublishFirstSignInGateStatus = (): ((status: FirstSignInGateStatus) => void) => { const context = useContext(FirstSignInGateStatusContext); return context?.publish ?? NOOP; }; const NOOP = () => {};