@ic-reactor/react 3.13.0 → 4.0.0-beta.1

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.
Files changed (174) hide show
  1. package/README.md +234 -787
  2. package/dist/index.d.ts +259 -16
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +470 -23
  5. package/dist/index.js.map +1 -1
  6. package/llms.txt +80 -278
  7. package/package.json +11 -39
  8. package/src/index.tsx +611 -0
  9. package/dist/auth/auth-client-compat.d.ts +0 -122
  10. package/dist/auth/auth-client-compat.d.ts.map +0 -1
  11. package/dist/auth/auth-client-compat.js +0 -162
  12. package/dist/auth/auth-client-compat.js.map +0 -1
  13. package/dist/auth/authentication-manager.d.ts +0 -405
  14. package/dist/auth/authentication-manager.d.ts.map +0 -1
  15. package/dist/auth/authentication-manager.js +0 -1537
  16. package/dist/auth/authentication-manager.js.map +0 -1
  17. package/dist/auth/constants.d.ts +0 -24
  18. package/dist/auth/constants.d.ts.map +0 -1
  19. package/dist/auth/constants.js +0 -24
  20. package/dist/auth/constants.js.map +0 -1
  21. package/dist/auth/createIdentityAttributeHooks.d.ts +0 -14
  22. package/dist/auth/createIdentityAttributeHooks.d.ts.map +0 -1
  23. package/dist/auth/createIdentityAttributeHooks.js +0 -122
  24. package/dist/auth/createIdentityAttributeHooks.js.map +0 -1
  25. package/dist/auth/identity-attributes-manager.d.ts +0 -27
  26. package/dist/auth/identity-attributes-manager.d.ts.map +0 -1
  27. package/dist/auth/identity-attributes-manager.js +0 -191
  28. package/dist/auth/identity-attributes-manager.js.map +0 -1
  29. package/dist/auth/identity-attributes.d.ts +0 -19
  30. package/dist/auth/identity-attributes.d.ts.map +0 -1
  31. package/dist/auth/identity-attributes.js +0 -227
  32. package/dist/auth/identity-attributes.js.map +0 -1
  33. package/dist/auth/index.d.ts +0 -8
  34. package/dist/auth/index.d.ts.map +0 -1
  35. package/dist/auth/index.js +0 -8
  36. package/dist/auth/index.js.map +0 -1
  37. package/dist/auth/local-ii-probe.d.ts +0 -57
  38. package/dist/auth/local-ii-probe.d.ts.map +0 -1
  39. package/dist/auth/local-ii-probe.js +0 -121
  40. package/dist/auth/local-ii-probe.js.map +0 -1
  41. package/dist/auth/types.d.ts +0 -222
  42. package/dist/auth/types.d.ts.map +0 -1
  43. package/dist/auth/types.js +0 -2
  44. package/dist/auth/types.js.map +0 -1
  45. package/dist/createActorHooks.d.ts +0 -41
  46. package/dist/createActorHooks.d.ts.map +0 -1
  47. package/dist/createActorHooks.js +0 -17
  48. package/dist/createActorHooks.js.map +0 -1
  49. package/dist/createInfiniteQuery.d.ts +0 -185
  50. package/dist/createInfiniteQuery.d.ts.map +0 -1
  51. package/dist/createInfiniteQuery.js +0 -198
  52. package/dist/createInfiniteQuery.js.map +0 -1
  53. package/dist/createMutation.d.ts +0 -33
  54. package/dist/createMutation.d.ts.map +0 -1
  55. package/dist/createMutation.js +0 -199
  56. package/dist/createMutation.js.map +0 -1
  57. package/dist/createQuery.d.ts +0 -63
  58. package/dist/createQuery.d.ts.map +0 -1
  59. package/dist/createQuery.js +0 -204
  60. package/dist/createQuery.js.map +0 -1
  61. package/dist/createReactorProvider.d.ts +0 -158
  62. package/dist/createReactorProvider.d.ts.map +0 -1
  63. package/dist/createReactorProvider.js +0 -256
  64. package/dist/createReactorProvider.js.map +0 -1
  65. package/dist/createSuspenseInfiniteQuery.d.ts +0 -154
  66. package/dist/createSuspenseInfiniteQuery.d.ts.map +0 -1
  67. package/dist/createSuspenseInfiniteQuery.js +0 -209
  68. package/dist/createSuspenseInfiniteQuery.js.map +0 -1
  69. package/dist/createSuspenseQuery.d.ts +0 -46
  70. package/dist/createSuspenseQuery.d.ts.map +0 -1
  71. package/dist/createSuspenseQuery.js +0 -158
  72. package/dist/createSuspenseQuery.js.map +0 -1
  73. package/dist/defineDisplayReactor.d.ts +0 -43
  74. package/dist/defineDisplayReactor.d.ts.map +0 -1
  75. package/dist/defineDisplayReactor.js +0 -42
  76. package/dist/defineDisplayReactor.js.map +0 -1
  77. package/dist/defineReactor.d.ts +0 -99
  78. package/dist/defineReactor.d.ts.map +0 -1
  79. package/dist/defineReactor.js +0 -15
  80. package/dist/defineReactor.js.map +0 -1
  81. package/dist/defineReactorShared.d.ts +0 -84
  82. package/dist/defineReactorShared.d.ts.map +0 -1
  83. package/dist/defineReactorShared.js +0 -139
  84. package/dist/defineReactorShared.js.map +0 -1
  85. package/dist/hooks/createAuthHooks.d.ts +0 -50
  86. package/dist/hooks/createAuthHooks.d.ts.map +0 -1
  87. package/dist/hooks/createAuthHooks.js +0 -291
  88. package/dist/hooks/createAuthHooks.js.map +0 -1
  89. package/dist/hooks/index.d.ts +0 -21
  90. package/dist/hooks/index.d.ts.map +0 -1
  91. package/dist/hooks/index.js +0 -24
  92. package/dist/hooks/index.js.map +0 -1
  93. package/dist/hooks/useActorInfiniteQuery.d.ts +0 -67
  94. package/dist/hooks/useActorInfiniteQuery.d.ts.map +0 -1
  95. package/dist/hooks/useActorInfiniteQuery.js +0 -89
  96. package/dist/hooks/useActorInfiniteQuery.js.map +0 -1
  97. package/dist/hooks/useActorMethod.d.ts +0 -148
  98. package/dist/hooks/useActorMethod.d.ts.map +0 -1
  99. package/dist/hooks/useActorMethod.js +0 -394
  100. package/dist/hooks/useActorMethod.js.map +0 -1
  101. package/dist/hooks/useActorMutation.d.ts +0 -51
  102. package/dist/hooks/useActorMutation.d.ts.map +0 -1
  103. package/dist/hooks/useActorMutation.js +0 -70
  104. package/dist/hooks/useActorMutation.js.map +0 -1
  105. package/dist/hooks/useActorQuery.d.ts +0 -45
  106. package/dist/hooks/useActorQuery.d.ts.map +0 -1
  107. package/dist/hooks/useActorQuery.js +0 -67
  108. package/dist/hooks/useActorQuery.js.map +0 -1
  109. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +0 -51
  110. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +0 -1
  111. package/dist/hooks/useActorSuspenseInfiniteQuery.js +0 -76
  112. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +0 -1
  113. package/dist/hooks/useActorSuspenseQuery.d.ts +0 -32
  114. package/dist/hooks/useActorSuspenseQuery.d.ts.map +0 -1
  115. package/dist/hooks/useActorSuspenseQuery.js +0 -58
  116. package/dist/hooks/useActorSuspenseQuery.js.map +0 -1
  117. package/dist/ownedAuthentication.d.ts +0 -52
  118. package/dist/ownedAuthentication.d.ts.map +0 -1
  119. package/dist/ownedAuthentication.js +0 -49
  120. package/dist/ownedAuthentication.js.map +0 -1
  121. package/dist/server.d.ts +0 -21
  122. package/dist/server.d.ts.map +0 -1
  123. package/dist/server.js +0 -23
  124. package/dist/server.js.map +0 -1
  125. package/dist/testing.d.ts +0 -19
  126. package/dist/testing.d.ts.map +0 -1
  127. package/dist/testing.js +0 -19
  128. package/dist/testing.js.map +0 -1
  129. package/dist/types.d.ts +0 -671
  130. package/dist/types.d.ts.map +0 -1
  131. package/dist/types.js +0 -5
  132. package/dist/types.js.map +0 -1
  133. package/dist/utils.d.ts +0 -207
  134. package/dist/utils.d.ts.map +0 -1
  135. package/dist/utils.js +0 -405
  136. package/dist/utils.js.map +0 -1
  137. package/dist/validation.d.ts +0 -136
  138. package/dist/validation.d.ts.map +0 -1
  139. package/dist/validation.js +0 -144
  140. package/dist/validation.js.map +0 -1
  141. package/src/auth/auth-client-compat.ts +0 -273
  142. package/src/auth/authentication-manager.ts +0 -1682
  143. package/src/auth/constants.ts +0 -32
  144. package/src/auth/createIdentityAttributeHooks.ts +0 -169
  145. package/src/auth/identity-attributes-manager.ts +0 -226
  146. package/src/auth/identity-attributes.ts +0 -345
  147. package/src/auth/index.ts +0 -7
  148. package/src/auth/local-ii-probe.ts +0 -173
  149. package/src/auth/types.ts +0 -243
  150. package/src/createActorHooks.ts +0 -208
  151. package/src/createInfiniteQuery.ts +0 -670
  152. package/src/createMutation.ts +0 -324
  153. package/src/createQuery.ts +0 -369
  154. package/src/createReactorProvider.ts +0 -365
  155. package/src/createSuspenseInfiniteQuery.ts +0 -651
  156. package/src/createSuspenseQuery.ts +0 -304
  157. package/src/defineDisplayReactor.ts +0 -62
  158. package/src/defineReactor.ts +0 -142
  159. package/src/defineReactorShared.ts +0 -268
  160. package/src/hooks/createAuthHooks.ts +0 -371
  161. package/src/hooks/index.ts +0 -103
  162. package/src/hooks/useActorInfiniteQuery.ts +0 -278
  163. package/src/hooks/useActorMethod.ts +0 -710
  164. package/src/hooks/useActorMutation.ts +0 -205
  165. package/src/hooks/useActorQuery.ts +0 -157
  166. package/src/hooks/useActorSuspenseInfiniteQuery.ts +0 -248
  167. package/src/hooks/useActorSuspenseQuery.ts +0 -147
  168. package/src/index.ts +0 -31
  169. package/src/ownedAuthentication.ts +0 -81
  170. package/src/server.ts +0 -23
  171. package/src/testing.ts +0 -18
  172. package/src/types.ts +0 -948
  173. package/src/utils.ts +0 -505
  174. package/src/validation.ts +0 -226
