import { useEffect, useRef, useState } from 'react'; import { useAkashicAgreementsServiceContext, type AgreementKind, type OutstandingAgreement, } from '../../services/AkashicAgreementsService.tsx'; import { useAuthServiceContext } from '../../services/AuthService.tsx'; import { HttpError, UnreadableWriteAnswerError } from '../../services/HttpService.tsx'; import { recordSessionEnd } from '../../services/sessionEndReason.ts'; import { isPatientExperience } from '../../utils/mountContext.ts'; import { blocksShowTheirText, deriveBody, resolveCopy } from './agreementCopy.ts'; import type { AgreementCopy } from './agreementCopy.ts'; import { AgreementBlocks } from './AgreementBlocks.tsx'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog.tsx'; import { DemoGateRoleSwitch } from '../../pages/Demo/DemoRoleSwitch'; import { useActAs } from '../../hooks/useActAs.tsx'; import { usePublishFirstSignInGateStatus } from './firstSignInGateStatus.tsx'; import { onViewerChange, viewerEpoch } from '../../utils/writeThroughHold.ts'; import { BirthDateSaveRetired, BirthDateSaveUnconfirmed } from '../../services/ownBirthDateOutcome.ts'; import { signupBirthDateFor } from '../../utils/signupDetailsHandoff.ts'; import { environment } from '../../environments/environment'; import { leaveSandboxMode, reloadPage } from '../../environments/sandboxMode'; // (first-sign-in gate, Plan B), client Task 3 - the gate itself. This is the product's // FIRST genuine hard block: every existing dialog in this codebase (OrgAuthorizationPrompt, the // analogue this mirrors for fetch/state shape) is dismissible on purpose, and `ui/dialog`'s // `DialogContent` defaults `showCloseButton` to `true` for exactly that reason. None of that is // safe to inherit here - a user who hasn't accepted Akashic's current Terms of Service (and, on // the patient-portal surface, the Privacy Notice & Consent) must not be able to Escape, click // outside, or find a close button past this dialog. The only ways out are Accept and Decline (both // record a decision), or - on a copy/version mismatch, where there is nothing honest to record - a // plain Sign out or Try again, neither of which records anything. Text we failed to show still holds // the person here: a published version stays owed until it can be read and answered. // // GATED ON `currentUser` FROM THE FIRST RENDER - no fetch, no dialog, until the user is known. // An authenticated fetch fired from an app-wrapping provider before auth resolves is a banked trap // in this codebase (reference-app-provider-auth-gate): on a token refresh, `currentUser` is // momentarily null, and firing `getOutstanding` in that window turns a page refresh into a // logout -> /login loop the instant the resulting 401 is mishandled anywhere upstream. Building // this gated from the start avoids ever debugging that loop here. // // A failed read (network/5xx, or an answer without the list) fails CLOSED: an unanswered question is // not "nothing owed", so the gate holds a blocking dialog with Try again and a way out, and records nothing. const todayIso = () => new Date().toISOString().slice(0, 10); // YYYY-MM-DD, a real day, from 1900 and not after today; the server checks the same. const isEnterableBirthDate = (value: string): boolean => { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); if (!match) return false; const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])]; const date = new Date(Date.UTC(year, month - 1, day)); const real = date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day; return real && year >= 1900 && value <= todayIso(); }; const FirstSignInGate = () => { const { currentUser, logout } = useAuthServiceContext(); const { getOutstanding, recordDecision, recordOwnBirthDate } = useAkashicAgreementsServiceContext(); // - who is actually answering. `actingAs` is set while a guardian has switched INTO a // dependent's account, and it is PERSISTED, so it survives a reload exactly as the gate does. const { actingAs, exit } = useActAs(); // - the ordering channel. Everything optional stays silent until this says 'clear'. const publishGateStatus = usePublishFirstSignInGateStatus(); // `undefined` = not yet fetched (or nothing to fetch, because there's no user). A real, possibly // empty, array means the aggregator answered. Keeping these distinct means "haven't asked yet" // never gets confused with "asked, and nothing is owed". const [outstanding, setOutstanding] = useState(undefined); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // A failed read is not "nothing owed": the gate stays up with Try again until the server answers. const [unreachable, setUnreachable] = useState(false); const [attempt, setAttempt] = useState(0); // The signed-in person's own missing birth date, entered here when the Privacy notice cannot be answered without it. const [birthDateDraft, setBirthDateDraft] = useState(''); const [savingBirthDate, setSavingBirthDate] = useState(false); const [birthDateError, setBirthDateError] = useState(null); // The save in flight, owned by the viewer who started it; anything else that answers is ignored. const birthDateSave = useRef<{ epoch: number; controller: AbortController } | null>(null); // The accept/decline in flight, owned by the viewer and the exact item it answered; a late result // for a different viewer, or for an item no longer first in line, changes nothing. // The subject is part of the item: a guardian's own Terms and a child's at the same version are two answers. const decisionRef = useRef<{ epoch: number; kind: AgreementKind; version?: string; subject?: string; controller: AbortController } | null>(null); // Each viewer, act-as or practice has its own read of what is owed; a change starts a new one. const [readScope, setReadScope] = useState(0); // The read in flight and the viewer it was made for; only it may change what the gate shows. const agreementsRead = useRef<{ epoch: number; controller: AbortController } | null>(null); // After an unconfirmed save, nothing is resent until the server has said what is owed now. const [recovering, setRecovering] = useState(false); // A typed date and its save belong to one viewer: a viewer, act-as or practice change, or leaving, retires both. useEffect(() => { const retire = () => { agreementsRead.current?.controller.abort(); agreementsRead.current = null; birthDateSave.current?.controller.abort(); birthDateSave.current = null; decisionRef.current?.controller.abort(); decisionRef.current = null; setBirthDateDraft(''); setBirthDateError(null); setSavingBirthDate(false); setRecovering(false); setOutstanding(undefined); setUnreachable(false); setReadScope((n) => n + 1); }; const stop = onViewerChange(retire); return () => { stop(); birthDateSave.current?.controller.abort(); birthDateSave.current = null; decisionRef.current?.controller.abort(); decisionRef.current = null; }; }, []); const surface = isPatientExperience() ? 'patient-portal' : 'staff'; // Keyed on the COGNITO SUB, not `currentUser` itself. `currentUser` is real identity today only // because the memo chain behind it (AkashicAgreementsService.tsx:64 -> HttpService.tsx -> // AuthService.tsx's getValidToken) happens to hold - but the context VALUE that provides // `currentUser`/`getOutstanding` is itself unmemoized, so a future regression anywhere in that // chain (one of them loses its useCallback) hands this effect a new object identity on every // render with the SAME user underneath. Keying on the whole object would turn that into an // unbounded fetch loop against the aggregator; keying on the sub means the effect only re-runs // on a genuine identity change (login/logout), which is the only thing that should ever matter // here. const userKey = currentUser?.cognito?.sub ?? null; useEffect(() => { if (!currentUser) { // No refetch-on-logout residue: if a session ends while a stale outstanding list is // still in state, drop it rather than let a from-cache render slip past the identity // gate above once currentUser goes null. setOutstanding(undefined); setUnreachable(false); return; } const read = { epoch: viewerEpoch(), controller: new AbortController() }; agreementsRead.current = read; // Retired the moment the viewer changes, not when React later cleans this effect up. const current = () => agreementsRead.current === read && viewerEpoch() === read.epoch && !read.controller.signal.aborted; (async () => { try { const response = await getOutstanding(surface, { signal: read.controller.signal }); if (!current()) return; if (!Array.isArray(response?.outstanding)) throw new Error('No agreement list in the answer'); setUnreachable(false); setOutstanding(response.outstanding); setRecovering(false); // The typed date is kept only while the answer in front of the person still needs it. if (response.outstanding[0]?.ageUnknown !== true) { setBirthDateDraft(''); setBirthDateError(null); } } catch (err) { if (!current()) return; console.error('Failed to load outstanding agreements; the gate stays closed:', err); setUnreachable(true); } finally { if (current()) setSubmitting(false); } })(); return () => { read.controller.abort(); if (agreementsRead.current === read) agreementsRead.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps -- deliberately keyed on `userKey` // (a stable primitive), not `currentUser` (an object whose identity is not guaranteed // stable by every link in the chain that produces it) — see the comment above `userKey`. }, [userKey, getOutstanding, surface, attempt, readScope]); // outstanding[0] only - the server's order is authoritative (TOS before privacy-consent) and // this component must never re-sort it. const current = outstanding && outstanding.length > 0 ? outstanding[0] : undefined; const guardianAccepts = current?.acceptedByGuardian === true; const forChild = current?.forPatient; // A guardian answers a minor's privacy consent, and the minor still reads the notice. const isPrivacy = current?.kind === 'privacy-consent'; const ageUnknown = current?.ageUnknown === true; // Only for the person's own record: never while switched into someone else or answering for a child. const ownBirthDateMissing = ageUnknown && !actingAs && !forChild; const birthDateValid = isEnterableBirthDate(birthDateDraft); // The date this same address typed at signup fills the empty field once per read; only Save records it. const signupBirthDate = ownBirthDateMissing ? signupBirthDateFor(currentUser?.cognito?.email ?? '') : undefined; useEffect(() => { if (signupBirthDate && isEnterableBirthDate(signupBirthDate)) setBirthDateDraft((draft) => draft || signupBirthDate); }, [signupBirthDate, readScope]); const answerUnavailable = current?.answerUnavailable; // - PUBLISHED FROM THE SAME STATE THE DIALOG RENDERS FROM, so the answer other surfaces // read can never disagree with what is actually on screen. Deriving it anywhere else - a second fetch, // a duplicate flag - would reintroduce exactly the window this card is about: the two can differ for a // render, and one render is all the stacked dialogs needed. // `undefined` maps to 'unknown', NOT to 'clear'. "Has not finished asking" and "nothing is owed" look // identical for that first render, and treating them the same is the bug. const saveOwnBirthDate = async () => { if (!birthDateValid || !ownBirthDateMissing || recovering || birthDateSave.current) return; const operation = { epoch: viewerEpoch(), controller: new AbortController() }; birthDateSave.current = operation; setSavingBirthDate(true); setBirthDateError(null); let outcome: 'saved' | 'unconfirmed' | 'failed' | 'retired'; try { await recordOwnBirthDate(birthDateDraft, { signal: operation.controller.signal }); outcome = 'saved'; } catch (err) { outcome = err instanceof BirthDateSaveRetired ? 'retired' : err instanceof BirthDateSaveUnconfirmed ? 'unconfirmed' // Already recorded is done. : (err as { status?: number } | null)?.status === 409 ? 'saved' : 'failed'; } // Only the save still current, for the same viewer, may change this gate. if (birthDateSave.current !== operation || viewerEpoch() !== operation.epoch || operation.controller.signal.aborted) return; birthDateSave.current = null; setSavingBirthDate(false); if (outcome === 'saved') { setBirthDateDraft(''); setAttempt((n) => n + 1); } else if (outcome === 'unconfirmed') { // It may have been recorded, so the server is asked before anything is sent again. setBirthDateError('We could not confirm your date of birth was saved. If you are still asked for it, save it again.'); setRecovering(true); setAttempt((n) => n + 1); } else if (outcome === 'failed') { setBirthDateError('Your date of birth could not be saved. Try again.'); } }; const gateStatus = unreachable ? 'outstanding' : outstanding === undefined ? 'unknown' : current ? 'outstanding' : 'clear'; useEffect(() => { publishGateStatus(gateStatus); }, [gateStatus, publishGateStatus]); if (currentUser && unreachable) { return ( e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} className="max-w-2xl" aria-modal="true" data-testid="first-sign-in-gate-unreachable" > We couldn’t check your agreements Akashic Health Records opens once it knows which agreements you still need to answer.

