Shows its children only to a signed-in user: it waits visibly, explains a failure with a retry, and sends people to sign in only on a real signed-out answer.
import { AuthGate } from "@meowerse/ui"; · source packages/ui/src/components/AuthGate.tsx
variants and states
checking the session — its first render, on the server and in the browser
try it
the stand-in account service answers "signed in", so the gate shows what it wraps.
signed out isn't shown here: on a real signed-out answer the gate sends people to sign in (location.replace to loginPath), which would take you away from this page.
[wait]working: loading the demo…
demo source
tsx
import { useEffect, useState } from "react";
import { AuthGate, RadioGroup, Spinner, clearSessionCache } from "@meowerse/ui";
// Each state points AuthGate at a static stand-in for the account service under public/ui-demo/, so
// the gate runs for real (fetch, parse, render) at no cost and never touches a real session:
// - signed in: ui-demo/signed-in/api/session is a session JSON in exactly the shape useSession reads.
// (It's served without a JSON content type: fetch's .json() never looks at it, and it's same-origin.)
// - service error: ui-demo/no-account/api/session isn't JSON at all, so the check fails honestly (an
// unreadable answer, not a network error) and the gate shows its error, whose "try again" re-checks.
// - checking: a static host can't hold a request open, so this one is a still of the gate's wait state:
// the same markup AuthGate renders while it waits (an e2e test compares the two).
// Signed out isn't offered: on a real signed-out answer AuthGate leaves the page for loginPath.
const BASES = { "signed-in": "/ui-demo/signed-in", error: "/ui-demo/no-account" } as const;
const NOTES: Record<string, string> = {
"signed-in": "the stand-in account service answers \"signed in\", so the gate shows what it wraps.",
checking: "a still of the wait: a real check shows this until the account service answers (at most 8 s).",
error: "a simulated failure: the stand-in answer isn't one the gate can read. \"try again\" checks again, and fails the same way.",
};
export default function Demo() {
const [k, setK] = useState("signed-in");
// useSession shares one cache and one in-flight check per page: drop both on a switch, so the next
// gate asks its own stand-in instead of reusing the last one's answer.
const pick = (v: string) => { clearSessionCache(); setK(v); };
// ...and on the way out: the stand-in's "signed in" must never outlive the demo as a cached session.
useEffect(() => clearSessionCache, []);
return (
<div className="demo">
<RadioGroup name="demo-gate" legend="account service answers" value={k} onChange={pick} options={[
{ label: "signed in", value: "signed-in" }, { label: "still checking", value: "checking" }, { label: "service error (simulated)", value: "error" },
]} />
<p className="demo__out">{NOTES[k]}</p>
<div className="demo__frame demo__frame--gate">
{k === "checking"
? <div className="mw-gate"><Spinner size="lg" label="checking your session" /></div>
: <AuthGate key={k} base={BASES[k as keyof typeof BASES]}><p>account settings</p></AuthGate>}
</div>
<p className="mw-muted">signed out isn't shown here: on a real signed-out answer the gate sends people to sign in (location.replace to loginPath), which would take you away from this page.</p>
</div>
);
}
props
prop
type
default
required
base
string the origin of the account service to check the session against.
—
yes
children
ReactNode shown once the session is confirmed signed in.
—
yes
loginPath
string where to send a signed-out visitor; the current path is appended as next=.
"/login"
no
css custom properties
variable
dark
light
--c-danger
#ff6b63
#b42318
--c-fg-muted
#b3b3af
#46464a
--c-info
#7cc4ff
#1d5fa8
--c-ok
#3ddc84
#11652f
--c-warn
#ffb454
#8a5200
--fs-3
0.875rem
same
--leading-normal
1.5
same
--sp-2
8px
same
--sp-4
16px
same
Classes: .mw-gate, .mw-status. Set any of these variables on an ancestor (or wrap a subtree in .mw-theme--dark / .mw-theme--light) to restyle just that part of a page.