@ic-reactor/react 3.7.0 → 3.8.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.
Files changed (86) hide show
  1. package/README.md +93 -9
  2. package/dist/auth/authentication-manager.d.ts +37 -5
  3. package/dist/auth/authentication-manager.d.ts.map +1 -1
  4. package/dist/auth/authentication-manager.js +111 -37
  5. package/dist/auth/authentication-manager.js.map +1 -1
  6. package/dist/auth/createIdentityAttributeHooks.d.ts +2 -2
  7. package/dist/auth/createIdentityAttributeHooks.d.ts.map +1 -1
  8. package/dist/auth/identity-attributes-manager.d.ts +12 -5
  9. package/dist/auth/identity-attributes-manager.d.ts.map +1 -1
  10. package/dist/auth/identity-attributes-manager.js +42 -28
  11. package/dist/auth/identity-attributes-manager.js.map +1 -1
  12. package/dist/auth/identity-attributes.d.ts +1 -1
  13. package/dist/auth/identity-attributes.d.ts.map +1 -1
  14. package/dist/auth/identity-attributes.js +1 -1
  15. package/dist/auth/identity-attributes.js.map +1 -1
  16. package/dist/auth/index.d.ts +7 -7
  17. package/dist/auth/index.d.ts.map +1 -1
  18. package/dist/auth/index.js +7 -7
  19. package/dist/auth/index.js.map +1 -1
  20. package/dist/auth/types.d.ts +123 -16
  21. package/dist/auth/types.d.ts.map +1 -1
  22. package/dist/createActorHooks.d.ts +4 -4
  23. package/dist/createActorHooks.d.ts.map +1 -1
  24. package/dist/createActorHooks.js +6 -6
  25. package/dist/createActorHooks.js.map +1 -1
  26. package/dist/createInfiniteQuery.d.ts +1 -1
  27. package/dist/createInfiniteQuery.d.ts.map +1 -1
  28. package/dist/createInfiniteQuery.js +1 -1
  29. package/dist/createInfiniteQuery.js.map +1 -1
  30. package/dist/createMutation.d.ts +1 -1
  31. package/dist/createMutation.d.ts.map +1 -1
  32. package/dist/createQuery.d.ts +1 -1
  33. package/dist/createQuery.d.ts.map +1 -1
  34. package/dist/createQuery.js +1 -1
  35. package/dist/createQuery.js.map +1 -1
  36. package/dist/createSuspenseInfiniteQuery.d.ts +1 -1
  37. package/dist/createSuspenseInfiniteQuery.d.ts.map +1 -1
  38. package/dist/createSuspenseInfiniteQuery.js +1 -1
  39. package/dist/createSuspenseInfiniteQuery.js.map +1 -1
  40. package/dist/createSuspenseQuery.d.ts +1 -1
  41. package/dist/createSuspenseQuery.d.ts.map +1 -1
  42. package/dist/createSuspenseQuery.js +1 -1
  43. package/dist/createSuspenseQuery.js.map +1 -1
  44. package/dist/defineReactor.d.ts +54 -4
  45. package/dist/defineReactor.d.ts.map +1 -1
  46. package/dist/defineReactor.js +77 -7
  47. package/dist/defineReactor.js.map +1 -1
  48. package/dist/hooks/createAuthHooks.d.ts +2 -2
  49. package/dist/hooks/createAuthHooks.d.ts.map +1 -1
  50. package/dist/hooks/createAuthHooks.js +7 -2
  51. package/dist/hooks/createAuthHooks.js.map +1 -1
  52. package/dist/hooks/useActorInfiniteQuery.js +1 -1
  53. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  54. package/dist/hooks/useActorSuspenseInfiniteQuery.js +1 -1
  55. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  56. package/dist/index.d.ts +11 -11
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +11 -11
  59. package/dist/index.js.map +1 -1
  60. package/llms.txt +1 -1
  61. package/package.json +14 -9
  62. package/src/auth/authentication-manager.ts +121 -49
  63. package/src/auth/createIdentityAttributeHooks.ts +2 -2
  64. package/src/auth/identity-attributes-manager.ts +53 -44
  65. package/src/auth/identity-attributes.ts +2 -2
  66. package/src/auth/index.ts +7 -7
  67. package/src/auth/types.ts +134 -18
  68. package/src/createActorHooks.ts +9 -9
  69. package/src/createInfiniteQuery.ts +2 -2
  70. package/src/createMutation.ts +1 -1
  71. package/src/createQuery.ts +2 -2
  72. package/src/createSuspenseInfiniteQuery.ts +2 -2
  73. package/src/createSuspenseQuery.ts +2 -2
  74. package/src/defineReactor.ts +136 -14
  75. package/src/hooks/createAuthHooks.ts +8 -3
  76. package/src/hooks/useActorInfiniteQuery.ts +1 -1
  77. package/src/hooks/useActorSuspenseInfiniteQuery.ts +1 -1
  78. package/src/index.ts +11 -11
  79. package/src/defineReactor.test.ts +0 -70
  80. package/src/hooks/useActorInfiniteQuery.test.tsx +0 -457
  81. package/src/hooks/useActorMethod.test.tsx +0 -798
  82. package/src/hooks/useActorMutation.test.tsx +0 -220
  83. package/src/hooks/useActorQuery.test.tsx +0 -287
  84. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +0 -472
  85. package/src/hooks/useActorSuspenseQuery.test.tsx +0 -254
  86. package/src/utils.test.ts +0 -14