We could not check whether you have agreements to answer, so nothing was recorded. Try again in a moment.

); } if (!currentUser || !current) { return null; } // - THE GUARDIAN IS NOT THE PERSON THIS AGREEMENT IS ASKING // // OBSERVED: a guardian switched into a dependent whose privacy notice was unanswered, and the gate // opened over the switched-in view. Hit-testing every visible control found exactly TWO reachable: the // agreement's own **Decline** and **Accept**. Exit, Dashboard, every link - all intercepted by the // overlay. And a full page reload did not release it, because the acting-as state is persisted: the // guardian could not get back to their own record AT ALL, by any route the UI offered. // // THE SHARP PART IS NOT THE LOCKOUT, IT IS WHAT THE TWO REACHABLE THINGS WERE. The only way out was // to accept or decline a legal agreement ON A MINOR'S BEHALF - precisely the decision a guardian may not // be entitled to make, and the product forced one of the two. That is a consent question wearing a UX // bug, and a stuck overlay must not settle it by leaving those as the only affordances. // // SO THIS DOES NOT SIMPLY RE-ENABLE EXIT AND LEAVE ACCEPT SITTING THERE. That would look finished // while keeping the harder question open. Acting as someone else, the gate states the fact and offers // ONLY the way back. // Reinforced by a known limitation: the product cannot yet distinguish `declined` from `never asked`, // so a guardian pressing Decline here would write a state nobody can read back. // // WHAT IS DELIBERATELY UNCHANGED: the gate still blocks the dependent's record until it is answered. // Leaving the switch is not answering it - the dependent signs in themselves and is asked then. // The Akashic terms for a minor are the exception: a guardian accepts those for the named child. if (actingAs && !forChild) { return ( e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} className="max-w-lg" aria-modal="true" data-testid="first-sign-in-gate-acting-as" > {actingAs.subjectName || 'This person'} hasn’t finished signing in yet They must sign in and answer their Akashic agreement themselves. You can return afterward.
{/* The ONLY affordance, and that is the point. */}
); } // THE DOCUMENT THE SERVER RESOLVED, AND THE BUNDLE ONLY WHEN IT SENT NONE. // // The server publishes legal copy and resolves the version it is going to hold this person to; when // it has authored blocks for that version it sends them. Rendering the bundled copy instead meant // two independent sources for one document, agreeing only while both said v0.1-placeholder. The // first publication of real copy would have moved the version the server demands while the screen // still showed the old text - an acceptance recorded against a document the person was never shown, // and nothing on either side would have said so. // // The bundle stays as the fallback rather than being deleted. Absent copy is the server's own // stated ordinary case, and this is a gate nobody can get past, so there has to be something // correct to render when the store has nothing for this version. // // BOTH HALVES OF THE SERVED COPY ARE CHECKED AGAINST EACH OTHER, which is what they are sent for. // The server's own contract says so: blocks are what a client renders, body is the canonical string // the acceptance is recorded against, and sending both exists so "a client can assert // deriveBody(blocks) === body and catch drift in either direction". // // A served copy whose halves disagree is refused rather than rendered, and refused rather than // quietly replaced by the bundle. Rendering it would show one document and record another. Falling // back to the bundle would be worse still: the bundle is a DIFFERENT version, so the person would // read old text while the server holds them to the new one - the exact hole this whole change // closes. With nothing trustworthy to show there is nothing a decision could honestly record, and // the gate's existing refusal is the honest answer. // // The bundle is the fallback for exactly one case: the server sent no copy at all, which is its // stated ordinary state for a version published before the text moved into the store. const served = current.copy; const servedCopy: AgreementCopy | undefined = served && served.blocks?.length && deriveBody(served.blocks) === served.body && blocksShowTheirText(served.blocks) ? { title: served.title, blocks: served.blocks, body: served.body } : undefined; const copy: AgreementCopy | undefined = served ? servedCopy : resolveCopy(current.kind, current.version); // A failure that does not mean "nothing happened": the request may still land, or already have. // A retry is safe either way - the server finishes the same operation - so this re-reads what is // actually outstanding rather than asserting a refusal the server never made. const isUnresolvedDecisionOutcome = (err: unknown): boolean => { if (err instanceof BirthDateSaveUnconfirmed || err instanceof UnreadableWriteAnswerError) return true; if (!(err instanceof HttpError)) return true; // network error: never reaches the server's own answer. if (err.status >= 500) return true; const code = (err.body as { code?: string } | undefined)?.code; const retryable = (err.body as { retryable?: boolean } | undefined)?.retryable; return err.status === 503 && (retryable === true || code === 'ACCEPTANCE_UNCONFIRMED' || code === 'ACCEPTANCE_BUSY' || code === 'ACCEPTANCE_UNRESOLVED'); }; const answers = (item: OutstandingAgreement, attempt: { kind: AgreementKind; version?: string; subject?: string }) => item.kind === attempt.kind && item.version === attempt.version && item.forPatient?.id === attempt.subject; const handleAccept = async () => { if (!current) return; const attempt = { epoch: viewerEpoch(), kind: current.kind, version: current.version, subject: current.forPatient?.id, controller: new AbortController() }; decisionRef.current = attempt; const owns = () => decisionRef.current === attempt && viewerEpoch() === attempt.epoch && !attempt.controller.signal.aborted; setSubmitting(true); setError(null); try { await recordDecision(current.kind, 'accept', surface, forChild?.id, current.version, { signal: attempt.controller.signal }); if (!owns()) return; setOutstanding((prev) => (prev ? prev.filter((item) => !answers(item, attempt)) : prev)); } catch (err) { if (!owns()) return; // A newer version was published while this was open: show it rather than a failure to retry. const body = err instanceof HttpError ? err.body as { code?: string } | undefined : undefined; if (err instanceof HttpError && err.status === 409 && body?.code === 'stale-version') { try { const response = await getOutstanding(surface, { signal: attempt.controller.signal }); if (!owns()) return; setOutstanding(response.outstanding); setError('This agreement changed since you opened it. Please read the current version.'); } catch (reloadErr) { if (!owns()) return; console.error('Failed to reload the current agreement:', reloadErr); setError('Could not record your acceptance. Try again.'); } return; } if (isUnresolvedDecisionOutcome(err)) { try { const response = await getOutstanding(surface, { signal: attempt.controller.signal }); if (!owns()) return; const stillOwed = response.outstanding.some((item) => answers(item, attempt)); setOutstanding(response.outstanding); // Recorded already, under a slow or dropped answer: nothing to retry, and no failure to show. if (stillOwed) setError('We could not confirm your acceptance yet. Please try again.'); } catch (reloadErr) { if (!owns()) return; // Can't say either way - stay unresolved rather than mark it clear or call it a failure. console.error('Failed to confirm agreement acceptance:', reloadErr); setError('We could not confirm your acceptance yet. Please try again.'); } return; } console.error('Failed to record agreement acceptance:', err); setError('Could not record your acceptance. Try again.'); } finally { if (owns()) { setSubmitting(false); decisionRef.current = null; } } }; const handleDecline = async () => { if (!current) return; const attempt = { epoch: viewerEpoch(), kind: current.kind, version: current.version, subject: current.forPatient?.id, controller: new AbortController() }; decisionRef.current = attempt; const owns = () => decisionRef.current === attempt && viewerEpoch() === attempt.epoch && !attempt.controller.signal.aborted; setSubmitting(true); setError(null); try { // Record FIRST. The decline is the evidence this person was asked and said no; a // sign-out that runs before it lands would lose that evidence, not merely delay it - // logout() clears local session state and reloads, so there is no "record it after" once // that has happened. await recordDecision(current.kind, 'decline', surface, undefined, current.version, { signal: attempt.controller.signal }); } catch (err) { if (!owns()) return; console.error('Failed to record agreement decline:', err); } finally { if (owns()) setSubmitting(false); } if (!owns()) return; decisionRef.current = null; recordSessionEnd('agreement-declined'); logout(); }; // Declining for a child records the child's refusal, then returns the guardian to their own account. // A decline that was not recorded keeps them here, so leaving never stands in for an answer. const handleChildDecline = async () => { if (!forChild || !current) return; const attempt = { epoch: viewerEpoch(), kind: current.kind, version: current.version, subject: current.forPatient?.id, controller: new AbortController() }; decisionRef.current = attempt; const owns = () => decisionRef.current === attempt && viewerEpoch() === attempt.epoch && !attempt.controller.signal.aborted; setSubmitting(true); setError(null); try { await recordDecision(current.kind, 'decline', surface, forChild.id, current.version, { signal: attempt.controller.signal }); } catch (err) { if (!owns()) return; const body = err instanceof HttpError ? err.body as { code?: string } | undefined : undefined; if (err instanceof HttpError && err.status === 409 && (body?.code === 'stale-version' || body?.code === 'already-accepted')) { try { const response = await getOutstanding(surface, { signal: attempt.controller.signal }); if (!owns()) return; setOutstanding(response.outstanding); if (body.code === 'stale-version') setError('This agreement changed since you opened it. Please read the current version.'); return; } catch (reloadErr) { if (!owns()) return; console.error('Failed to reload the current agreement:', reloadErr); } } console.error('Failed to record the decline for a child:', err); setError(`Could not record the decline for ${forChild.name}. Try again.`); return; } finally { if (owns()) setSubmitting(false); } if (!owns()) return; decisionRef.current = null; if (actingAs) void exit(); else handleSignOut(); }; // Final review fix round, finding 2: on a copy/version mismatch (`copy` undefined, below) there // is no text this user was ever shown, so there is nothing honest for a decision write to record // - offering Decline here (previously ruled a "safe exit") was in fact a WRITE: it filed a legal // record asserting a refusal of unseen text, and did so at the version the SERVER just reported, // not the version bundled in this client. This handler is the only exit on that path: sign out, // no `recordDecision` call, ever. const handleSignOut = () => { logout(); }; // Text this client cannot show is our defect, so it must not lock the person out. Like an // unpublished agreement, they may go on without anything being recorded; it stays outstanding on // the server, so they are asked again at the next sign-in. const handleRetry = async () => { setSubmitting(true); setError(null); try { setOutstanding((await getOutstanding(surface)).outstanding); } catch (err) { console.error('Failed to reload the agreement:', err); setError('We still couldn’t load it. Try again in a moment.'); } finally { setSubmitting(false); } }; return ( e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} className="max-w-2xl" //, the smaller finding on the same element: this is functionally modal and // scroll-locks the body, so a screen reader must be told. It was absent. aria-modal="true" data-testid="first-sign-in-gate-dialog" > {copy ? (forChild ? copy.title + ' for ' + forChild.name : copy.title) : 'We couldn’t display this agreement'} You must respond to this agreement to continue using Akashic Health Records. {forChild && (

