@ic-reactor/react 3.12.5 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +410 -44
- package/dist/auth/auth-client-compat.d.ts +122 -0
- package/dist/auth/auth-client-compat.d.ts.map +1 -0
- package/dist/auth/auth-client-compat.js +162 -0
- package/dist/auth/auth-client-compat.js.map +1 -0
- package/dist/auth/authentication-manager.d.ts +287 -5
- package/dist/auth/authentication-manager.d.ts.map +1 -1
- package/dist/auth/authentication-manager.js +920 -150
- package/dist/auth/authentication-manager.js.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
- package/dist/auth/createIdentityAttributeHooks.js +36 -20
- package/dist/auth/createIdentityAttributeHooks.js.map +1 -1
- package/dist/auth/identity-attributes-manager.d.ts +2 -1
- package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
- package/dist/auth/identity-attributes-manager.js +90 -6
- package/dist/auth/identity-attributes-manager.js.map +1 -1
- package/dist/auth/identity-attributes.d.ts.map +1 -1
- package/dist/auth/identity-attributes.js +57 -0
- package/dist/auth/identity-attributes.js.map +1 -1
- package/dist/auth/local-ii-probe.d.ts +12 -1
- package/dist/auth/local-ii-probe.d.ts.map +1 -1
- package/dist/auth/local-ii-probe.js +22 -3
- package/dist/auth/local-ii-probe.js.map +1 -1
- package/dist/auth/types.d.ts +48 -5
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/createActorHooks.d.ts +7 -18
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createInfiniteQuery.d.ts +51 -10
- package/dist/createInfiniteQuery.d.ts.map +1 -1
- package/dist/createInfiniteQuery.js +39 -15
- package/dist/createInfiniteQuery.js.map +1 -1
- package/dist/createMutation.d.ts +3 -0
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +87 -80
- package/dist/createMutation.js.map +1 -1
- package/dist/createQuery.d.ts +35 -2
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +104 -17
- package/dist/createQuery.js.map +1 -1
- package/dist/createReactorProvider.d.ts +158 -0
- package/dist/createReactorProvider.d.ts.map +1 -0
- package/dist/createReactorProvider.js +256 -0
- package/dist/createReactorProvider.js.map +1 -0
- package/dist/createSuspenseInfiniteQuery.d.ts +16 -9
- package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/createSuspenseInfiniteQuery.js +59 -27
- package/dist/createSuspenseInfiniteQuery.js.map +1 -1
- package/dist/createSuspenseQuery.d.ts +23 -2
- package/dist/createSuspenseQuery.d.ts.map +1 -1
- package/dist/createSuspenseQuery.js +68 -21
- package/dist/createSuspenseQuery.js.map +1 -1
- package/dist/defineDisplayReactor.d.ts +43 -0
- package/dist/defineDisplayReactor.d.ts.map +1 -0
- package/dist/defineDisplayReactor.js +42 -0
- package/dist/defineDisplayReactor.js.map +1 -0
- package/dist/defineReactor.d.ts +46 -72
- package/dist/defineReactor.d.ts.map +1 -1
- package/dist/defineReactor.js +11 -176
- package/dist/defineReactor.js.map +1 -1
- package/dist/defineReactorShared.d.ts +84 -0
- package/dist/defineReactorShared.d.ts.map +1 -0
- package/dist/defineReactorShared.js +139 -0
- package/dist/defineReactorShared.js.map +1 -0
- package/dist/hooks/createAuthHooks.d.ts +7 -0
- package/dist/hooks/createAuthHooks.d.ts.map +1 -1
- package/dist/hooks/createAuthHooks.js +180 -20
- package/dist/hooks/createAuthHooks.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +34 -6
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js +54 -21
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +37 -4
- package/dist/hooks/useActorMethod.d.ts.map +1 -1
- package/dist/hooks/useActorMethod.js +201 -57
- package/dist/hooks/useActorMethod.js.map +1 -1
- package/dist/hooks/useActorMutation.d.ts +10 -7
- package/dist/hooks/useActorMutation.d.ts.map +1 -1
- package/dist/hooks/useActorMutation.js +14 -13
- package/dist/hooks/useActorMutation.js.map +1 -1
- package/dist/hooks/useActorQuery.d.ts +17 -4
- package/dist/hooks/useActorQuery.d.ts.map +1 -1
- package/dist/hooks/useActorQuery.js +30 -9
- package/dist/hooks/useActorQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +15 -3
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js +37 -17
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.d.ts +2 -2
- package/dist/hooks/useActorSuspenseQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseQuery.js +20 -9
- package/dist/hooks/useActorSuspenseQuery.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/ownedAuthentication.d.ts +52 -0
- package/dist/ownedAuthentication.d.ts.map +1 -0
- package/dist/ownedAuthentication.js +49 -0
- package/dist/ownedAuthentication.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +23 -0
- package/dist/server.js.map +1 -0
- package/dist/testing.d.ts +19 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +416 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/utils.d.ts +159 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +301 -1
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +12 -7
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +34 -15
- package/dist/validation.js.map +1 -1
- package/llms.txt +259 -33
- package/package.json +17 -5
- package/src/auth/auth-client-compat.ts +273 -0
- package/src/auth/authentication-manager.ts +918 -96
- package/src/auth/createIdentityAttributeHooks.ts +47 -21
- package/src/auth/identity-attributes-manager.ts +100 -5
- package/src/auth/identity-attributes.ts +75 -0
- package/src/auth/local-ii-probe.ts +29 -3
- package/src/auth/types.ts +49 -6
- package/src/createActorHooks.ts +20 -32
- package/src/createInfiniteQuery.ts +120 -28
- package/src/createMutation.ts +161 -178
- package/src/createQuery.ts +164 -32
- package/src/createReactorProvider.ts +365 -0
- package/src/createSuspenseInfiniteQuery.ts +93 -43
- package/src/createSuspenseQuery.ts +102 -32
- package/src/defineDisplayReactor.ts +62 -0
- package/src/defineReactor.ts +81 -263
- package/src/defineReactorShared.ts +268 -0
- package/src/hooks/createAuthHooks.ts +206 -24
- package/src/hooks/useActorInfiniteQuery.ts +122 -49
- package/src/hooks/useActorMethod.ts +295 -92
- package/src/hooks/useActorMutation.ts +23 -23
- package/src/hooks/useActorQuery.ts +43 -10
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +93 -50
- package/src/hooks/useActorSuspenseQuery.ts +30 -15
- package/src/index.ts +8 -0
- package/src/ownedAuthentication.ts +81 -0
- package/src/server.ts +23 -0
- package/src/testing.ts +18 -0
- package/src/types.ts +463 -14
- package/src/utils.ts +387 -3
- package/src/validation.ts +43 -19
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @ic-reactor/react
|
|
2
2
|
|
|
3
|
+
> **AI coding agents:** read [`llms.txt`](./llms.txt) in this package
|
|
4
|
+
> (`node_modules/@ic-reactor/react/llms.txt`) before writing code with it. It
|
|
5
|
+
> is written for the installed version and lists the patterns to use and the
|
|
6
|
+
> mistakes to avoid.
|
|
7
|
+
|
|
3
8
|
React bindings for IC Reactor. This package re-exports everything from
|
|
4
9
|
`@ic-reactor/core` and adds hook factories, auth hooks, direct reactor hooks,
|
|
5
10
|
and reusable query or mutation factories built around TanStack Query.
|
|
@@ -10,19 +15,80 @@ and reusable query or mutation factories built around TanStack Query.
|
|
|
10
15
|
pnpm add @ic-reactor/react @icp-sdk/core @tanstack/react-query
|
|
11
16
|
|
|
12
17
|
# Optional: Internet Identity login helpers
|
|
13
|
-
pnpm add @icp-sdk/auth@^
|
|
18
|
+
pnpm add @icp-sdk/auth@^10
|
|
14
19
|
```
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
It needs `@tanstack/react-query` 5.90.2 or later and React 18 or later. CI runs
|
|
22
|
+
this package's tests at those minimums (`pnpm verify:peer-floors`).
|
|
23
|
+
|
|
24
|
+
## Which `@icp-sdk/auth` to install
|
|
25
|
+
|
|
26
|
+
The peer range is `^8.0.0 || ^10.0.0`. **v10 is the one to install.** It is the
|
|
27
|
+
first release whose peer is `@icp-sdk/core@^6` — the version this package needs —
|
|
28
|
+
so a strict `npm install` resolves it with no `overrides` block.
|
|
29
|
+
|
|
30
|
+
**v9 is deliberately excluded.** It peers `@icp-sdk/core@^5`, so it reintroduces
|
|
31
|
+
the resolution failure v10 fixes.
|
|
32
|
+
|
|
33
|
+
**v8 still works**, and this repository's real-client suite runs against both
|
|
34
|
+
majors. On npm it still needs the override, because its peer metadata is stale rather
|
|
35
|
+
than the versions being incompatible — auth v8 runs against core v6:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{ "overrides": { "@icp-sdk/auth": { "@icp-sdk/core": "$@icp-sdk/core" } } }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### What changes if you move from v8 to v10
|
|
42
|
+
|
|
43
|
+
IC Reactor keeps one options contract across both majors and translates at the
|
|
44
|
+
boundary, so `identityProvider`, `derivationOrigin`, `windowOpenerFeatures`,
|
|
45
|
+
`transport` and `openIdProvider` are written the same way either way. One
|
|
46
|
+
difference in `identityProvider`: v10 names a provider by its authorize URL
|
|
47
|
+
and the canister that mints its delegations, so a URL you set yourself needs
|
|
48
|
+
`internetIdentityId` on v10 as well. IC Reactor pairs the mainnet URL and its
|
|
49
|
+
own local default with the right canister, and throws for any other URL that
|
|
50
|
+
has none, instead of guessing one. Four
|
|
51
|
+
things genuinely have no v10 equivalent, and IC Reactor warns once on each
|
|
52
|
+
rather than forwarding an option the client ignores:
|
|
53
|
+
|
|
54
|
+
| Option | On v10 |
|
|
55
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `storage` | Dropped. Credentials moved behind `credentialStorage`, whose store also generates identities and holds a delegation alongside each key, so an `AuthClientStorage` cannot be adapted into one. Pass a pre-built `authClient` to keep a custom store. |
|
|
57
|
+
| `keyType` | Dropped. The credential store decides the key type. |
|
|
58
|
+
| `idleOptions` | Dropped. The idle timeout belongs to the identity provider canister; pass `maxTimeToIdle` to `login()` instead. |
|
|
59
|
+
| `identity` | Dropped from constructor options. The agent signs as the session. |
|
|
60
|
+
|
|
61
|
+
Two options exist only on v10: `maxTimeToIdle` on `login()` and
|
|
62
|
+
`disableBrowserActivity` on the client. IC Reactor forwards them to a v10 client
|
|
63
|
+
and drops them on v8 with a one-time warning, since v8 has no equivalent.
|
|
64
|
+
|
|
65
|
+
A v10 client also tells IC Reactor when the session changes in another tab of
|
|
66
|
+
the origin. `AuthenticationManager` follows it: a sign-out in another tab signs
|
|
67
|
+
this tab out, and a sign-in there as another account is adopted here, with no
|
|
68
|
+
call in this tab. A v8 tab notices only when it checks the session again.
|
|
69
|
+
|
|
70
|
+
One difference is security-relevant and warns unconditionally: **`targets` on
|
|
71
|
+
`login()` is ignored by v10.** v8 forwards it to restrict the delegation to named
|
|
72
|
+
canisters; v10 removed it and scopes a session at the identity provider instead.
|
|
73
|
+
A v10 client will hand you a delegation broader than a `targets` list asks for.
|
|
74
|
+
Pin `@icp-sdk/auth` to `^8` if you depend on canister-scoped delegations.
|
|
75
|
+
|
|
76
|
+
> **Support scope.** The real-client suite
|
|
77
|
+
> (`tests/auth/internet-identity-integration.test.ts` and
|
|
78
|
+
> `tests/auth/auth-client-lifecycle.test.ts`) runs the actual `AuthClient`
|
|
79
|
+
> under v10 and under v8. A fake Internet Identity answers both
|
|
80
|
+
> sign-in protocols (`icrc34_delegation` and `ii_session_delegation`). A fake
|
|
81
|
+
> replica certifies v10's mint and revoke calls
|
|
82
|
+
> (`app_prepare_delegation`, `app_get_delegation`, `app_revoke_session`) and
|
|
83
|
+
> checks request signatures the way a replica does, so v10's own minting agent
|
|
84
|
+
> verifies them unchanged.
|
|
22
85
|
>
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
>
|
|
86
|
+
> v10 makes those calls through an agent of its own. Off mainnet, IC Reactor
|
|
87
|
+
> gives that agent the replica your app already uses and has it fetch the
|
|
88
|
+
> network's root key, since certificates from a local replica or testnet cannot
|
|
89
|
+
> be checked against mainnet's. A root key you passed as `agentOptions.rootKey`
|
|
90
|
+
> goes to that agent instead, as your app's own agent keeps it. The suite covers that path. What it does not
|
|
91
|
+
> cover is a deployed Internet Identity canister.
|
|
26
92
|
|
|
27
93
|
`@icp-sdk/auth` is an optional peer. `AuthenticationManager` reaches it through a
|
|
28
94
|
literal `import("@icp-sdk/auth/client")`, so Vite, Rollup and webpack code-split
|
|
@@ -115,6 +181,13 @@ export function App() {
|
|
|
115
181
|
|
|
116
182
|
## Main APIs
|
|
117
183
|
|
|
184
|
+
- `defineReactor(...)` for one-call setup: the `QueryClient`, `ClientManager`,
|
|
185
|
+
a `Reactor`, its actor hooks and the Internet Identity hooks together;
|
|
186
|
+
`defineDisplayReactor(...)` takes the same options and builds a
|
|
187
|
+
`DisplayReactor` instead
|
|
188
|
+
- `createReactorProvider(factory)` for a server-rendered app: a provider that
|
|
189
|
+
builds the reactors once per mounted tree (so once per request on a server)
|
|
190
|
+
and a `useReactor()` hook that returns them fully typed
|
|
118
191
|
- `createActorHooks(reactor)` for per-canister hooks like `useActorQuery` and
|
|
119
192
|
`useActorMutation`
|
|
120
193
|
- `createAuthHooks(authentication)` for `useAuth`, `useAgentState`, and
|
|
@@ -130,13 +203,63 @@ export function App() {
|
|
|
130
203
|
## Choosing the Right Pattern
|
|
131
204
|
|
|
132
205
|
- Use `createActorHooks` for the simplest component-first integration.
|
|
206
|
+
- Use `createReactorProvider` when the app renders on a server (Next.js, any
|
|
207
|
+
React SSR) or a part of the UI needs reactors of its own: module-scope
|
|
208
|
+
reactors are right only for a client-only app.
|
|
133
209
|
- Use query and mutation factories when you also need loader, action, service,
|
|
134
210
|
or test usage through `.fetch()`, `.prefetch()`, `.execute()`, `.invalidate()`,
|
|
135
211
|
`.getCacheData()`, or `.setData()`.
|
|
136
|
-
- Use `DisplayReactor` when you want UI-friendly
|
|
137
|
-
of `bigint` or `Principal`.
|
|
212
|
+
- Use `DisplayReactor` (or `defineDisplayReactor`) when you want UI-friendly
|
|
213
|
+
values such as strings instead of `bigint` or `Principal`. It adds zod to the
|
|
214
|
+
bundle; see [Bundle Size](#bundle-size).
|
|
138
215
|
- Use generated hooks from `@ic-reactor/vite-plugin` or `@ic-reactor/cli` when
|
|
139
216
|
you have larger canisters or frequent `.did` changes.
|
|
217
|
+
- When a query's arguments are not known yet, pass `skipToken` (re-exported
|
|
218
|
+
from TanStack Query) in their place: `args: owner ? [owner] : skipToken` in
|
|
219
|
+
`useActorQuery`, `getArgs: skipToken` in `useActorInfiniteQuery`, or
|
|
220
|
+
`getBalance(owner ? [owner] : skipToken).useQuery()` on a query factory. The
|
|
221
|
+
query waits without calling the canister. Placeholder args with `enabled`,
|
|
222
|
+
or a `!`, are not needed. The suspense variants do not take it.
|
|
223
|
+
- Call a method that changes state through a mutation (`useActorMutation`,
|
|
224
|
+
`useActorMethod`, `createMutation`), never a query hook or factory. A query
|
|
225
|
+
runs its method again on every refetch (mount, window focus, reconnect,
|
|
226
|
+
invalidation), and an update method executes each time. Without a `retry` of
|
|
227
|
+
its own, a query of an update method retries only a `SysTransient`
|
|
228
|
+
rejection, which proves the call never ran, so a lost response is not
|
|
229
|
+
executed twice; see
|
|
230
|
+
[Update Methods in Queries](https://ic-reactor.b3pay.net/v3/framework/queries#update-methods-in-queries).
|
|
231
|
+
|
|
232
|
+
## Bundle Size
|
|
233
|
+
|
|
234
|
+
What each setup path adds to a browser bundle, measured with esbuild 0.28
|
|
235
|
+
(minified ESM, `@ic-reactor/core` bundled). The peers `react`,
|
|
236
|
+
`@tanstack/react-query` and `@icp-sdk/*` are left out, since an app ships them
|
|
237
|
+
either way; so is `@icp-sdk/auth`, which loads as its own chunk on first use.
|
|
238
|
+
|
|
239
|
+
| Setup | Minified | Gzipped | zod |
|
|
240
|
+
| ---------------------------------------------------------------- | -------: | ------: | :-: |
|
|
241
|
+
| `createActorHooks` + `Reactor` + `ClientManager` | 31 kB | 9.8 kB | no |
|
|
242
|
+
| … + `AuthenticationManager` + `createAuthHooks` | 52 kB | 14.9 kB | no |
|
|
243
|
+
| … + `IdentityAttributesManager` + `createIdentityAttributeHooks` | 58 kB | 16.7 kB | no |
|
|
244
|
+
| `createActorHooks` + `DisplayReactor` + `ClientManager` | 129 kB | 37.3 kB | yes |
|
|
245
|
+
| `defineDisplayReactor` | 158 kB | 45.4 kB | yes |
|
|
246
|
+
| `defineReactor` | 158 kB | 45.4 kB | yes |
|
|
247
|
+
|
|
248
|
+
`DisplayReactor` builds its codecs on zod's classic API, which does not
|
|
249
|
+
tree-shake. That is about 85 kB minified (24 kB gzipped) of every row marked
|
|
250
|
+
"yes"; an app that already bundles zod for its own code pays it once.
|
|
251
|
+
|
|
252
|
+
`defineReactor` costs as much as `defineDisplayReactor` today, even without
|
|
253
|
+
`display`: its deprecated `display: true` option can still build a
|
|
254
|
+
`DisplayReactor`, so the class and zod stay in the bundle. When that option is
|
|
255
|
+
removed at the next major, `defineReactor` drops to about 60 kB minified,
|
|
256
|
+
17.6 kB gzipped. Until then, an app that wants the smallest bundle and has no
|
|
257
|
+
use for `DisplayReactor` sets up with `createActorHooks(new Reactor(...))` and
|
|
258
|
+
`createAuthHooks` (see [Internet Identity](#internet-identity)).
|
|
259
|
+
|
|
260
|
+
Both `define*` functions also include the identity-attribute code (about 6 kB
|
|
261
|
+
minified, 1.8 kB gzipped) whether or not the app calls `useIdentityAttributes`,
|
|
262
|
+
because it is part of the object they return.
|
|
140
263
|
|
|
141
264
|
## Factory Example
|
|
142
265
|
|
|
@@ -150,6 +273,8 @@ export const getProfile = createSuspenseQueryFactory(backend, {
|
|
|
150
273
|
|
|
151
274
|
export const updateProfile = createMutation(backend, {
|
|
152
275
|
functionName: "update_profile",
|
|
276
|
+
// Every get_profile query this factory made, whatever the args
|
|
277
|
+
invalidateQueries: [getProfile],
|
|
153
278
|
onCanisterError: (err) => console.error("Canister Err variant:", err.code),
|
|
154
279
|
})
|
|
155
280
|
```
|
|
@@ -161,17 +286,25 @@ const profileQuery = getProfile(["alice"])
|
|
|
161
286
|
const { data } = profileQuery.useSuspenseQuery()
|
|
162
287
|
|
|
163
288
|
// Prefetch before navigating (fire-and-forget)
|
|
164
|
-
profileQuery.prefetch()
|
|
289
|
+
void profileQuery.prefetch()
|
|
165
290
|
|
|
166
|
-
//
|
|
291
|
+
// Write into the cache
|
|
167
292
|
profileQuery.setData({ id: "alice", name: "Alice" })
|
|
168
293
|
|
|
169
|
-
// Mutation with
|
|
294
|
+
// Mutation with extra invalidation: a query object, a query factory, a
|
|
295
|
+
// `{ functionName, args? }` method of the reactor, or a query key
|
|
170
296
|
const mutation = updateProfile.useMutation({
|
|
171
|
-
invalidateQueries: [
|
|
297
|
+
invalidateQueries: [{ functionName: "list_profiles" }],
|
|
172
298
|
})
|
|
173
299
|
```
|
|
174
300
|
|
|
301
|
+
An `invalidateQueries` entry of `createMutation`, `useActorMutation` and
|
|
302
|
+
`useActorMethod` is a query object, a query factory (every query it returns),
|
|
303
|
+
a `{ functionName, args? }` method of the mutation's reactor, whose name and
|
|
304
|
+
args are type-checked, or a query key. The invalidation is awaited before
|
|
305
|
+
`onSuccess`. Query keys start with the canister ID, so a hand-written
|
|
306
|
+
`["get_profile"]` matches nothing.
|
|
307
|
+
|
|
175
308
|
## Internet Identity
|
|
176
309
|
|
|
177
310
|
`defineReactor` wires up Internet Identity for you — `useAuth`,
|
|
@@ -180,24 +313,36 @@ alongside the actor hooks:
|
|
|
180
313
|
|
|
181
314
|
```tsx
|
|
182
315
|
// src/reactor.ts
|
|
316
|
+
import { defineReactor } from "@ic-reactor/react"
|
|
317
|
+
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
|
|
318
|
+
|
|
183
319
|
export const { useActorQuery, useAuth, useIdentityAttributes, authentication } =
|
|
184
320
|
defineReactor<_SERVICE>({
|
|
185
321
|
name: "backend",
|
|
186
322
|
idlFactory,
|
|
323
|
+
canisterId,
|
|
187
324
|
auth: {
|
|
188
325
|
// Required when the app is served from more than one origin, so every
|
|
189
326
|
// origin resolves to the same principal.
|
|
190
327
|
derivationOrigin: "https://app.example.com",
|
|
191
|
-
// The default signs the user out and reloads after 10 minutes idle.
|
|
192
|
-
idleOptions: { disableIdle: true },
|
|
193
328
|
},
|
|
194
329
|
})
|
|
195
330
|
```
|
|
196
331
|
|
|
332
|
+
Idle handling depends on the installed major. On v8 the client signs the user
|
|
333
|
+
out and reloads the page after 10 minutes idle unless `auth` carries
|
|
334
|
+
`idleOptions: { disableIdle: true }`. v10 drops `idleOptions` with a warning and
|
|
335
|
+
leaves the idle limit to the identity provider: pass `maxTimeToIdle` to
|
|
336
|
+
`login()` to set it, and `disableBrowserActivity: true` in `auth` if only
|
|
337
|
+
requests should count as activity.
|
|
338
|
+
|
|
197
339
|
Auth options are forwarded to the underlying `@icp-sdk/auth` client:
|
|
198
340
|
`identityProvider`, `derivationOrigin`, `windowOpenerFeatures`,
|
|
199
|
-
`openIdProvider
|
|
200
|
-
`
|
|
341
|
+
`openIdProvider` and `transport` on either major, `storage`, `keyType`,
|
|
342
|
+
`idleOptions` and `identity` on v8 only, and `disableBrowserActivity` on v10
|
|
343
|
+
only. An option the installed major lacks is dropped with a one-time warning
|
|
344
|
+
(see [Which `@icp-sdk/auth` to install](#which-icp-sdkauth-to-install)). Only
|
|
345
|
+
the `"google" | "apple" | "microsoft"` aliases are accepted
|
|
201
346
|
for `openIdProvider`; any other value is dropped, since raw issuer URLs are
|
|
202
347
|
only meaningful on `requestOpenIdAttributes`, where they scope the keys.
|
|
203
348
|
|
|
@@ -248,6 +393,7 @@ If your bundler cannot resolve the optional peer at all, construct the client
|
|
|
248
393
|
yourself and inject it — IC Reactor then never imports `@icp-sdk/auth`:
|
|
249
394
|
|
|
250
395
|
```ts
|
|
396
|
+
import { AuthenticationManager } from "@ic-reactor/react"
|
|
251
397
|
import { AuthClient } from "@icp-sdk/auth/client"
|
|
252
398
|
|
|
253
399
|
const authentication = new AuthenticationManager({
|
|
@@ -268,6 +414,12 @@ verification-error page. From `release-2026-03-23` the II frontend moved out of
|
|
|
268
414
|
the canister, so no local build past that point can be used for sign-in — pin an
|
|
269
415
|
older release in `dfx.json`.
|
|
270
416
|
|
|
417
|
+
Pinning only helps `@icp-sdk/auth` v8: v10 signs in through calls II gained
|
|
418
|
+
after its frontend left the canister, so no build both serves `/authorize` and
|
|
419
|
+
supports v10 sessions. With v10, serve an II frontend separately and pass its
|
|
420
|
+
authorize URL as `identityProvider`, with `internetIdentityId` set to the local
|
|
421
|
+
II canister that mints its delegations.
|
|
422
|
+
|
|
271
423
|
An inconclusive probe — the canister unreachable, or answering no `http_request`
|
|
272
424
|
— does not throw: it keeps `/authorize`, because a diagnostic that blocks a
|
|
273
425
|
login that might have worked is worse than the failure it explains. To override
|
|
@@ -277,9 +429,9 @@ takes the path as its third argument.
|
|
|
277
429
|
## Identity Attributes / OpenID email and profile values
|
|
278
430
|
|
|
279
431
|
Identity attributes use a dedicated `IdentityAttributesManager`, with React
|
|
280
|
-
bindings created by `createIdentityAttributeHooks`. Requires `@icp-sdk/auth` v8
|
|
281
|
-
|
|
282
|
-
|
|
432
|
+
bindings created by `createIdentityAttributeHooks`. Requires `@icp-sdk/auth` v8
|
|
433
|
+
or v10. The peer range is `^8.0.0 || ^10.0.0`, and the v7 compatibility path was
|
|
434
|
+
removed in 3.12.0. Both take the nonce as a thunk (`() => Promise<Uint8Array>`); IC Reactor accepts
|
|
283
435
|
either a value or a callback and adapts it, but the callback form is what
|
|
284
436
|
preserves the user gesture (see below).
|
|
285
437
|
|
|
@@ -297,7 +449,11 @@ await requestOpenIdAttributes({
|
|
|
297
449
|
|
|
298
450
|
// ❌ gesture is gone by the time the window would open
|
|
299
451
|
const nonce = await backend.callMethod({ functionName: "register_begin" })
|
|
300
|
-
await requestOpenIdAttributes({
|
|
452
|
+
await requestOpenIdAttributes({
|
|
453
|
+
nonce,
|
|
454
|
+
openIdProvider: "google",
|
|
455
|
+
keys: ["email", "name"],
|
|
456
|
+
})
|
|
301
457
|
```
|
|
302
458
|
|
|
303
459
|
```tsx
|
|
@@ -366,6 +522,8 @@ cached result for a caller-scoped method (`get_my_balance`, a deposit address,
|
|
|
366
522
|
|
|
367
523
|
```tsx
|
|
368
524
|
// ❌ Shared by every request on the server
|
|
525
|
+
import { defineReactor } from "@ic-reactor/react"
|
|
526
|
+
|
|
369
527
|
export const app = defineReactor<_SERVICE>({
|
|
370
528
|
name: "backend",
|
|
371
529
|
idlFactory,
|
|
@@ -374,30 +532,113 @@ export const app = defineReactor<_SERVICE>({
|
|
|
374
532
|
```
|
|
375
533
|
|
|
376
534
|
```tsx
|
|
377
|
-
// ✅ Per request: nothing is shared between users
|
|
535
|
+
// ✅ Per request: nothing is shared between users.
|
|
536
|
+
// In a server component this resolves to the `react-server` entry: the core
|
|
537
|
+
// classes are there, the hooks are not.
|
|
538
|
+
import { ClientManager, Reactor } from "@ic-reactor/react"
|
|
539
|
+
import { QueryClient } from "@tanstack/react-query"
|
|
540
|
+
|
|
378
541
|
export default async function Page() {
|
|
379
|
-
const
|
|
542
|
+
const clientManager = new ClientManager({ queryClient: new QueryClient() })
|
|
543
|
+
const reactor = new Reactor<_SERVICE>({
|
|
380
544
|
name: "backend",
|
|
545
|
+
clientManager,
|
|
381
546
|
idlFactory,
|
|
382
547
|
canisterId,
|
|
383
|
-
queryClient: new QueryClient(),
|
|
384
548
|
})
|
|
385
549
|
|
|
386
|
-
const data = await
|
|
550
|
+
const data = await reactor.fetchQuery({ functionName: "get_my_profile" })
|
|
387
551
|
return <Profile data={data} />
|
|
388
552
|
}
|
|
389
553
|
```
|
|
390
554
|
|
|
555
|
+
Client components need their reactors built inside the React tree too.
|
|
556
|
+
`createReactorProvider` builds them once per mounted provider, in a `useState`
|
|
557
|
+
initializer, and a server render is a tree of its own, so each request gets its
|
|
558
|
+
own reactors, cache and `AuthenticationManager`. `useReactor()` returns what
|
|
559
|
+
the factory built with its full type, so the hooks on it keep their generic
|
|
560
|
+
signatures:
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
// src/reactor.tsx
|
|
564
|
+
"use client"
|
|
565
|
+
import { createReactorProvider, defineReactor } from "@ic-reactor/react"
|
|
566
|
+
|
|
567
|
+
export const { ReactorProvider, useReactor } = createReactorProvider(() =>
|
|
568
|
+
defineReactor<_SERVICE>({ name: "backend", idlFactory, canisterId })
|
|
569
|
+
)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
```tsx
|
|
573
|
+
// app/layout.tsx (a server component) renders
|
|
574
|
+
// <ReactorProvider>{children}</ReactorProvider>, and a client component
|
|
575
|
+
// below it takes its hooks from useReactor():
|
|
576
|
+
export function Profile() {
|
|
577
|
+
const { useActorQuery } = useReactor()
|
|
578
|
+
const { data } = useActorQuery({ functionName: "get_my_profile" })
|
|
579
|
+
return <h1>{data?.name}</h1>
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
The factory can return a record (`{ backend, ledger }`, read with
|
|
584
|
+
`useReactor("ledger")`), reactors and managers built by hand, or query and
|
|
585
|
+
mutation objects. It receives the provider's props, read once per mount; a new
|
|
586
|
+
`key` builds a new value. The provider also renders a `QueryClientProvider` for
|
|
587
|
+
the value's QueryClient, so `useQueryClient()`, React Query Devtools and a
|
|
588
|
+
`HydrationBoundary` below it use the cache the hooks fill. Below it, that
|
|
589
|
+
provider takes the place of an outer `QueryClientProvider`; pass
|
|
590
|
+
`{ queryClientProvider: false }` to keep your own.
|
|
591
|
+
|
|
592
|
+
A suspense hook below the provider may suspend its first render: the provider
|
|
593
|
+
reuses the value that render built when React renders it again. A provider that
|
|
594
|
+
a transition mounts (`startTransition`, a client-side navigation) can be built
|
|
595
|
+
again on each retry, so wrap its suspending components in a `<Suspense>`
|
|
596
|
+
boundary inside the provider.
|
|
597
|
+
|
|
598
|
+
When the tree unmounts, the provider disposes each `AuthenticationManager`
|
|
599
|
+
built for the value (in the factory, or later by a `defineReactor` result in
|
|
600
|
+
it), releasing the Internet Identity client it built: a v10 client keeps
|
|
601
|
+
listening to the page until it is disposed, so each remount would otherwise
|
|
602
|
+
leave one behind. A manager built elsewhere and passed in, such as an app-wide
|
|
603
|
+
one, is left alone. A provider you write yourself has to do the same from its
|
|
604
|
+
cleanup:
|
|
605
|
+
|
|
606
|
+
```tsx
|
|
607
|
+
import { useEffect, useState } from "react"
|
|
608
|
+
|
|
609
|
+
const [value] = useState(createReactorContext)
|
|
610
|
+
useEffect(() => () => value.authentication.dispose(), [value])
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
`dispose()` only forgets the client, and the next sign-in builds a new one, so
|
|
614
|
+
this is safe under StrictMode, which runs the cleanup and the effect again on
|
|
615
|
+
the same managers. A client passed in as `authClient` is left alone.
|
|
616
|
+
|
|
391
617
|
Two further constraints on the App Router specifically:
|
|
392
618
|
|
|
393
619
|
- Hooks are client-only, like every React hook — call them from a `"use client"`
|
|
394
|
-
module. A server component
|
|
395
|
-
|
|
620
|
+
module. A server component, server action or route handler resolves
|
|
621
|
+
`@ic-reactor/react` to its `react-server` entry, which Next.js (Turbopack and
|
|
622
|
+
webpack) selects through the export condition of that name. That entry exports
|
|
623
|
+
everything `@ic-reactor/core` does — `Reactor`, `DisplayReactor`,
|
|
624
|
+
`ClientManager`, the error classes and utilities — plus the validation helpers
|
|
625
|
+
(`mapValidationErrors`, `getFieldError`, …), and nothing that imports React.
|
|
626
|
+
Importing a hook, `defineReactor`, `createActorHooks`, a query or mutation
|
|
627
|
+
factory, or the auth classes there fails `next build` with "Export
|
|
628
|
+
defineReactor doesn't exist in target module"; hooks belong in a
|
|
629
|
+
`"use client"` module, and server code calls the reactor itself
|
|
630
|
+
(`reactor.fetchQuery()`, `reactor.callMethod()`) where client code would use a
|
|
631
|
+
factory's `.fetch()` or `.execute()`. TypeScript does not read the condition,
|
|
632
|
+
so the editor does not flag it first. A server-component bundler that ignores
|
|
633
|
+
`react-server` loads the full entry and rejects its hooks: import from
|
|
634
|
+
`@ic-reactor/core` there, and list it in your own `package.json`, since a
|
|
635
|
+
transitive dependency does not resolve under pnpm.
|
|
396
636
|
- Hooks bind to their reactor's own `QueryClient` rather than to a
|
|
397
637
|
`QueryClientProvider`, so `HydrationBoundary` prefetch does not feed them
|
|
398
|
-
unless the provider's client _is_ that reactor's client
|
|
399
|
-
evaluates a shared module
|
|
400
|
-
module-scope reactor is
|
|
638
|
+
unless the provider's client _is_ that reactor's client, as it is below
|
|
639
|
+
`createReactorProvider`'s provider. Next.js also evaluates a shared module
|
|
640
|
+
twice on the server (the RSC and SSR graphs), so a module-scope reactor is
|
|
641
|
+
two different instances there.
|
|
401
642
|
|
|
402
643
|
If none of that applies — a client-only SPA — module-scope reactors are exactly
|
|
403
644
|
right and none of this is a concern.
|
|
@@ -407,15 +648,64 @@ right and none of this is a concern.
|
|
|
407
648
|
Every object returned by `createQuery`, `createSuspenseQuery`, and their
|
|
408
649
|
factory variants exposes:
|
|
409
650
|
|
|
410
|
-
| Method | Description
|
|
411
|
-
| ----------------------------------- |
|
|
412
|
-
| `fetch()` | Cache-first fetch — returns data, populates cache. Use in route loaders.
|
|
413
|
-
| `prefetch()` | Fire-and-forget cache warm-up. Use on hover or before navigation.
|
|
414
|
-
| `invalidate()` | Invalidates the cache entry (triggers refetch if query is mounted).
|
|
415
|
-
| `getQueryKey()` | Returns the TanStack Query key for this query.
|
|
416
|
-
| `getCacheData(select?)` | Read directly from cache without fetching. Returns `undefined` if not cached.
|
|
417
|
-
| `setData(updater)` | Write raw data into the cache. Accepts a value or updater function.
|
|
418
|
-
| `
|
|
651
|
+
| Method | Description |
|
|
652
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
653
|
+
| `fetch()` | Cache-first fetch — returns data, populates cache. Use in route loaders. |
|
|
654
|
+
| `prefetch()` | Fire-and-forget cache warm-up. Use on hover or before navigation. |
|
|
655
|
+
| `invalidate()` | Invalidates the cache entry (triggers refetch if query is mounted). |
|
|
656
|
+
| `getQueryKey()` | Returns the TanStack Query key for this query. |
|
|
657
|
+
| `getCacheData(select?)` | Read directly from cache without fetching. Returns `undefined` if not cached. |
|
|
658
|
+
| `setData(updater)` | Write raw data into the cache. Accepts a value or updater function. |
|
|
659
|
+
| `optimisticUpdate(updater)` | Cancel the fetch in flight, write `updater(cached)`, resolve with `{ rollback() }`. |
|
|
660
|
+
| `cancel()` | Cancel this query's fetch in flight; the entry keeps its previous value. |
|
|
661
|
+
| `reset()` | Reset this entry to its initial state; a mounted hook refetches, a suspense hook suspends. |
|
|
662
|
+
| `useQuery()` / `useSuspenseQuery()` | React hook for the query. |
|
|
663
|
+
|
|
664
|
+
A sign-in or sign-out while `fetch()` is in flight does not reject it: the
|
|
665
|
+
previous identity's answer is dropped, and `fetch()` runs again for the new
|
|
666
|
+
identity and resolves with that answer. `prefetch()` runs again too, and still
|
|
667
|
+
never rejects: when that run succeeds, the cache holds the new identity's
|
|
668
|
+
answer by the time it resolves. The infinite query factories' `fetch()` behaves
|
|
669
|
+
the same (they have no `prefetch()`).
|
|
670
|
+
|
|
671
|
+
`optimisticUpdate`, `cancel` and `reset` act on the query's own entry only, and
|
|
672
|
+
the infinite query objects have them too, over their `{ pages, pageParams }`.
|
|
673
|
+
An optimistic update is three lines of mutation config:
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
import { createMutation, createQueryFactory } from "@ic-reactor/react"
|
|
677
|
+
import { backend } from "./reactor"
|
|
678
|
+
|
|
679
|
+
const getPost = createQueryFactory(backend, { functionName: "get_post" })
|
|
680
|
+
const likePost = createMutation(backend, { functionName: "like_post" })
|
|
681
|
+
|
|
682
|
+
// In a component
|
|
683
|
+
const { mutate } = likePost.useMutation({
|
|
684
|
+
onMutate: ([postId]) =>
|
|
685
|
+
getPost([postId]).optimisticUpdate((post) => ({
|
|
686
|
+
...post,
|
|
687
|
+
likes: post.likes + 1n,
|
|
688
|
+
})),
|
|
689
|
+
onError: (_error, _args, update) => update?.rollback(),
|
|
690
|
+
onSettled: (_data, _error, [postId]) => getPost([postId]).invalidate(),
|
|
691
|
+
})
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
The updater gets the raw, typed value and is not called when nothing is
|
|
695
|
+
cached. Refetch once the mutation settles, as `onSettled` does here: the fetch
|
|
696
|
+
`optimisticUpdate` cancels may be a refetch an invalidation or a sign-in
|
|
697
|
+
started. `rollback()` does nothing after a sign-in or sign-out, because the
|
|
698
|
+
value it kept was the previous principal's.
|
|
699
|
+
|
|
700
|
+
Use `backend.queryClient` rather than `useQueryClient()` when you need the
|
|
701
|
+
QueryClient itself: the hooks bind to the reactor's client, and
|
|
702
|
+
`useQueryClient()` throws without the optional `QueryClientProvider`.
|
|
703
|
+
|
|
704
|
+
The function a factory variant returns (`createQueryFactory`,
|
|
705
|
+
`createSuspenseQueryFactory`, `createInfiniteQueryFactory`,
|
|
706
|
+
`createSuspenseInfiniteQueryFactory`) also has `getQueryKey()`, the key prefix
|
|
707
|
+
every query it returns shares, and `invalidate()`, which invalidates all of
|
|
708
|
+
them whatever their args.
|
|
419
709
|
|
|
420
710
|
## Canister Error Handling
|
|
421
711
|
|
|
@@ -425,6 +715,9 @@ via `onCanisterError`. This callback is supported on both `createMutation` and
|
|
|
425
715
|
the direct `useActorMutation` hook:
|
|
426
716
|
|
|
427
717
|
```tsx
|
|
718
|
+
import { createMutation } from "@ic-reactor/react"
|
|
719
|
+
import { backend, useActorMutation } from "./reactor"
|
|
720
|
+
|
|
428
721
|
// Via createActorHooks
|
|
429
722
|
const { mutate } = useActorMutation({
|
|
430
723
|
functionName: "transfer",
|
|
@@ -448,8 +741,10 @@ const transferMutation = createMutation(backend, {
|
|
|
448
741
|
|
|
449
742
|
## Re-exports
|
|
450
743
|
|
|
451
|
-
`@ic-reactor/react` re-exports the core runtime, so
|
|
452
|
-
single package
|
|
744
|
+
`@ic-reactor/react` re-exports the core runtime, so client and server code can
|
|
745
|
+
import these from a single package. A React Server Component resolves the
|
|
746
|
+
package's `react-server` entry, which has them but none of the hooks; see
|
|
747
|
+
[Server-Side Rendering](#server-side-rendering):
|
|
453
748
|
|
|
454
749
|
- `ClientManager`
|
|
455
750
|
- `Reactor`
|
|
@@ -457,6 +752,77 @@ single package:
|
|
|
457
752
|
- `CallError`
|
|
458
753
|
- `CanisterError`
|
|
459
754
|
- `ValidationError`
|
|
755
|
+
- `formatTokenAmount` and `parseTokenAmount`, which convert a ledger's base
|
|
756
|
+
units to and from decimal text exactly (see
|
|
757
|
+
[Token Amounts](../core/README.md#token-amounts)); do not use `Number` for
|
|
758
|
+
either
|
|
759
|
+
- `isPrincipalText`, which checks a principal a person typed without a `try`
|
|
760
|
+
around `Principal.fromText`
|
|
761
|
+
|
|
762
|
+
The main entry also re-exports TanStack Query's `skipToken` (and its
|
|
763
|
+
`SkipToken` type), the same symbol `@tanstack/react-query` exports.
|
|
764
|
+
|
|
765
|
+
## Testing
|
|
766
|
+
|
|
767
|
+
`@ic-reactor/react/testing` re-exports `@ic-reactor/core/testing`, so an app
|
|
768
|
+
that depends on this package alone can test its components against a fake
|
|
769
|
+
replica instead of a `Reactor` stub:
|
|
770
|
+
|
|
771
|
+
```tsx
|
|
772
|
+
import { afterEach, beforeEach, expect, it } from "vitest"
|
|
773
|
+
import { renderHook, waitFor } from "@testing-library/react"
|
|
774
|
+
import { ClientManager, Reactor, createActorHooks } from "@ic-reactor/react"
|
|
775
|
+
import {
|
|
776
|
+
createTestCanister,
|
|
777
|
+
installFakeReplica,
|
|
778
|
+
type FakeReplica,
|
|
779
|
+
} from "@ic-reactor/react/testing"
|
|
780
|
+
import { QueryClient } from "@tanstack/react-query"
|
|
781
|
+
import { idlFactory, type _SERVICE } from "./declarations/backend"
|
|
782
|
+
|
|
783
|
+
const BACKEND = "bkyz2-fmaaa-aaaaa-qaaaq-cai"
|
|
784
|
+
let replica: FakeReplica
|
|
785
|
+
|
|
786
|
+
beforeEach(() => {
|
|
787
|
+
replica = installFakeReplica({
|
|
788
|
+
canisters: {
|
|
789
|
+
[BACKEND]: createTestCanister<_SERVICE>(idlFactory, {
|
|
790
|
+
balance: () => 42n,
|
|
791
|
+
}),
|
|
792
|
+
},
|
|
793
|
+
})
|
|
794
|
+
})
|
|
795
|
+
// Also when the test fails, so the next test gets a fetch of its own.
|
|
796
|
+
afterEach(() => replica.restore())
|
|
797
|
+
|
|
798
|
+
it("reads the balance", async () => {
|
|
799
|
+
// Built after the fake is installed, and pointed at it.
|
|
800
|
+
const reactor = new Reactor<_SERVICE>({
|
|
801
|
+
clientManager: new ClientManager({
|
|
802
|
+
queryClient: new QueryClient(),
|
|
803
|
+
agentOptions: { host: replica.host },
|
|
804
|
+
}),
|
|
805
|
+
name: "backend",
|
|
806
|
+
canisterId: BACKEND,
|
|
807
|
+
idlFactory,
|
|
808
|
+
})
|
|
809
|
+
const { useActorQuery } = createActorHooks(reactor)
|
|
810
|
+
|
|
811
|
+
const { result } = renderHook(() =>
|
|
812
|
+
useActorQuery({ functionName: "balance" })
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
await waitFor(() => expect(result.current.data).toBe(42n))
|
|
816
|
+
})
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
A reactor built at module scope, such as one from `defineReactor`, builds its
|
|
820
|
+
agent when its module is imported: install the fake first, then import the
|
|
821
|
+
component under test dynamically. With no `host` on either side, its
|
|
822
|
+
`ClientManager` and the fake both use the page's origin in jsdom, so they
|
|
823
|
+
meet. The
|
|
824
|
+
[Testing guide](https://ic-reactor.b3pay.net/v3/guides/testing) shows how, and
|
|
825
|
+
how to test as a signed-in user.
|
|
460
826
|
|
|
461
827
|
## See Also
|
|
462
828
|
|