package/src/auth/types.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Identity } from "@icp-sdk/core/agent"
1
+ import type { Identity, SignIdentity } from "@icp-sdk/core/agent"
2
+ import type { PartialIdentity } from "@icp-sdk/core/identity"
2
3
  import type { Principal } from "@icp-sdk/core/principal"
3
4
 
4
5
  export interface SignedIdentityAttributes {
@@ -6,9 +7,21 @@ export interface SignedIdentityAttributes {
6
7
  signature: Uint8Array
7
8
  }
8
9
 
10
+ /**
11
+ * The 32-byte nonce issued by the relying-party canister.
12
+ *
13
+ * A function (or promise) lets the Internet Identity window open while the
14
+ * nonce is still being fetched, so the popup is opened inside the user gesture
15
+ * instead of after it. `@icp-sdk/auth` v8 requires the function form; v7
16
+ * accepts the promise form. IC Reactor normalizes whichever you pass to the
17
+ * shape the installed client expects.
18
+ */
19
+ export type IdentityAttributeNonce =
20
+ Uint8Array | Promise<Uint8Array> | (() => Uint8Array | Promise<Uint8Array>)
21
+
9
22
  export interface IdentityAttributeRequest {
10
23
  keys: string[]
11
- nonce: Uint8Array
24
+ nonce: IdentityAttributeNonce
12
25
  }
13
26
 
14
27
  export interface IdentityAttributeValues {
@@ -26,15 +39,102 @@ export interface IdentityAttributeResult {
26
39
  completedAt: string
27
40
  }
28
41
 
42
+ /**
43
+ * The providers that Internet Identity supports for one-click sign-in. Only
44
+ * these three reach the underlying auth client.
45
+ */
29
46
  type IdentityAttributeOpenIdProviderAlias = "google" | "apple" | "microsoft"
30
47
 
48
+ /**
49
+ * An OpenID provider, either as a well-known alias or as a raw issuer URL.
50
+ *
51
+ * The two forms are **not** interchangeable, and what each one does depends on
52
+ * where it is passed:
53
+ *
54
+ * - An alias (`"google" | "apple" | "microsoft"`) is forwarded to the auth
55
+ * client, which adds an `openid` search param to the identity provider URL so
56
+ * the user signs in through that provider directly.
57
+ * - Any other string is treated as a raw issuer URL and is **never** forwarded
58
+ * to the auth client, so it never affects how the user signs in. It is only
59
+ * meaningful when passed to {@link IdentityAttributesManager.requestOpenId}
60
+ * (or `identityAttributeKeys`), which scopes the requested keys to
61
+ * `openid:<issuer>:<key>`. Passed anywhere else it has no effect at all.
62
+ */
31
63
  export type IdentityAttributeOpenIdProvider =
32
64
  IdentityAttributeOpenIdProviderAlias | (string & {})
33
65
 
66
+ /** Session key algorithm used for each sign-in. */
67
+ export type AuthClientKeyType = "ECDSA" | "Ed25519"
68
+
69
+ /** Structural shape of an `@icp-sdk/auth` `AuthClientStorage`. */
70
+ export interface AuthClientStorageLike {
71
+ get(key: string): Promise<string | CryptoKeyPair | null>
72
+ set(key: string, value: string | CryptoKeyPair): Promise<void>
73
+ remove(key: string): Promise<void>
74
+ }
75
+
76
+ /** Idle-session configuration forwarded to the underlying AuthClient. */
77
+ export interface AuthClientIdleOptions {
78
+ /** Called once the user has been idle for `idleTimeout` ms. */
79
+ onIdle?: () => unknown
80
+ /** Idle timeout in ms. @default 600_000 (10 minutes) */
81
+ idleTimeout?: number
82
+ /** Treat scroll events as activity. @default false */
83
+ captureScroll?: boolean
84
+ /** Scroll debounce in ms. @default 100 */
85
+ scrollDebounce?: number
86
+ /** Disable idle detection entirely. @default false */
87
+ disableIdle?: boolean
88
+ /**
89
+ * Disable the built-in idle callback, which signs the user out and reloads
90
+ * the page. @default false
91
+ */
92
+ disableDefaultIdleCallback?: boolean
93
+ }
94
+
34
95
  export interface AuthenticationClientOptions {
96
+ /** Identity provider URL. @default "https://id.ai/authorize" */
35
97
  identityProvider?: string | URL
98
+ /** `window.open` features string for the identity provider popup. */
36
99
  windowOpenerFeatures?: string
100
+ /**
101
+ * Provider for one-click sign-in.
102
+ *
103
+ * Only the `"google" | "apple" | "microsoft"` aliases reach the auth client.
104
+ * Any other value is dropped here and has **no effect** — raw issuer URLs are
105
+ * only meaningful on `requestOpenId`, where they scope the requested keys.
106
+ * @see {@link IdentityAttributeOpenIdProvider}
107
+ */
37
108
  openIdProvider?: IdentityAttributeOpenIdProvider
109
+ /**
110
+ * Derivation origin for the identity provider.
111
+ *
112
+ * Required when the app is served from more than one origin (e.g. a custom
113
+ * domain plus `<canisterId>.icp0.io`) and every origin must resolve to the
114
+ * same principal.
115
+ * @see https://github.com/dfinity/internet-identity/blob/main/docs/internet-identity-spec.adoc
116
+ */
117
+ derivationOrigin?: string | URL
118
+ /** Persistent storage backend. @default IndexedDB */
119
+ storage?: AuthClientStorageLike
120
+ /**
121
+ * Session key algorithm. Use `"Ed25519"` when the storage backend cannot
122
+ * hold a `CryptoKey`. @default "ECDSA"
123
+ */
124
+ keyType?: AuthClientKeyType
125
+ /**
126
+ * Idle timeout configuration. Pass `{ disableIdle: true }` to opt out of the
127
+ * default behaviour, which signs the user out and reloads after 10 minutes.
128
+ */
129
+ idleOptions?: AuthClientIdleOptions
130
+ /** An existing identity to authenticate via delegation. */
131
+ identity?: SignIdentity | PartialIdentity
132
+ /**
133
+ * How the client talks to the identity provider. `"redirect"` requires
134
+ * `@icp-sdk/auth` v8 or later and is ignored by older versions.
135
+ * @default "window"
136
+ */
137
+ transport?: "window" | "redirect"
38
138
  }
39
139
 
40
140
  export interface AuthClientSignInOptions {
@@ -48,36 +148,52 @@ export interface AuthenticationSignInOptions
48
148
  onError?: (error?: string) => void | Promise<void>
49
149
  }
50
150
 
51
- export interface RequestIdentityAttributesParameters {
151
+ export interface RequestIdentityAttributesParameters extends AuthenticationClientOptions {
52
152
  keys: string[]
53
- nonce: Uint8Array
54
- identityProvider?: string | URL
55
- openIdProvider?: IdentityAttributeOpenIdProvider
56
- windowOpenerFeatures?: string
153
+ nonce: IdentityAttributeNonce
154
+ /**
155
+ * Sign in as part of the same identity-provider interaction.
156
+ * @default true
157
+ */
57
158
  signIn?: boolean
58
159
  maxTimeToLive?: bigint
59
160
  targets?: Principal[]
60
161
  }
61
162
 
62
- export interface RequestOpenIdIdentityAttributesParameters {
63
- nonce: Uint8Array
163
+ export interface RequestOpenIdIdentityAttributesParameters extends Omit<
164
+ RequestIdentityAttributesParameters,
165
+ "openIdProvider"
166
+ > {
167
+ /**
168
+ * Provider the requested `keys` are scoped to, producing
169
+ * `openid:<issuer>:<key>`.
170
+ *
171
+ * Passing one of the `"google" | "apple" | "microsoft"` aliases additionally
172
+ * routes sign-in through that provider, so the user grants attribute access
173
+ * in the same step. A raw issuer URL only scopes the keys.
174
+ * @see {@link IdentityAttributeOpenIdProvider}
175
+ */
64
176
  openIdProvider: IdentityAttributeOpenIdProvider
65
- keys: string[]
66
- identityProvider?: string | URL
67
- windowOpenerFeatures?: string
68
- signIn?: boolean
69
- maxTimeToLive?: bigint
70
- targets?: Principal[]
71
177
  }
72
178
 
179
+ /**
180
+ * Structural subset of `@icp-sdk/auth`'s `AuthClient` that IC Reactor relies
181
+ * on. Declared locally so `@icp-sdk/auth` stays an optional peer dependency.
182
+ */
73
183
  export interface AuthClientLike {
74
184
  getIdentity(): Promise<Identity> | Identity
75
185
  isAuthenticated(): Promise<boolean> | boolean
76
186
  signIn(options?: AuthClientSignInOptions): Promise<Identity>
77
187
  signOut(options?: { returnTo?: string }): Promise<void>
78
- requestAttributes(
79
- params: IdentityAttributeRequest
80
- ): Promise<SignedIdentityAttributes>
188
+ requestAttributes(params: {
189
+ keys: string[]
190
+ // Widened across `@icp-sdk/auth` v7 (value or promise) and v8 (thunk).
191
+ // Method-shorthand parameters are bivariant, so both real signatures are
192
+ // assignable to this one.
193
+ nonce: Uint8Array | Promise<Uint8Array> | (() => Promise<Uint8Array>)
194
+ }): Promise<SignedIdentityAttributes>
195
+ /** Present from `@icp-sdk/auth` v8; used to detect the nonce contract. */
196
+ memoize?<T>(produce: () => T | Promise<T>): Promise<T>
81
197
  }
82
198
 
83
199
  export interface AuthState {
@@ -34,18 +34,18 @@ import {
34
34
  UseMutationResult,
35
35
  InfiniteData,
36
36
  } from "@tanstack/react-query"
37
- import { useActorQuery } from "./hooks/useActorQuery"
38
- import { useActorSuspenseQuery } from "./hooks/useActorSuspenseQuery"
39
- import { useActorInfiniteQuery } from "./hooks/useActorInfiniteQuery"
40
- import { useActorSuspenseInfiniteQuery } from "./hooks/useActorSuspenseInfiniteQuery"
41
- import { useActorMutation } from "./hooks/useActorMutation"
37
+ import { useActorQuery } from "./hooks/useActorQuery.js"
38
+ import { useActorSuspenseQuery } from "./hooks/useActorSuspenseQuery.js"
39
+ import { useActorInfiniteQuery } from "./hooks/useActorInfiniteQuery.js"
40
+ import { useActorSuspenseInfiniteQuery } from "./hooks/useActorSuspenseInfiniteQuery.js"
41
+ import { useActorMutation } from "./hooks/useActorMutation.js"
42
42
  import {
43
43
  useActorMethod,
44
44
  UseActorMethodParameters,
45
- } from "./hooks/useActorMethod"
46
- import { InfiniteQueryConfig } from "./createInfiniteQuery"
47
- import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery"
48
- import { QueryConfig, SuspenseQueryConfig, MutationConfig } from "./types"
45
+ } from "./hooks/useActorMethod.js"
46
+ import { InfiniteQueryConfig } from "./createInfiniteQuery.js"
47
+ import { SuspenseInfiniteQueryConfig } from "./createSuspenseInfiniteQuery.js"
48
+ import { QueryConfig, SuspenseQueryConfig, MutationConfig } from "./types.js"
49
49
 
50
50
  export type ActorHooks<Service, Transform extends TransformKey> = {
51
51
  useActorQuery: {
@@ -43,8 +43,8 @@ import {
43
43
  InfiniteQueryObserverOptions,
44
44
  } from "@tanstack/react-query"
45
45
  import { CallConfig } from "@icp-sdk/core/agent"
46
- import { NoInfer } from "./types"
47
- import { mergeFactoryQueryKey, normalizeQueryData } from "./utils"
46
+ import { NoInfer } from "./types.js"
47
+ import { mergeFactoryQueryKey, normalizeQueryData } from "./utils.js"
48
48
 
49
49
  type InfiniteQueryFactoryFn<
50
50
  Service,
@@ -39,7 +39,7 @@ import type {
39
39
  MutationResult,
40
40
  MutationHookOptions,
41
41
  NoInfer,
42
- } from "./types"
42
+ } from "./types.js"
43
43
 
44
44
  // ============================================================================
45
45
  // Internal helpers
@@ -44,8 +44,8 @@ import type {
44
44
  QueryResult,
45
45
  QueryFactoryConfig,
46
46
  NoInfer,
47
- } from "./types"
48
- import { buildChainedSelect } from "./utils"
47
+ } from "./types.js"
48
+ import { buildChainedSelect } from "./utils.js"
49
49
 
50
50
  // ============================================================================
51
51
  // Internal Implementation
@@ -45,8 +45,8 @@ import {
45
45
  InfiniteQueryObserverOptions,
46
46
  } from "@tanstack/react-query"
47
47
  import { CallConfig } from "@icp-sdk/core/agent"
48
- import { NoInfer } from "./types"
49
- import { mergeFactoryQueryKey, normalizeQueryData } from "./utils"
48
+ import { NoInfer } from "./types.js"
49
+ import { mergeFactoryQueryKey, normalizeQueryData } from "./utils.js"
50
50
 
51
51
  type SuspenseInfiniteFactoryCallOptions = {
52
52
  queryKey?: QueryKey
@@ -34,8 +34,8 @@ import type {
34
34
  SuspenseQueryResult,
35
35
  SuspenseQueryFactoryConfig,
36
36
  NoInfer,
37
- } from "./types"
38
- import { buildChainedSelect } from "./utils"
37
+ } from "./types.js"
38
+ import { buildChainedSelect } from "./utils.js"
39
39
 
40
40
  // ============================================================================
41
41
  // Internal Implementation
@@ -33,8 +33,26 @@
33
33
  * name: "index",
34
34
  * idlFactory: indexIdl,
35
35
  * clientManager: ledger.clientManager,
36
+ * authentication: ledger.authentication, // one Internet Identity session
36
37
  * })
37
38
  * ```
39
+ *
40
+ * @example Internet Identity is wired up out of the box
41
+ * ```typescript
42
+ * const { useAuth, useIdentityAttributes } = defineReactor<_SERVICE>({
43
+ * name: "backend",
44
+ * idlFactory,
45
+ * // Needed when the app is served from more than one origin.
46
+ * auth: { derivationOrigin: "https://app.example.com" },
47
+ * })
48
+ *
49
+ * function LoginButton() {
50
+ * const { login, logout, isAuthenticated, principal } = useAuth()
51
+ * return isAuthenticated
52
+ * ? <button onClick={() => logout()}>{principal?.toText()}</button>
53
+ * : <button onClick={() => login()}>Sign in</button>
54
+ * }
55
+ * ```
38
56
  */
39
57
  import { ClientManager, Reactor, DisplayReactor } from "@ic-reactor/core"
40
58
  import type {
@@ -45,7 +63,14 @@ import type {
45
63
  DisplayReactorParameters,
46
64
  } from "@ic-reactor/core"
47
65
  import { QueryClient } from "@tanstack/react-query"
48
- import { createActorHooks, ActorHooks } from "./createActorHooks"
66
+ import { createActorHooks, ActorHooks } from "./createActorHooks.js"
67
+ import { AuthenticationManager } from "./auth/authentication-manager.js"
68
+ import { IdentityAttributesManager } from "./auth/identity-attributes-manager.js"
69
+ import { createIdentityAttributeHooks } from "./auth/createIdentityAttributeHooks.js"
70
+ import type { UseIdentityAttributesReturn } from "./auth/createIdentityAttributeHooks.js"
71
+ import { createAuthHooks } from "./hooks/createAuthHooks.js"
72
+ import type { CreateAuthHooksReturn } from "./hooks/createAuthHooks.js"
73
+ import type { AuthenticationManagerParameters } from "./auth/authentication-manager.js"
49
74
 
50
75
  /** Options shared by both the standard and display variants of defineReactor. */
51
76
  export interface DefineReactorSharedParameters
@@ -55,13 +80,35 @@ export interface DefineReactorSharedParameters
55
80
  /**
56
81
  * Reuse an existing ClientManager (e.g. to share one agent across canisters).
57
82
  * When omitted, a ClientManager is created from the agent options below.
83
+ *
84
+ * A supplied or adopted manager brings its own agent and QueryClient, so
85
+ * `agentOptions` and `queryClient` apply only when this call creates one.
58
86
  */
59
87
  clientManager?: ClientManager
60
88
  /**
61
- * Reuse an existing QueryClient. When omitted, the provided ClientManager's
62
- * QueryClient is reused, or a new one is created.
89
+ * QueryClient for a ClientManager created by this call.
90
+ *
91
+ * Ignored when `clientManager` or `authentication` is supplied — queries run
92
+ * against that manager's own QueryClient, which is what is returned.
63
93
  */
64
94
  queryClient?: QueryClient
95
+ /**
96
+ * Reuse an existing AuthenticationManager, so several reactors share one
97
+ * Internet Identity session. When omitted, one is created for this reactor.
98
+ *
99
+ * Its `clientManager` is adopted for this reactor, so sign-in updates the
100
+ * same agent the reactor calls through. Supplying a different `clientManager`
101
+ * alongside it is rejected.
102
+ */
103
+ authentication?: AuthenticationManager
104
+ /**
105
+ * Internet Identity options forwarded to the AuthenticationManager
106
+ * (`identityProvider`, `derivationOrigin`, `idleOptions`, `storage`, …).
107
+ *
108
+ * Mutually exclusive with `authentication`: a manager built elsewhere is
109
+ * already configured, so these could not be applied to it.
110
+ */
111
+ auth?: Omit<AuthenticationManagerParameters, "clientManager">
65
112
  }
66
113
 
67
114
  /** Parameters for a standard (raw Candid types) reactor. */
@@ -83,11 +130,17 @@ export type DefineReactorResult<
83
130
  Service,
84
131
  Transform extends TransformKey,
85
132
  R extends Reactor<Service, Transform>,
86
- > = ActorHooks<Service, Transform> & {
87
- reactor: R
88
- clientManager: ClientManager
89
- queryClient: QueryClient
90
- }
133
+ > = ActorHooks<Service, Transform> &
134
+ CreateAuthHooksReturn & {
135
+ reactor: R
136
+ clientManager: ClientManager
137
+ queryClient: QueryClient
138
+ /** Internet Identity session manager backing `useAuth`. */
139
+ authentication: AuthenticationManager
140
+ /** Signed identity attribute requests backing `useIdentityAttributes`. */
141
+ identityAttributes: IdentityAttributesManager
142
+ useIdentityAttributes: () => UseIdentityAttributesReturn
143
+ }
91
144
 
92
145
  export function defineReactor<Service = BaseActor>(
93
146
  params: DefineDisplayReactorParameters<Service>
@@ -104,6 +157,8 @@ export function defineReactor<Service = BaseActor>(
104
157
  const {
105
158
  clientManager: providedClientManager,
106
159
  queryClient: providedQueryClient,
160
+ authentication: providedAuthentication,
161
+ auth,
107
162
  display,
108
163
  agentOptions,
109
164
  name,
@@ -112,18 +167,44 @@ export function defineReactor<Service = BaseActor>(
112
167
  pollingOptions,
113
168
  } = params as DefineDisplayReactorParameters<Service>
114
169
 
115
- const queryClient =
116
- providedQueryClient ??
117
- providedClientManager?.queryClient ??
118
- new QueryClient()
170
+ // A shared AuthenticationManager updates the identity on its own
171
+ // ClientManager. Giving this reactor a different one would leave its calls
172
+ // anonymous after sign-in, so adopt the manager's rather than building a new
173
+ // one, and refuse an explicit mismatch instead of splitting them silently.
174
+ if (
175
+ providedClientManager &&
176
+ providedAuthentication &&
177
+ providedAuthentication.clientManager !== providedClientManager
178
+ ) {
179
+ throw new Error(
180
+ `[ic-reactor] defineReactor("${name}") received an \`authentication\` manager bound to a different \`clientManager\`. ` +
181
+ `Sign-in would update the authentication manager's agent while this reactor calls through another one, ` +
182
+ `leaving its calls anonymous. Pass \`clientManager: authentication.clientManager\`, or omit \`clientManager\` to adopt it.`
183
+ )
184
+ }
185
+
186
+ // `auth` configures a manager this call would build; an existing one is
187
+ // already constructed, so these options could only be dropped on the floor.
188
+ if (providedAuthentication && auth) {
189
+ throw new Error(
190
+ `[ic-reactor] defineReactor("${name}") received both \`authentication\` and \`auth\`. ` +
191
+ `The supplied manager is already configured, so \`auth\` (${Object.keys(auth).join(", ")}) would be ignored. ` +
192
+ `Pass those options where that AuthenticationManager is created, or drop \`authentication\` to build one here.`
193
+ )
194
+ }
119
195
 