{isPrivacy ? `${forChild.name} is under 18, so you answer this privacy consent for them. This covers only ${forChild.name} and is separate from your own.` : `${forChild.name} is under 18, so you accept these terms for them. This covers only ${forChild.name} and is separate from your own acceptance.`}

)} {guardianAccepts && (

{isPrivacy ? 'Because you are under 18, a parent or guardian answers this privacy consent for you. You can read it here. Ask them to sign in to their own account and answer it; you can continue once they have.' : 'Because you are under 18, a parent or guardian accepts these terms for you. Ask them to sign in to their own account and accept them; you can continue once they have.'}

)} {ageUnknown && ownBirthDateMissing && (

We need your date of birth before this privacy consent can be answered. Enter it here to continue.

{ setBirthDateDraft(e.target.value); setBirthDateError(null); }} /> {birthDateError &&

{birthDateError}

}
)} {ageUnknown && !ownBirthDateMissing && (

We need your date of birth before this privacy consent can be answered. Please ask your practice to add it to your record; you can continue once it is there.

)} {answerUnavailable && (

{answerUnavailable === 'guardian-only' ? 'Only a parent or guardian can answer this privacy consent for a child. You can read it here.' : 'We could not confirm who may answer this just now. You can read it here; try again in a moment.'}

)} {guardianAccepts && !isPrivacy ? null : copy ? ( // - one element per AUTHORED block, never a parse of the text. // // THE RULE, because the obvious "improvement" here re-opens: nothing below // inspects the CONTENT of a block. It switches on `block.type`, which the document // declares, and renders `block.text` verbatim as a single child. There is no syntax to // read and therefore nothing that can be mis-read - which is the distinction Spencer // drew when he rejected a markdown renderer and then chose blocks. // Do not add a regex here to "detect" a heading, a bullet or emphasis. That is a // parser, and a parser between the evidentiary record and the screen is the whole risk. // // Paragraph text carries the source document's hard wraps. `

` collapses them, so // the text reflows to the reader's width - while `deriveBody` keeps the wraps in the // canonical string. Both are true at once, which is why the version needed no bump. // - semantic tokens, not `gray-*`. This panel is a RECESSED reading well, // which is exactly what `bg-muted` means in the warm ramp (`--muted` `--sunken`). // The `gray-50/200/700` it replaces were COOL greys sitting next to the saturated // blue - the thing D8 names outright ("never a pure grey next to the blue; an unmixed // grey reads dirty") - and, being literals, they would not have followed the ramp or // dark mode. Two of them were mine, added hours before the ramp landed.

) : ( // Task 2's resolveCopy() returns undefined on ANY version mismatch - wrong version // or unrecognized kind. Offering Accept here would file a legal record claiming // this person read text they were never shown. Decline is NOT offered either // (final review fix round, finding 2): a decline write is still a write, and it // would assert a refusal of the same unseen text at the server's reported // version. The only exit below is "Sign out", which calls logout() directly with // no recordDecision call.

We couldn't load the current version of this agreement, so it can't be answered yet. Try again in a moment.

)} {error && (

{error}

)}
{/* Leaving Sandbox Mode answers nothing: the live gate still asks for its own agreements. */} {environment.inSandboxMode && ( )} {copy && forChild ? ( <> ) : copy && !guardianAccepts && !ageUnknown && !answerUnavailable ? ( <> {/* Signing out answers nothing: the agreement is asked again at the next sign-in. */} ) : !copy || answerUnavailable === 'retry' ? ( <> ) : ( )}
); }; export default FirstSignInGate;