@@ -1,268 +0,0 @@
1
- /**
2
- * The body `defineReactor` and `defineDisplayReactor` share.
3
- *
4
- * It takes the reactor's construction as a callback, so this module imports
5
- * neither reactor class. That matters for bundle size: `DisplayReactor` builds
6
- * its codecs on zod, and a module that imported it here would put zod in every
7
- * app that calls `defineReactor`, whether or not it ever builds a
8
- * DisplayReactor. Keep it that way — no `DisplayReactor` import in this file.
9
- */
10
- import { ClientManager, reactorRetry } from "@ic-reactor/core"
11
- import type {
12
- Reactor,
13
- TransformKey,
14
- ReactorParameters,
15
- ClientManagerParameters,
16
- } from "@ic-reactor/core"
17
- import { QueryClient } from "@tanstack/react-query"
18
- import { createActorHooks, ActorHooks } from "./createActorHooks.js"
19
- import { AuthenticationManager } from "./auth/authentication-manager.js"
20
- import { IdentityAttributesManager } from "./auth/identity-attributes-manager.js"
21
- import { createIdentityAttributeHooks } from "./auth/createIdentityAttributeHooks.js"
22
- import type { UseIdentityAttributesReturn } from "./auth/createIdentityAttributeHooks.js"
23
- import { createAuthHooks } from "./hooks/createAuthHooks.js"
24
- import type { CreateAuthHooksReturn } from "./hooks/createAuthHooks.js"
25
- import type { AuthenticationManagerParameters } from "./auth/authentication-manager.js"
26
- import { registerAuthentication } from "./ownedAuthentication.js"
27
-
28
- /** Options shared by both the standard and display variants of defineReactor. */
29
- export interface DefineReactorSharedParameters
30
- extends
31
- Omit<ReactorParameters, "clientManager">,
32
- Omit<ClientManagerParameters, "queryClient"> {
33
- /**
34
- * Reuse an existing ClientManager (e.g. to share one agent across canisters).
35
- * When omitted, a ClientManager is created from the agent options below.
36
- *
37
- * A supplied or adopted manager brings its own agent and QueryClient, so
38
- * `agentOptions`, `queryClient` and `allowEnvConfig` apply only when this
39
- * call creates one.
40
- */
41
- clientManager?: ClientManager
42
- /**
43
- * QueryClient for a ClientManager created by this call.
44
- *
45
- * Ignored when `clientManager` or `authentication` is supplied — queries run
46
- * against that manager's own QueryClient, which is what is returned.
47
- */
48
- queryClient?: QueryClient
49
- /**
50
- * Reuse an existing AuthenticationManager, so several reactors share one
51
- * Internet Identity session. When omitted, one is created for this reactor.
52
- *
53
- * Its `clientManager` is adopted for this reactor, so sign-in updates the
54
- * same agent the reactor calls through. Supplying a different `clientManager`
55
- * alongside it is rejected.
56
- */
57
- authentication?: AuthenticationManager
58
- /**
59
- * Internet Identity options forwarded to the AuthenticationManager
60
- * (`identityProvider`, `derivationOrigin`, `idleOptions`, `storage`, …).
61
- *
62
- * Not every option reaches both `@icp-sdk/auth` majors. `idleOptions`,
63
- * `storage`, `keyType` and `identity` are honoured only by v8: v10 has no
64
- * equivalent, so IC Reactor drops each with a one-time warning. For idle
65
- * handling on v10, pass `maxTimeToIdle` to `login()` and set
66
- * `disableBrowserActivity` here. `disableBrowserActivity` is v10-only.
67
- *
68
- * Mutually exclusive with `authentication`: a manager built elsewhere is
69
- * already configured, so these could not be applied to it.
70
- */
71
- auth?: Omit<AuthenticationManagerParameters, "clientManager">
72
- }
73
-
74
- /** The reactor instance plus its bound hooks and shared infrastructure. */
75
- export type DefineReactorResult<
76
- Service,
77
- Transform extends TransformKey,
78
- R extends Reactor<Service, Transform>,
79
- > = ActorHooks<Service, Transform> &
80
- CreateAuthHooksReturn & {
81
- reactor: R
82
- clientManager: ClientManager
83
- queryClient: QueryClient
84
- /** Internet Identity session manager backing `useAuth`. */
85
- authentication: AuthenticationManager
86
- /** Signed identity attribute requests backing `useIdentityAttributes`. */
87
- identityAttributes: IdentityAttributesManager
88
- useIdentityAttributes: () => UseIdentityAttributesReturn
89
- }
90
-
91
- /**
92
- * The QueryClient this module creates when the caller does not supply one.
93
- *
94
- * React Query retries every failure three times by default, which for canister
95
- * calls means four attempts and several seconds of backoff on outcomes that
96
- * cannot change — a canister `Err`, a validation failure, a Candid encode
97
- * error that never reached the network. `reactorRetry` keeps the same three
98
- * attempts for transport failures and stops immediately on the rest.
99
- *
100
- * A caller-supplied `queryClient` is left exactly as given; opt in there with
101
- * `defaultOptions: { queries: { retry: reactorRetry } }`.
102
- */
103
- const createDefaultQueryClient = () =>
104
- new QueryClient({
105
- defaultOptions: { queries: { retry: reactorRetry } },
106
- })
107
-
108
- /**
109
- * Builds the ClientManager, the reactor `createReactor` returns, its hooks and
110
- * the lazily created auth managers, for `defineReactor` and
111
- * `defineDisplayReactor`. Not part of the public API.
112
- *
113
- * `caller` is the function the app called; the errors below name it, so an
114
- * app that called `defineDisplayReactor` is not sent looking for a
115
- * `defineReactor` call it never made.
116
- *
117
- * @internal
118
- */
119
- export function defineReactorWith<
120
- Service,
121
- Transform extends TransformKey,
122
- R extends Reactor<Service, Transform>,
123
- >(
124
- caller: "defineReactor" | "defineDisplayReactor",
125
- params: DefineReactorSharedParameters,
126
- createReactor: (config: ReactorParameters) => R
127
- ): DefineReactorResult<Service, Transform, R> {
128
- const {
129
- clientManager: providedClientManager,
130
- queryClient: providedQueryClient,
131
- authentication: providedAuthentication,
132
- auth,
133
- agentOptions,
134
- allowEnvConfig,
135
- allowEnvRootKey,
136
- name,
137
- idlFactory,
138
- canisterId,
139
- pollingOptions,
140
- } = params
141
-
142
- // A shared AuthenticationManager updates the identity on its own
143
- // ClientManager. Giving this reactor a different one would leave its calls
144
- // anonymous after sign-in, so adopt the manager's rather than building a new
145
- // one, and refuse an explicit mismatch instead of splitting them silently.
146
- if (
147
- providedClientManager &&
148
- providedAuthentication &&
149
- providedAuthentication.clientManager !== providedClientManager
150
- ) {
151
- throw new Error(
152
- `[ic-reactor] ${caller}("${name}") received an \`authentication\` manager bound to a different \`clientManager\`. ` +
153
- `Sign-in would update the authentication manager's agent while this reactor calls through another one, ` +
154
- `leaving its calls anonymous. Pass \`clientManager: authentication.clientManager\`, or omit \`clientManager\` to adopt it.`
155
- )
156
- }
157
-
158
- // `auth` configures a manager this call would build; an existing one is
159
- // already constructed, so these options could only be dropped on the floor.
160
- if (providedAuthentication && auth) {
161
- throw new Error(
162
- `[ic-reactor] ${caller}("${name}") received both \`authentication\` and \`auth\`. ` +
163
- `The supplied manager is already configured, so \`auth\` (${Object.keys(auth).join(", ")}) would be ignored. ` +
164
- `Pass those options where that AuthenticationManager is created, or drop \`authentication\` to build one here.`
165
- )
166
- }
167
-
168
- // Same reasoning as `auth`, and it matters more here: an ignored
169
- // `allowEnvConfig: false` reads as "I locked the cookie out" while the
170
- // supplied manager carries whatever decision it was built with.
171
- if (
172
- (providedClientManager || providedAuthentication) &&
173
- (allowEnvConfig !== undefined || allowEnvRootKey !== undefined)
174
- ) {
175
- const passed = [
176
- allowEnvConfig !== undefined && "allowEnvConfig",
177
- allowEnvRootKey !== undefined && "allowEnvRootKey",
178
- ]
179
- .filter(Boolean)
180
- .join(", ")
181
- throw new Error(
182
- `[ic-reactor] ${caller}("${name}") received both a ClientManager and \`${passed}\`. ` +
183
- `That option is resolved when a ClientManager is constructed, so the supplied one already carries its own ` +
184
- `decision and this would be ignored — silently changing nothing about whether the ic_env cookie is trusted. ` +
185
- `Pass it where that ClientManager is created, or drop \`clientManager\` to build one here.`
186
- )
187
- }
188
-
189
- const clientManager =
190
- providedClientManager ??
191
- providedAuthentication?.clientManager ??
192
- new ClientManager({
193
- queryClient: providedQueryClient ?? createDefaultQueryClient(),
194
- agentOptions,
195
- // Forwarded, not dropped: the type has always accepted these (it extends
196
- // ClientManagerParameters) while the call ignored them, so the one
197
- // documented setup path silently discarded the ic_env opt-in it advertised.
198
- allowEnvConfig,
199
- allowEnvRootKey,
200
- })
201
-
202
- // Always report the QueryClient actually in use: when a ClientManager is
203
- // supplied or adopted, its own QueryClient is the one queries run against.
204
- const queryClient = clientManager.queryClient
205
-
206
- const reactor = createReactor({
207
- clientManager,
208
- name,
209
- idlFactory,
210
- canisterId,
211
- pollingOptions,
212
- })
213
-
214
- const hooks = createActorHooks<Service, Transform>(reactor)
215
-
216
- // Auth is built on first use. `AuthenticationManager` dynamically imports the
217
- // optional `@icp-sdk/auth` peer as soon as it is constructed, and reactors
218
- // that never touch authentication should not pay for that.
219
- let authenticationInstance: AuthenticationManager | undefined
220
- const getAuthentication = () =>
221
- (authenticationInstance ??=
222
- providedAuthentication ??
223
- new AuthenticationManager({ ...auth, clientManager }))
224
-
225
- let identityAttributesInstance: IdentityAttributesManager | undefined
226
- const getIdentityAttributes = () =>
227
- (identityAttributesInstance ??= new IdentityAttributesManager(
228
- getAuthentication()
229
- ))
230
-
231
- let authHooks: CreateAuthHooksReturn | undefined
232
- const getAuthHooks = () =>
233
- (authHooks ??= createAuthHooks(getAuthentication()))
234
-
235
- let attributeHooks:
236
- ReturnType<typeof createIdentityAttributeHooks> | undefined
237
- const getAttributeHooks = () =>
238
- (attributeHooks ??= createIdentityAttributeHooks(getIdentityAttributes()))
239
-
240
- const result: DefineReactorResult<Service, Transform, R> = {
241
- ...hooks,
242
- reactor,
243
- clientManager,
244
- queryClient,
245
- // Stable wrappers: the hook call order inside them never changes, so the
246
- // rules of hooks still hold.
247
- useAuth: () => getAuthHooks().useAuth(),
248
- useAgentState: () => getAuthHooks().useAgentState(),
249
- useUserPrincipal: () => getAuthHooks().useUserPrincipal(),
250
- useIdentityAttributes: () => getAttributeHooks().useIdentityAttributes(),
251
- get authentication() {
252
- return getAuthentication()
253
- },
254
- get identityAttributes() {
255
- return getIdentityAttributes()
256
- },
257
- }
258
-
259
- // For `createReactorProvider`, which disposes the manager this result
260
- // builds when its tree unmounts. It reads the manager without the getter
261
- // above, so a tree that never touched authentication does not build one
262
- // just to release it. A supplied manager is not this result's to release.
263
- registerAuthentication(result, () =>
264
- providedAuthentication ? undefined : authenticationInstance
265
- )
266
-
267
- return result
268
- }
@@ -1,371 +0,0 @@
1
- import { useSyncExternalStore, useEffect, useRef, useMemo } from "react"
2
- import type { AuthenticationManager } from "../auth/authentication-manager.js"
3
- import type { AuthState, AuthenticationSignInOptions } from "../auth/types.js"
4
- import type { AgentState } from "@ic-reactor/core"
5
- import type { Principal } from "@icp-sdk/core/principal"
6
- import type { Identity } from "@icp-sdk/core/agent"
7
-
8
- export interface UseAuthReturn {
9
- authenticate: () => Promise<Identity | undefined>
10
- login: (options?: AuthenticationSignInOptions) => Promise<void>
11
- logout: (options?: { returnTo?: string }) => Promise<void>
12
- isAuthenticated: boolean
13
- /**
14
- * `true` while a sign-in or sign-out is in progress, and until the session
15
- * restore the first `useAuth()` starts has settled: on the first render, in
16
- * a server render and while the stored session is read. A restore that fails
17
- * settles it too. Show a loading state while it is `true` rather than
18
- * treating `isAuthenticated: false` as signed out.
19
- */
20
- isAuthenticating: boolean
21
- /**
22
- * The signed-in user's principal, or `null` while signed out. A signed-out
23
- * session still holds an anonymous `identity`, and its principal is not
24
- * returned here.
25
- */
26
- principal: Principal | null
27
- identity: Identity | null
28
- error: Error | undefined
29
- }
30
-
31
- export interface CreateAuthHooksReturn {
32
- useAgentState: () => AgentState
33
- useUserPrincipal: () => Principal | null
34
- useAuth: () => UseAuthReturn
35
- }
36
-
37
- /**
38
- * The managers whose session a `useAuth()` has already started restoring.
39
- *
40
- * Restoring ends in `authenticate()`, which publishes `isAuthenticating: true`
41
- * and then the restored state. It used to run once per mounted `useAuth()`
42
- * rather than once per manager, so every consumer that mounted while signed out
43
- * flipped `isAuthenticating` for the whole app, and one that mounted while a
44
- * sign-in popup was open cleared `isAuthenticating` before the sign-in ended. A
45
- * component that hides a `useAuth()` consumer while `isAuthenticating` never
46
- * settled: each restore unmounted the consumer, and each remount restored
47
- * again. Both clients answer from memory once loaded, so that loop ran on the
48
- * microtask queue and the page never painted again.
49
- *
50
- * Keyed by manager rather than by `createAuthHooks` call, because reactors that
51
- * share one `AuthenticationManager` each build their own hooks.
52
- */
53
- const restoredSessions = new WeakSet<AuthenticationManager>()
54
-
55
- /**
56
- * How many `useAuth()` consumers of each manager are mounted, so that a
57
- * restore can tell a manager that was discarded from one StrictMode released
58
- * and mounted again; see `useAuth()`.
59
- */
60
- const mountedConsumers = new WeakMap<AuthenticationManager, number>()
61
-
62
- /**
63
- * The principal both hooks return.
64
- *
65
- * `authenticate()` and `logout()` leave the client's anonymous identity in
66
- * `authState.identity` with `isAuthenticated: false`, so deriving the principal
67
- * from the identity alone reported `2vxsx-fae` for a signed-out user, and
68
- * `principal ? <SignedIn /> : <SignedOut />` rendered the signed-in branch.
69
- * Memoized on the identity, because `getPrincipal()` may build a new object on
70
- * each call and the result is often a hook dependency.
71
- */
72
- function usePrincipal(
73
- isAuthenticated: boolean,
74
- identity: Identity | null
75
- ): Principal | null {
76
- return useMemo(
77
- () => (isAuthenticated && identity ? identity.getPrincipal() : null),
78
- [isAuthenticated, identity]
79
- )
80
- }
81
-
82
- /**
83
- * The auth state a server render shows, and so the one hydration shows.
84
- *
85
- * `useSyncExternalStore` renders `getServerSnapshot` on the server and again
86
- * while hydrating, and both renders have to produce the HTML the server sent.
87
- * The hooks passed the live state for both. A server holds no session, so its
88
- * HTML is signed out. A component that hydrates after the session is restored,
89
- * such as one inside a Suspense boundary whose code arrives later, read the
90
- * restored state and no longer matched that HTML. React then reported a
91
- * hydration error and threw the boundary's server HTML away to render it again
92
- * on the client. A constant also keeps a server render from showing whatever a
93
- * manager shared across requests happens to hold.
94
- *
95
- * It is the state the hooks report before the session has been checked: a
96
- * server cannot know the session, so it renders the "checking" state that a
97
- * browser shows until its restore settles. Once hydrated, React compares it
98
- * with the live state and renders again with that.
99
- */
100
- const SERVER_AUTH_STATE: AuthState = Object.freeze({
101
- identity: null,
102
- isAuthenticating: true,
103
- isAuthenticated: false,
104
- error: undefined,
105
- })
106
-
107
- /**
108
- * Create authentication hooks for managing user sessions with Internet Identity.
109
- *
110
- * @example
111
- * const { useAuth, useUserPrincipal, useAgentState } = createAuthHooks(authentication)
112
- *
113
- * function App() {
114
- * const { login, logout, principal, isAuthenticated } = useAuth()
115
- *
116
- * return isAuthenticated
117
- * ? <button onClick={() => logout()}>Logout {principal?.toText()}</button>
118
- * : <button onClick={() => login()}>Login with II</button>
119
- * }
120
- */
121
- export const createAuthHooks = (
122
- authentication: AuthenticationManager
123
- ): CreateAuthHooksReturn => {
124
- // Passing a ClientManager here is the natural mistake — it is what
125
- // `AuthenticationManager` is built from, and the two are adjacent in every
126
- // setup snippet. TypeScript rejects it, but a JS caller got no error until
127
- // render, where it surfaced as "Cannot destructure property 'isAuthenticated'
128
- // of 'useAuthState(...)' as it is undefined" — which names neither the cause
129
- // nor this function.
130
- if (
131
- !authentication ||
132
- typeof authentication.subscribeAuthState !== "function"
133
- ) {
134
- throw new TypeError(
135
- "[ic-reactor] createAuthHooks() expects an AuthenticationManager, not a " +
136
- "ClientManager. Build one first: " +
137
- "`new AuthenticationManager({ clientManager })`, or take it from " +
138
- "`defineReactor(...).authentication`."
139
- )
140
- }
141
-
142
- const { clientManager } = authentication
143
-
144
- // The agent state a manager starts in, for the reason given at
145
- // SERVER_AUTH_STATE: `useAuth()` initializes the agent once hydrated, so a
146
- // component hydrating later read `isInitialized: true` against server HTML
147
- // rendered before any initialization. The network is kept: it comes from the
148
- // agent's host, which initialization does not change.
149
- const serverAgentState: AgentState = Object.freeze({
150
- isInitialized: false,
151
- isInitializing: false,
152
- error: undefined,
153
- network: clientManager.network,
154
- isLocalhost: clientManager.isLocal,
155
- })
156
-
157
- // What every consumer hands `useSyncExternalStore`, built once rather than
158
- // on each render. React unsubscribes and subscribes again whenever the
159
- // subscribe function changes, and each unsubscribe filters the manager's
160
- // whole subscriber list. An auth state change re-renders every consumer, so
161
- // a function per render cost k re-subscriptions and about k²/2 subscriber
162
- // visits for k consumers: two million for 2,000 `useUserPrincipal()` rows.
163
- const subscribeAgentState = (callback: () => void) =>
164
- clientManager.subscribeAgentState(callback)
165
- const getAgentState = () => clientManager.agentState
166
- const getServerAgentState = () => serverAgentState
167
-
168
- // Until the session has been checked, `authState` is the signed-out state
169
- // the manager starts in, and reporting it as the answer sent signed-in users
170
- // to the login page (#621). `useAuth()` starts the check from an effect, so
171
- // on the first render nothing had been checked, yet `isAuthenticating` was
172
- // false: a guard that redirects when `!isAuthenticating && !isAuthenticated`
173
- // redirected before the session was ever read. The hooks report
174
- // `isAuthenticating: true` until the check settles instead. The snapshot is
175
- // kept per source state, because `useSyncExternalStore` needs the same object
176
- // back until something changes.
177
- let unchecked: { source: AuthState; state: AuthState } | undefined
178
- const subscribeAuthState = (callback: () => void) => {
179
- const unsubscribeState = authentication.subscribeAuthState(callback)
180
- const unsubscribeChecked = authentication.subscribeSessionChecked(callback)
181
- return () => {
182
- unsubscribeState()
183
- unsubscribeChecked()
184
- }
185
- }
186
- const getAuthState = () => {
187
- const state = authentication.authState
188
- if (authentication.sessionChecked || state.isAuthenticating) return state
189
- if (unchecked?.source !== state) {
190
- unchecked = {
191
- source: state,
192
- state: Object.freeze({ ...state, isAuthenticating: true }),
193
- }
194
- }
195
- return unchecked.state
196
- }
197
- const getServerAuthState = () => SERVER_AUTH_STATE
198
-
199
- /**
200
- * Subscribe to agent state changes.
201
- * Returns the current agent state (agent, isInitialized, etc.)
202
- */
203
- const useAgentState = (): AgentState =>
204
- useSyncExternalStore(
205
- subscribeAgentState,
206
- getAgentState,
207
- getServerAgentState
208
- )
209
-
210
- /**
211
- * Subscribe to authentication state changes.
212
- * Returns auth state (isAuthenticated, isAuthenticating, identity, error)
213
- */
214
- const useAuthState = (): AuthState =>
215
- useSyncExternalStore(subscribeAuthState, getAuthState, getServerAuthState)
216
-
217
- /**
218
- * Main authentication hook that provides login/logout methods and auth state.
219
- * Automatically initializes the session on first use, restoring any previous session.
220
- *
221
- * `isAuthenticating` is `true` until that restore has settled, including on
222
- * the first render and in a server render, so a guard that shows a spinner
223
- * while `isAuthenticating` never takes the state before the restore for a
224
- * signed-out user. A restore that fails settles it too.
225
- *
226
- * @example
227
- * function AuthButton() {
228
- * const { login, logout, isAuthenticated, isAuthenticating } = useAuth()
229
- *
230
- * if (isAuthenticated) {
231
- * return <button onClick={() => logout()}>Logout</button>
232
- * }
233
- * return (
234
- * <button onClick={() => login()} disabled={isAuthenticating}>
235
- * {isAuthenticating ? "Connecting..." : "Login"}
236
- * </button>
237
- * )
238
- * }
239
- */
240
- const useAuth = (): UseAuthReturn => {
241
- const { login, logout, authenticate } = authentication
242
- const { isAuthenticated, isAuthenticating, identity, error } =
243
- useAuthState()
244
-
245
- // Keeps a StrictMode re-run of the effect below from repeating it.
246
- const initializedRef = useRef(false)
247
-
248
- // Restore the previous session when the first consumer of this manager
249
- // mounts. `prepareClient` also warms up the AuthClient so a later
250
- // `login()` can open the identity provider window inside the click handler.
251
- useEffect(() => {
252
- mountedConsumers.set(
253
- authentication,
254
- (mountedConsumers.get(authentication) ?? 0) + 1
255
- )
256
- const unmount = () => {
257
- mountedConsumers.set(
258
- authentication,
259
- (mountedConsumers.get(authentication) ?? 1) - 1
260
- )
261
- }
262
-
263
- if (initializedRef.current && restoredSessions.has(authentication)) {
264
- return unmount
265
- }
266
- const firstRun = !initializedRef.current
267
- initializedRef.current = true
268
-
269
- if (restoredSessions.has(authentication)) {
270
- // Restored already. A live session is still checked again, which
271
- // notices a delegation that has lapsed since and publishes nothing
272
- // while it is valid.
273
- if (firstRun && authentication.authState.isAuthenticated) {
274
- authentication.authenticate().catch(() => undefined)
275
- }
276
- return unmount
277
- }
278
- restoredSessions.add(authentication)
279
-
280
- // A provider that builds its managers per mount disposes the manager
281
- // from its cleanup. One that unmounted before this restore had built
282
- // the client still got one, built after `dispose()` and never released,
283
- // so each quick remount left a live client listening to the page. The
284
- // restore stops instead once the manager has been disposed since it
285
- // began and no `useAuth()` of it is mounted, releasing what it built.
286
- // StrictMode's cleanup disposes the manager too, but mounts this
287
- // consumer again before the restore looks, and that restore goes on.
288
- // One that stopped counts as not having run, so a manager mounted again,
289
- // as `<Activity>` mounts a tree it showed again, restores.
290
- const releases = authentication.releaseCount
291
- const abandoned = () => {
292
- if (
293
- authentication.releaseCount === releases ||
294
- (mountedConsumers.get(authentication) ?? 0) > 0
295
- ) {
296
- return false
297
- }
298
- authentication.dispose()
299
- restoredSessions.delete(authentication)
300
- return true
301
- }
302
-
303
- void authentication
304
- .prepareClient()
305
- .catch(() => undefined)
306
- .then(async () => {
307
- if (abandoned()) return false
308
- await clientManager.initialize()
309
- if (abandoned()) return false
310
- // A check that has settled already and found no session, such as
311
- // the one a manager runs over a client handed to its constructor,
312
- // or a route loader's `authenticate()`, answered what this restore
313
- // would. Running it again only flashed `isAuthenticating`, and a
314
- // guard redirected a second time. A live session is still checked,
315
- // which publishes nothing while it is valid.
316
- const { isAuthenticated, error } = authentication.authState
317
- if (authentication.sessionChecked && !isAuthenticated && !error) {
318
- return true
319
- }
320
- await authentication.authenticate().catch(() => undefined)
321
- return !abandoned()
322
- })
323
- // Failures are already reflected in authState/agentState; without
324
- // this the rejection escapes as an unhandled promise rejection. A
325
- // restore that failed before it reached `authenticate()`, such as a
326
- // root key that could not be fetched, has settled all the same.
327
- .catch(() => true)
328
- .then((settled) => {
329
- if (settled) authentication.markSessionChecked()
330
- })
331
-
332
- return unmount
333
- }, [])
334
-
335
- const principal = usePrincipal(isAuthenticated, identity)
336
-
337
- return {
338
- authenticate,
339
- login,
340
- logout,
341
- isAuthenticated,
342
- isAuthenticating,
343
- principal,
344
- identity,
345
- error,
346
- }
347
- }
348
-
349
- /**
350
- * Get the current user's Principal.
351
- * Returns null if not authenticated, including while the signed-out session
352
- * holds the anonymous identity.
353
- *
354
- * @example
355
- * function UserInfo() {
356
- * const principal = useUserPrincipal()
357
- * if (!principal) return null
358
- * return <span>Logged in as: {principal.toText()}</span>
359
- * }
360
- */
361
- const useUserPrincipal = (): Principal | null => {
362
- const { isAuthenticated, identity } = useAuthState()
363
- return usePrincipal(isAuthenticated, identity)
364
- }
365
-
366
- return {
367
- useAuth,
368
- useAgentState,
369
- useUserPrincipal,
370
- }
371
- }