@@ -4,19 +4,22 @@ description: The session gate, the hooks, the ready-made screens, and the test h
44---
55
66[ ` @gopherium/react-auth ` ] ( https://www.npmjs.com/package/@gopherium/react-auth )
7- is the browser half of the authentication stack, speaking authkit's
8- JSON dialect. It ships four entry points so you take only the layers
9- you want:
7+ is the browser half of the authentication stack. It calls the
8+ [ authkit endpoints] ( /authentication/sessions-over-http/ ) for you and
9+ gives your React application everything around a login.
10+
11+ It ships four entry points, so you take only the layers you want:
1012
1113| Entry | Contents |
1214| --- | --- |
13- | ` @gopherium/react-auth ` | Headless core: gate, hooks, api client , typed errors |
15+ | ` @gopherium/react-auth ` | The core: gate, hooks, backend calls , typed errors, transport |
1416| ` @gopherium/react-auth/wpds ` | Ready-made screens on the WordPress Design System |
15- | ` @gopherium/react-auth/admin ` | The user- administration api client |
16- | ` @gopherium/react-auth/testing ` | msw harness, canned handlers , session seeding |
17+ | ` @gopherium/react-auth/admin ` | The user administration calls |
18+ | ` @gopherium/react-auth/testing ` | Test harness, canned responses , session seeding |
1719
18- ` react ` and ` @tanstack/react-query ` are peer dependencies. The ` /wpds `
19- screens additionally expect ` @wordpress/ui ` .
20+ ` react ` and ` @tanstack/react-query ` are peer dependencies, meaning
21+ your application installs them. The ` /wpds ` screens also expect
22+ ` @wordpress/ui ` .
2023
2124## The gate
2225
@@ -32,46 +35,147 @@ const queryClient = createAuthQueryClient()
3235</QueryClientProvider >
3336```
3437
35- ` AuthGate ` resolves the session and renders one of three things: your
36- app when signed in, the login screen when signed out, or the loading
37- and error states, both overridable props. The ` loginScreen ` render
38- prop receives the login-completion handler, so any login UI plugs in.
38+ ` AuthGate ` checks whether there is a session and renders one of four
39+ things: your application when signed in, the login screen when signed
40+ out, and a loading or error state in between. The last two have
41+ default UI you can replace with props.
42+
43+ ` loginScreen ` is a render prop, so you can pass any login UI you
44+ like. It receives a handler to call once the login succeeds.
3945
40- ` createAuthQueryClient ` matters as much as the gate: it builds a query
41- client whose caches drop the session whenever any query or mutation
42- fails with an ` UnauthorizedError ` . A session revoked mid-use brings
43- the login screen back without a reload, from anywhere in the app.
46+ ` createAuthQueryClient ` is easy to skip past, and it matters as much
47+ as the gate. It builds a React Query client that watches for
48+ ` UnauthorizedError ` coming back from any query or mutation, and drops
49+ the cached session when it sees one. The effect is that a session
50+ revoked while someone is working brings the login screen straight
51+ back, from anywhere in the application, with no page reload.
4452
4553## Hooks and the session key
4654
47- - ` useSession() ` reads the cached identity.
48- - ` useLogout() ` logs out and scrubs every cached query except the
49- session itself, so no stale data survives into the next login.
50- - ` sessionQueryKey ` is owned here, at the bottom of the dependency
51- graph. Anything else that touches the session, such as an SSE layer
52- probing whether a dead stream means a revoked login, imports the key
53- and ` isSessionRevoked ` from this package, never the other way
54- around.
55+ - ` useSession() ` reads the signed-in user from the cache.
56+ - ` useLogout() ` logs out and clears every cached query except the
57+ session itself, so one user's data cannot show up after the next
58+ person logs in.
59+ - ` sessionQueryKey ` is the React Query key for the session, and this
60+ package owns it.
61+
62+ That last point matters when other parts of your application need to
63+ care about the session. Say you have a live event stream that dies,
64+ and you want to know whether the cause was a revoked login. That code
65+ imports ` sessionQueryKey ` and ` isSessionRevoked ` from this package.
66+ Dependencies always point this way, never outward, so there is
67+ exactly one definition of what the session is.
5568
5669## Ready-made screens
5770
58- The ` /wpds ` entry covers the whole surface for apps on the WordPress
59- Design System: ` LoginScreen ` with a ` brand ` prop, ` AccountPanel ` for
60- the signed-in identity and logout control, ` UsersScreen ` and
61- ` NewUserScreen ` for administration, and ` usersNavItem ` for your
62- navigation. Router coupling stays out through render props: the users
63- screen takes a ` newUserRender ` element for its create link, and the
64- new-user form reports success through ` onCreated ` so the app decides
65- where to navigate. Import the stylesheet once:
71+ If your application uses the WordPress Design System, the ` /wpds `
72+ entry has every screen a login needs already built:
73+
74+ - ` LoginScreen ` , which takes a ` brand ` prop
75+ - ` AccountPanel ` , showing who is signed in with a logout control
76+ - ` UsersScreen ` and ` NewUserScreen ` for user administration
77+ - ` usersNavItem ` for your navigation
78+
79+ None of them know your router, and they stay that way by handing
80+ navigation back to you. ` UsersScreen ` takes ` newUserRender ` , an
81+ element to use as its create link, so you supply your own link
82+ component. ` NewUserScreen ` calls ` onCreated ` when the user is saved,
83+ and your application decides where to go next.
84+
85+ Import the stylesheet once:
6686
6787``` tsx
6888import ' @gopherium/react-auth/wpds/style.css'
6989```
7090
91+ ## Talking to a backend that is not REST
92+
93+ By default the package calls authkit's REST endpoints directly, such
94+ as ` POST /api/auth/login ` . If your application talks to its backend
95+ some other way, most often GraphQL, you would otherwise be stuck:
96+ the screens and hooks are useful, but the requests they make are
97+ wrong for your server.
98+
99+ ` configureAuthTransport ` replaces those requests. Call it once when
100+ your application starts, before anything renders:
101+
102+ ``` ts
103+ import {
104+ InvalidCredentialsError ,
105+ configureAuthTransport ,
106+ } from ' @gopherium/react-auth'
107+
108+ configureAuthTransport ({
109+ async login(email , password ) {
110+ const result = await graphqlClient .mutation (LoginDocument , { email , password })
111+ if (result .error ) {
112+ throw new InvalidCredentialsError (' invalid credentials' )
113+ }
114+ return result .data .login
115+ },
116+ })
117+ ```
118+
119+ Everything above this line keeps working unchanged. The screens, the
120+ gate, the hooks and the query keys never know the requests moved.
121+
122+ There are seven operations in the ` AuthTransport ` interface:
123+ ` fetchSession ` , ` login ` , ` logout ` , ` isSessionRevoked ` , ` fetchUsers ` ,
124+ ` createUser ` and ` setUserDisabled ` . You override the ones you want
125+ and the rest keep using REST, which makes a gradual migration
126+ possible. Calls to ` configureAuthTransport ` add up rather than
127+ replace each other, so you can configure in more than one place.
128+
129+ ### Matching the error contract
130+
131+ This is the part that is easy to get wrong. The hooks and screens
132+ decide what to show by catching specific error classes, so your
133+ implementation has to throw the same ones the REST version throws.
134+ A generic ` Error ` where the package expects ` InvalidCredentialsError `
135+ turns a wrong password into a crash instead of a form message.
136+
137+ | Operation | Must do this |
138+ | --- | --- |
139+ | ` fetchSession ` | Return ` null ` when signed out, never throw |
140+ | ` login ` | Throw ` InvalidCredentialsError ` for a bad password, ` RateLimitedError ` when the limiter refuses |
141+ | ` fetchUsers ` | Throw ` UnauthorizedError ` when the session is gone |
142+ | ` createUser ` | Throw ` UnauthorizedError ` , ` EmailTakenError ` for a duplicate email, ` ValidationError ` for bad input |
143+ | ` setUserDisabled ` | Throw ` UnauthorizedError ` when the session is gone |
144+
145+ ` InvalidCredentialsError ` , ` RateLimitedError ` and ` UnauthorizedError `
146+ come from the main entry point. ` EmailTakenError ` and
147+ ` ValidationError ` come from ` /admin ` .
148+
149+ ` UnauthorizedError ` deserves particular care, because it is what
150+ [ ` createAuthQueryClient ` ] ( #the-gate ) watches for to drop the session
151+ and bring the login screen back. Throw the wrong class from your
152+ transport and a revoked session goes unnoticed.
153+
154+ ### One setting for the whole module
155+
156+ The transport is stored in a module-level variable, not in React
157+ context, which has two practical consequences.
158+
159+ Configure it once at startup. There is no provider to place and no
160+ way to give different parts of your tree different transports.
161+
162+ More importantly, the setting belongs to one copy of the package. If
163+ your bundle somehow contains two copies of ` @gopherium/react-auth ` ,
164+ each has its own transport, and one of them silently keeps using
165+ REST. Add the package to your bundler's dedupe list along with
166+ ` react ` and ` @tanstack/react-query ` .
167+
168+ ` resetAuthTransport() ` puts every operation back to REST. It exists
169+ for tests, so one spec's transport cannot leak into the next, and
170+ belongs in a ` beforeEach ` .
171+
71172## Testing components that authenticate
72173
73- The ` /testing ` entry owns the msw server and its vitest lifecycle, so
74- app harnesses compose instead of re-creating:
174+ Components behind a login are awkward to test, because every one of
175+ them wants a session and a backend. The ` /testing ` entry provides
176+ both. It runs a fake HTTP server, using
177+ [ msw] ( https://mswjs.io/ ) , and manages that server's setup and
178+ teardown for you:
75179
76180``` tsx
77181import {
@@ -83,13 +187,23 @@ import {
83187} from ' @gopherium/react-auth/testing'
84188```
85189
86- ` installTestEnvironment() ` installs the server lifecycle and DOM
87- cleanup. ` seedSession(client, user) ` renders components signed in
88- without a network round trip. The canned handlers cover every auth
89- endpoint outcome, from ` loginOk ` to ` loginRateLimited ` , so specs
90- declare intent instead of re-typing response literals.
190+ ` installTestEnvironment() ` starts and stops the server around your
191+ tests and cleans up the DOM between them.
192+
193+ ` seedSession(client, user) ` puts a signed-in user straight into the
194+ query cache, so a test can render a component behind the gate without
195+ logging in over the network first.
196+
197+ The canned handlers cover every outcome each endpoint can produce,
198+ from ` loginOk ` to ` loginRateLimited ` . Naming one says what the test
199+ is about, which reads better than pasting a response body into every
200+ spec.
201+
202+ Two things you have to set up on your side:
91203
92- Two integration requirements: register your matcher and stub setup
93- against your own test runner in your harness file, and give your
94- bundler a dedupe rule for ` react ` and ` @tanstack/react-query ` so a
95- workspace-linked copy never splits the query-client context.
204+ - Register the matchers and stubs your own test runner needs, in your
205+ own setup file.
206+ - Tell your bundler to dedupe ` react ` and ` @tanstack/react-query ` .
207+ Without it, a workspace-linked copy of the package can end up with
208+ its own React Query context, and your components will not see the
209+ session you seeded.
0 commit comments