/** * AuthGate — the ONE await the application makes before it renders. * * Signing in is `ensureIdentity()` and nothing else: it takes no identifier, * resolves who we are and does the connection work (restoring what others shared * with us, draining our inboxes). Whatever a user has to see or do while that * resolves — opening the wallet, loading it onto a first-time device — belongs to * the SDK: it mounts a full-screen barrier of its own on every top-level load and * takes it down itself, and it owns the return from the broker round-trip (the * barrier comes back prefilled, and confirming it hands the page over a second * time; our page is never reloaded and nothing outside the barrier is touched). * Festipod renders no access screen of its own and re-drives nothing. * * TWO CALLS, IN ORDER, AND THE ORDER IS CONTRACTUAL: start the session (`init`), * then await `ensureIdentity()`. A session arrives only through `init`, and * `ensureIdentity()` awaited before it has been called THROWS. The order is a fact * of the statement sequence below, not of React's effect ordering — which would * get it wrong: this gate's effect runs BEFORE its parent provider's. * * It hands back WHO WE ARE, and this is the app's only upstream answer to that * question — everything else it knows about the user it has to read first. The * value is published for display (`shared/utils/currentPrincipal`) and goes * nowhere near a data call: no call takes an identity, because the session * already belongs to one user. * * Nothing of the app renders before that await settles: a screen mounted earlier * would read as an identity that is not yet settled. * * AND NOTHING RENDERS IF IT FAILS. A rejected `ensureIdentity()` is not a mode the * app degrades through: an app that could not sign in but still shows its screens * is indistinguishable from an app whose user simply owns nothing — a total * failure wearing the face of an empty account. So the rejection is SHOWN, and the * children stay unmounted, which is also what keeps the data layer from settling * on its empty stand-in provider for the rest of the session. */ import { useEffect, useState, type ReactNode } from 'react'; import { ensureIdentity } from '@ng-eventually/polyfill'; import { startNgSession } from '../shared/utils/ngSession'; import { setCurrentPrincipal } from '../shared/utils/currentPrincipal'; import { useRouter, useNavigate } from './router'; export function AuthGate({ children }: { children: ReactNode }) { const { route } = useRouter(); const navigate = useNavigate(); // Whether the ONE identity await has resolved. const [identityReady, setIdentityReady] = useState(false); // Why it did NOT resolve. Set once, never cleared: signing in is attempted once. const [signInError, setSignInError] = useState(null); useEffect(() => { let cancelled = false; // FIRST — a session arrives only through the SDK's `init`. Idempotent, so the // provider above may have started it already; what matters is that it has been // called before the await below, or the await throws. void startNgSession(); void ensureIdentity() .then(principal => { // Publish who we are BEFORE anything renders — the identity is a fact of // the session, not state of this component, so it is recorded even if the // effect was torn down in between. setCurrentPrincipal(principal); if (!cancelled) setIdentityReady(true); }) .catch(err => { console.error('[Auth] ensureIdentity failed:', err); if (!cancelled) setSignInError(err instanceof Error ? err.message : String(err)); }); return () => { cancelled = true; }; }, []); // Once identified, leave the disconnected welcome screen for the app home. useEffect(() => { if (identityReady && route.page === 'welcome') { navigate('/home'); } }, [identityReady, route.page, navigate]); // Signing in FAILED — say so. The app has nothing legitimate to show, and // showing it anyway would pass a broken session off as an empty one. if (signInError) { return ( ); } // Signing in is not settled yet. Render NOTHING — the SDK's own full-screen // barrier is what is on screen, it put it there and it takes it down. Anything // of ours here would be a second thing competing with it. if (!identityReady) { return null; } // The app. return <>{children}; }