120
196
  const clientManager =
121
197
  providedClientManager ??
198
+ providedAuthentication?.clientManager ??
122
199
  new ClientManager({
123
- queryClient,
200
+ queryClient: providedQueryClient ?? new QueryClient(),
124
201
  agentOptions,
125
202
  })
126
203
 
204
+ // Always report the QueryClient actually in use: when a ClientManager is
205
+ // supplied or adopted, its own QueryClient is the one queries run against.
206
+ const queryClient = clientManager.queryClient
207
+
127
208
  const reactorConfig = {
128
209
  clientManager,
129
210
  name,
@@ -146,5 +227,46 @@ export function defineReactor<Service = BaseActor>(
146
227
 
147
228
  const hooks = createActorHooks(reactor)
148
229
 
149
- return { ...hooks, reactor, clientManager, queryClient }
230
+ // Auth is built on first use. `AuthenticationManager` dynamically imports the
231
+ // optional `@icp-sdk/auth` peer as soon as it is constructed, and reactors
232
+ // that never touch authentication should not pay for that.
233
+ let authenticationInstance: AuthenticationManager | undefined
234
+ const getAuthentication = () =>
235
+ (authenticationInstance ??=
236
+ providedAuthentication ??
237
+ new AuthenticationManager({ ...auth, clientManager }))
238
+
239
+ let identityAttributesInstance: IdentityAttributesManager | undefined
240
+ const getIdentityAttributes = () =>
241
+ (identityAttributesInstance ??= new IdentityAttributesManager(
242
+ getAuthentication()
243
+ ))
244
+
245
+ let authHooks: CreateAuthHooksReturn | undefined
246
+ const getAuthHooks = () =>
247
+ (authHooks ??= createAuthHooks(getAuthentication()))
248
+
249
+ let attributeHooks:
250
+ ReturnType<typeof createIdentityAttributeHooks> | undefined
251
+ const getAttributeHooks = () =>
252
+ (attributeHooks ??= createIdentityAttributeHooks(getIdentityAttributes()))
253
+
254
+ return {
255
+ ...hooks,
256
+ reactor,
257
+ clientManager,
258
+ queryClient,
259
+ // Stable wrappers: the hook call order inside them never changes, so the
260
+ // rules of hooks still hold.
261
+ useAuth: () => getAuthHooks().useAuth(),
262
+ useAgentState: () => getAuthHooks().useAgentState(),
263
+ useUserPrincipal: () => getAuthHooks().useUserPrincipal(),
264
+ useIdentityAttributes: () => getAttributeHooks().useIdentityAttributes(),
265
+ get authentication() {
266
+ return getAuthentication()
267
+ },
268
+ get identityAttributes() {
269
+ return getIdentityAttributes()
270
+ },
271
+ }
150
272
  }
@@ -1,6 +1,6 @@
1
1
  import { useSyncExternalStore, useEffect, useRef, useMemo } from "react"
2
- import type { AuthenticationManager } from "../auth/authentication-manager"
3
- import type { AuthState, AuthenticationSignInOptions } from "../auth/types"
2
+ import type { AuthenticationManager } from "../auth/authentication-manager.js"
3
+ import type { AuthState, AuthenticationSignInOptions } from "../auth/types.js"
4
4
  import type { AgentState } from "@ic-reactor/core"
5
5
  import type { Principal } from "@icp-sdk/core/principal"
6
6
  import type { Identity } from "@icp-sdk/core/agent"
@@ -90,7 +90,9 @@ export const createAuthHooks = (
90
90
  // Track if we've already initialized to avoid duplicate calls
91
91
  const initializedRef = useRef(false)
92
92
 
93
- // Auto-initialize on first mount to restore previous session
93
+ // Auto-initialize on first mount to restore previous session.
94
+ // `prepareClient` also warms up the AuthClient so a later `login()` can
95
+ // open the identity provider window inside the click handler.
94
96
  useEffect(() => {
95
97
  if (!initializedRef.current) {
96
98
  initializedRef.current = true
@@ -99,6 +101,9 @@ export const createAuthHooks = (
99
101
  .catch(() => undefined)
100
102
  .then(() => clientManager.initialize())
101
103
  .then(() => authentication.authenticate())
104
+ // Failures are already reflected in authState/agentState; without
105
+ // this the rejection escapes as an unhandled promise rejection.
106
+ .catch(() => undefined)
102
107
  }
103
108
  }, [])
104
109
 
@@ -16,7 +16,7 @@ import {
16
16
  ReactorReturnErr,
17
17
  } from "@ic-reactor/core"
18
18
  import { CallConfig } from "@icp-sdk/core/agent"
19
- import { normalizeQueryData } from "../utils"
19
+ import { normalizeQueryData } from "../utils.js"
20
20
 
21
21
  /**
22
22
  * Parameters for useActorInfiniteQuery hook.
@@ -16,7 +16,7 @@ import {
16
16
  ReactorReturnErr,
17
17
  } from "@ic-reactor/core"
18
18
  import { CallConfig } from "@icp-sdk/core/agent"
19
- import { normalizeQueryData } from "../utils"
19
+ import { normalizeQueryData } from "../utils.js"
20
20
 
21
21
  /**
22
22
  * Parameters for useActorSuspenseInfiniteQuery hook.
package/src/index.ts CHANGED
@@ -3,21 +3,21 @@
3
3
  export * from "@ic-reactor/core"
4
4
 
5
5
  // Re-export hooks
6
- export * from "./hooks"
6
+ export * from "./hooks/index.js"
7
7
 
8
8
  // Validation utilities for React
9
- export * from "./validation"
9
+ export * from "./validation.js"
10
10
 
11
11
  // React-specific exports
12
- export * from "./createActorHooks"
13
- export * from "./defineReactor"
12
+ export * from "./createActorHooks.js"
13
+ export * from "./defineReactor.js"
14
14
 
15
- export * from "./createQuery"
16
- export * from "./createSuspenseQuery"
17
- export * from "./createInfiniteQuery"
18
- export * from "./createSuspenseInfiniteQuery"
19
- export * from "./createMutation"
15
+ export * from "./createQuery.js"
16
+ export * from "./createSuspenseQuery.js"
17
+ export * from "./createInfiniteQuery.js"
18
+ export * from "./createSuspenseInfiniteQuery.js"
19
+ export * from "./createMutation.js"
20
20
 
21
- export * from "./auth"
21
+ export * from "./auth/index.js"
22
22
 
23
- export * from "./types"
23
+ export * from "./types.js"
@@ -1,70 +0,0 @@
1
- import { describe, it, expect } from "vitest"
2
- import { ClientManager, Reactor, DisplayReactor } from "@ic-reactor/core"
3
- import { QueryClient } from "@tanstack/react-query"
4
- import { defineReactor } from "./defineReactor"
5
- import { ActorMethod } from "@icp-sdk/core/agent"
6
-
7
- const idlFactory = ({ IDL }: any) =>
8
- IDL.Service({
9
- greet: IDL.Func([IDL.Text], [IDL.Text], ["query"]),
10
- })
11
-
12
- interface TestActor {
13
- greet: ActorMethod<[string], string>
14
- }
15
-
16
- const baseConfig = {
17
- name: "test",
18
- idlFactory,
19
- canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
20
- }
21
-
22
- describe("defineReactor", () => {
23
- it("bootstraps a standard Reactor with hooks and infrastructure", () => {
24
- const result = defineReactor<TestActor>(baseConfig)
25
-
26
- expect(result.reactor).toBeInstanceOf(Reactor)
27
- expect(result.reactor).not.toBeInstanceOf(DisplayReactor)
28
- expect(result.clientManager).toBeInstanceOf(ClientManager)
29
- expect(result.queryClient).toBeInstanceOf(QueryClient)
30
- expect(typeof result.useActorQuery).toBe("function")
31
- expect(typeof result.useActorMutation).toBe("function")
32
- })
33
-
34
- it("creates a DisplayReactor when display is true", () => {
35
- const result = defineReactor<TestActor>({ ...baseConfig, display: true })
36
-
37
- expect(result.reactor).toBeInstanceOf(DisplayReactor)
38
- })
39
-
40
- it("wires the reactor to the created ClientManager and QueryClient", () => {
41
- const { reactor, clientManager, queryClient } =
42
- defineReactor<TestActor>(baseConfig)
43
-
44
- expect(reactor.clientManager).toBe(clientManager)
45
- expect(clientManager.queryClient).toBe(queryClient)
46
- })
47
-
48
- it("reuses an existing ClientManager and its QueryClient", () => {
49
- const queryClient = new QueryClient()
50
- const clientManager = new ClientManager({ queryClient })
51
-
52
- const result = defineReactor<TestActor>({ ...baseConfig, clientManager })
53
-
54
- expect(result.clientManager).toBe(clientManager)
55
- expect(result.queryClient).toBe(queryClient)
56
- expect(result.reactor.clientManager).toBe(clientManager)
57
- })
58
-
59
- it("lets multiple reactors share one ClientManager", () => {
60
- const ledger = defineReactor<TestActor>({ ...baseConfig, name: "ledger" })
61
- const index = defineReactor<TestActor>({
62
- ...baseConfig,
63
- name: "index",
64
- clientManager: ledger.clientManager,
65
- })
66
-
67
- expect(index.clientManager).toBe(ledger.clientManager)
68
- expect(index.reactor).not.toBe(ledger.reactor)
69
- })
70
- })