@swr-login/react 0.7.0 → 0.9.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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { SWRLoginConfig, AuthResponse, AuthInjector, User, TokenAdapter, PluginManager, TokenManager, AuthEventEmitter, AuthStateMachine, BroadcastSync } from '@swr-login/core';
2
+ import { SWRLoginConfig, AuthResponse, AuthInjector, User, UserChangeSource, UserChangeEvent, TokenAdapter, PluginManager, TokenManager, AuthEventEmitter, AuthStateMachine, BroadcastSync } from '@swr-login/core';
3
3
  import React from 'react';
4
4
 
5
5
  interface SWRLoginProviderProps {
@@ -196,6 +196,30 @@ interface UseUserReturn<T extends User = User> {
196
196
  * Call with `null` to clear, or with a user object to update.
197
197
  */
198
198
  mutate: (data?: T | null) => Promise<void>;
199
+ /**
200
+ * Why the most recent `user` transition happened.
201
+ *
202
+ * - `null` — No user-change has been observed yet in this component
203
+ * (e.g. before the first `fetchUser` resolves on mount).
204
+ * - `'initial'` — First time the Provider observed a user value
205
+ * (including `null` for unauthenticated cold start).
206
+ * - `'login'` — Triggered by an explicit `login()` / multi-step finalize
207
+ * / `injectAuth()` call.
208
+ * - `'logout'` — Triggered by an explicit `logout()` / `injectLogout()`.
209
+ * - `'revalidate'` — SWR background revalidation produced a different user.
210
+ * - `'external'` — Cross-tab sync via BroadcastChannel / storage events.
211
+ *
212
+ * Use this to differentiate user-initiated transitions from passive ones,
213
+ * e.g. to suppress a welcome toast on page refresh.
214
+ */
215
+ lastChangeSource: UserChangeSource | null;
216
+ /**
217
+ * Full event object for the most recent `user` transition, including
218
+ * `previousUser` and `timestamp`. `null` before the first transition.
219
+ *
220
+ * See `lastChangeSource` for a quick discriminator.
221
+ */
222
+ lastChangeEvent: UserChangeEvent<T> | null;
199
223
  }
200
224
  /**
201
225
  * Hook to access current user data via SWR cache.
@@ -213,9 +237,112 @@ interface UseUserReturn<T extends User = User> {
213
237
  * if (!isAuthenticated) return <LoginPage />;
214
238
  * return <Dashboard user={user} />;
215
239
  * ```
240
+ *
241
+ * @example Distinguish user-change sources
242
+ * ```tsx
243
+ * const { user, lastChangeSource } = useUser();
244
+ *
245
+ * useEffect(() => {
246
+ * // Only auto-redirect when we detect an *existing* session on mount,
247
+ * // not when the user just pressed the "Login" button (the login form
248
+ * // is responsible for its own redirect there).
249
+ * if (user && lastChangeSource === 'initial') {
250
+ * showRedirectOverlay();
251
+ * }
252
+ * }, [user, lastChangeSource]);
253
+ * ```
216
254
  */
217
255
  declare function useUser<T extends User = User>(): UseUserReturn<T>;
218
256
 
257
+ /**
258
+ * Subscribe to `user-change` events as a **discrete event stream**.
259
+ *
260
+ * Unlike `useUser().user` (which reflects the *current* user value), this
261
+ * hook returns the most recent transition event and re-renders the caller
262
+ * only when a new transition occurs. It's intended for `useEffect`-driven
263
+ * side effects that care about *when* and *why* the user changed rather
264
+ * than the current user value itself.
265
+ *
266
+ * Returns `null` until the first transition is observed by the Provider.
267
+ *
268
+ * @typeParam T - Concrete user type (defaults to base `User`)
269
+ *
270
+ * @example
271
+ * ```tsx
272
+ * const change = useUserChange<MyUser>();
273
+ *
274
+ * useEffect(() => {
275
+ * if (change?.source === 'login') {
276
+ * toast.success(`Welcome back, ${change.user?.name}!`);
277
+ * }
278
+ * }, [change]);
279
+ * ```
280
+ *
281
+ * @example Filter by source
282
+ * ```tsx
283
+ * const change = useUserChange();
284
+ * useEffect(() => {
285
+ * if (change?.source === 'external') {
286
+ * // Another tab just logged in / out — refresh local UI
287
+ * refreshSidebar();
288
+ * }
289
+ * }, [change]);
290
+ * ```
291
+ */
292
+ declare function useUserChange<T extends User = User>(): UserChangeEvent<T> | null;
293
+ /**
294
+ * Register a side-effect callback that fires on every `user-change` event,
295
+ * **without** triggering a re-render of the calling component.
296
+ *
297
+ * Ideal for analytics / logging / imperative side effects that don't need
298
+ * to participate in React's render cycle.
299
+ *
300
+ * The callback receives the full `UserChangeEvent` (including `previousUser`,
301
+ * `timestamp`, and `source`). The listener is automatically unsubscribed on
302
+ * unmount.
303
+ *
304
+ * Note: the callback reference is stored in a ref and always called with
305
+ * the latest closure — you do NOT need to memoize it with `useCallback`.
306
+ *
307
+ * @example Analytics
308
+ * ```tsx
309
+ * useUserChangeEffect((e) => {
310
+ * if (e.source === 'login') {
311
+ * analytics.track('user_login', { userId: e.user?.id });
312
+ * }
313
+ * if (e.source === 'logout') {
314
+ * analytics.track('user_logout', { userId: e.previousUser?.id });
315
+ * }
316
+ * });
317
+ * ```
318
+ *
319
+ * @example Filter sources
320
+ * ```tsx
321
+ * useUserChangeEffect((e) => {
322
+ * // Only react to passive revalidations that flipped the user to null
323
+ * // (e.g. server returned 401 → session expired silently).
324
+ * if (e.source === 'revalidate' && e.user === null && e.previousUser) {
325
+ * showReLoginPrompt();
326
+ * }
327
+ * });
328
+ * ```
329
+ */
330
+ declare function useUserChangeEffect<T extends User = User>(listener: (event: UserChangeEvent<T>) => void): void;
331
+ /**
332
+ * Convenience hook: fires the callback only when the change source matches
333
+ * the given filter. Thin wrapper around {@link useUserChangeEffect}.
334
+ *
335
+ * @example
336
+ * ```tsx
337
+ * // Only run on explicit login (not on cold-start initial load)
338
+ * useUserChangeOn('login', (e) => router.push('/dashboard'));
339
+ *
340
+ * // Subscribe to multiple sources at once
341
+ * useUserChangeOn(['login', 'external'], (e) => refreshSidebar());
342
+ * ```
343
+ */
344
+ declare function useUserChangeOn<T extends User = User>(source: UserChangeSource | UserChangeSource[], listener: (event: UserChangeEvent<T>) => void): void;
345
+
219
346
  interface UseLogoutOptions {
220
347
  /** Specific plugin to call logout on */
221
348
  pluginName?: string;
@@ -410,6 +537,26 @@ interface AuthGuardProps {
410
537
  */
411
538
  declare function AuthGuard({ children, permissions, roles, requireAll, fallback, loadingComponent, }: AuthGuardProps): react_jsx_runtime.JSX.Element;
412
539
 
540
+ /**
541
+ * Mutable hint object shared between the Provider and `useUser()` to describe
542
+ * *why* the next user-change event is expected to happen.
543
+ *
544
+ * The Provider writes into this object as it observes `login` / `logout` /
545
+ * `external` events; `useUser()` reads and clears it when the SWR cache value
546
+ * actually transitions. Unset/expired hint → the change is a passive
547
+ * `revalidate` or the very first `initial` load.
548
+ *
549
+ * Using a mutable ref-like object (instead of React state) avoids re-renders
550
+ * and is safe because it is written synchronously from event callbacks and
551
+ * read synchronously during `useUser()`'s `useEffect`.
552
+ *
553
+ * @internal
554
+ */
555
+ interface UserChangeHint {
556
+ source: UserChangeSource | null;
557
+ /** `Date.now()` when the hint was written. Stale hints (>1s) are ignored. */
558
+ timestamp: number;
559
+ }
413
560
  /** Internal context value passed through SWRLoginProvider */
414
561
  interface AuthContextValue {
415
562
  pluginManager: PluginManager;
@@ -418,6 +565,8 @@ interface AuthContextValue {
418
565
  stateMachine: AuthStateMachine;
419
566
  broadcastSync: BroadcastSync | null;
420
567
  config: SWRLoginConfig;
568
+ /** @internal shared hint for the next user-change source */
569
+ userChangeHint: UserChangeHint;
421
570
  }
422
571
  /**
423
572
  * Internal hook to access auth context.
@@ -426,4 +575,4 @@ interface AuthContextValue {
426
575
  */
427
576
  declare function useAuthContext(): AuthContextValue;
428
577
 
429
- export { AUTH_KEY, type AuthContextValue, AuthGuard, type AuthGuardProps, SWRLoginProvider, type SWRLoginProviderProps, type SessionInfo, type UseAdapterReturn, type UseLoginOptions, type UseLoginReturn, type UseLogoutOptions, type UseLogoutReturn, type UseMultiStepLoginReturn, type UsePermissionReturn, type UseUserReturn, useAdapter, useAuthContext, useAuthInjector, useLogin, useLogout, useMultiStepLogin, usePermission, useSession, useUser };
578
+ export { AUTH_KEY, type AuthContextValue, AuthGuard, type AuthGuardProps, SWRLoginProvider, type SWRLoginProviderProps, type SessionInfo, type UseAdapterReturn, type UseLoginOptions, type UseLoginReturn, type UseLogoutOptions, type UseLogoutReturn, type UseMultiStepLoginReturn, type UsePermissionReturn, type UseUserReturn, useAdapter, useAuthContext, useAuthInjector, useLogin, useLogout, useMultiStepLogin, usePermission, useSession, useUser, useUserChange, useUserChangeEffect, useUserChangeOn };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { SWRLoginConfig, AuthResponse, AuthInjector, User, TokenAdapter, PluginManager, TokenManager, AuthEventEmitter, AuthStateMachine, BroadcastSync } from '@swr-login/core';
2
+ import { SWRLoginConfig, AuthResponse, AuthInjector, User, UserChangeSource, UserChangeEvent, TokenAdapter, PluginManager, TokenManager, AuthEventEmitter, AuthStateMachine, BroadcastSync } from '@swr-login/core';
3
3
  import React from 'react';
4
4
 
5
5
  interface SWRLoginProviderProps {
@@ -196,6 +196,30 @@ interface UseUserReturn<T extends User = User> {
196
196
  * Call with `null` to clear, or with a user object to update.
197
197
  */
198
198
  mutate: (data?: T | null) => Promise<void>;
199
+ /**
200
+ * Why the most recent `user` transition happened.
201
+ *
202
+ * - `null` — No user-change has been observed yet in this component
203
+ * (e.g. before the first `fetchUser` resolves on mount).
204
+ * - `'initial'` — First time the Provider observed a user value
205
+ * (including `null` for unauthenticated cold start).
206
+ * - `'login'` — Triggered by an explicit `login()` / multi-step finalize
207
+ * / `injectAuth()` call.
208
+ * - `'logout'` — Triggered by an explicit `logout()` / `injectLogout()`.
209
+ * - `'revalidate'` — SWR background revalidation produced a different user.
210
+ * - `'external'` — Cross-tab sync via BroadcastChannel / storage events.
211
+ *
212
+ * Use this to differentiate user-initiated transitions from passive ones,
213
+ * e.g. to suppress a welcome toast on page refresh.
214
+ */
215
+ lastChangeSource: UserChangeSource | null;
216
+ /**
217
+ * Full event object for the most recent `user` transition, including
218
+ * `previousUser` and `timestamp`. `null` before the first transition.
219
+ *
220
+ * See `lastChangeSource` for a quick discriminator.
221
+ */
222
+ lastChangeEvent: UserChangeEvent<T> | null;
199
223
  }
200
224
  /**
201
225
  * Hook to access current user data via SWR cache.
@@ -213,9 +237,112 @@ interface UseUserReturn<T extends User = User> {
213
237
  * if (!isAuthenticated) return <LoginPage />;
214
238
  * return <Dashboard user={user} />;
215
239
  * ```
240
+ *
241
+ * @example Distinguish user-change sources
242
+ * ```tsx
243
+ * const { user, lastChangeSource } = useUser();
244
+ *
245
+ * useEffect(() => {
246
+ * // Only auto-redirect when we detect an *existing* session on mount,
247
+ * // not when the user just pressed the "Login" button (the login form
248
+ * // is responsible for its own redirect there).
249
+ * if (user && lastChangeSource === 'initial') {
250
+ * showRedirectOverlay();
251
+ * }
252
+ * }, [user, lastChangeSource]);
253
+ * ```
216
254
  */
217
255
  declare function useUser<T extends User = User>(): UseUserReturn<T>;
218
256
 
257
+ /**
258
+ * Subscribe to `user-change` events as a **discrete event stream**.
259
+ *
260
+ * Unlike `useUser().user` (which reflects the *current* user value), this
261
+ * hook returns the most recent transition event and re-renders the caller
262
+ * only when a new transition occurs. It's intended for `useEffect`-driven
263
+ * side effects that care about *when* and *why* the user changed rather
264
+ * than the current user value itself.
265
+ *
266
+ * Returns `null` until the first transition is observed by the Provider.
267
+ *
268
+ * @typeParam T - Concrete user type (defaults to base `User`)
269
+ *
270
+ * @example
271
+ * ```tsx
272
+ * const change = useUserChange<MyUser>();
273
+ *
274
+ * useEffect(() => {
275
+ * if (change?.source === 'login') {
276
+ * toast.success(`Welcome back, ${change.user?.name}!`);
277
+ * }
278
+ * }, [change]);
279
+ * ```
280
+ *
281
+ * @example Filter by source
282
+ * ```tsx
283
+ * const change = useUserChange();
284
+ * useEffect(() => {
285
+ * if (change?.source === 'external') {
286
+ * // Another tab just logged in / out — refresh local UI
287
+ * refreshSidebar();
288
+ * }
289
+ * }, [change]);
290
+ * ```
291
+ */
292
+ declare function useUserChange<T extends User = User>(): UserChangeEvent<T> | null;
293
+ /**
294
+ * Register a side-effect callback that fires on every `user-change` event,
295
+ * **without** triggering a re-render of the calling component.
296
+ *
297
+ * Ideal for analytics / logging / imperative side effects that don't need
298
+ * to participate in React's render cycle.
299
+ *
300
+ * The callback receives the full `UserChangeEvent` (including `previousUser`,
301
+ * `timestamp`, and `source`). The listener is automatically unsubscribed on
302
+ * unmount.
303
+ *
304
+ * Note: the callback reference is stored in a ref and always called with
305
+ * the latest closure — you do NOT need to memoize it with `useCallback`.
306
+ *
307
+ * @example Analytics
308
+ * ```tsx
309
+ * useUserChangeEffect((e) => {
310
+ * if (e.source === 'login') {
311
+ * analytics.track('user_login', { userId: e.user?.id });
312
+ * }
313
+ * if (e.source === 'logout') {
314
+ * analytics.track('user_logout', { userId: e.previousUser?.id });
315
+ * }
316
+ * });
317
+ * ```
318
+ *
319
+ * @example Filter sources
320
+ * ```tsx
321
+ * useUserChangeEffect((e) => {
322
+ * // Only react to passive revalidations that flipped the user to null
323
+ * // (e.g. server returned 401 → session expired silently).
324
+ * if (e.source === 'revalidate' && e.user === null && e.previousUser) {
325
+ * showReLoginPrompt();
326
+ * }
327
+ * });
328
+ * ```
329
+ */
330
+ declare function useUserChangeEffect<T extends User = User>(listener: (event: UserChangeEvent<T>) => void): void;
331
+ /**
332
+ * Convenience hook: fires the callback only when the change source matches
333
+ * the given filter. Thin wrapper around {@link useUserChangeEffect}.
334
+ *
335
+ * @example
336
+ * ```tsx
337
+ * // Only run on explicit login (not on cold-start initial load)
338
+ * useUserChangeOn('login', (e) => router.push('/dashboard'));
339
+ *
340
+ * // Subscribe to multiple sources at once
341
+ * useUserChangeOn(['login', 'external'], (e) => refreshSidebar());
342
+ * ```
343
+ */
344
+ declare function useUserChangeOn<T extends User = User>(source: UserChangeSource | UserChangeSource[], listener: (event: UserChangeEvent<T>) => void): void;
345
+
219
346
  interface UseLogoutOptions {
220
347
  /** Specific plugin to call logout on */
221
348
  pluginName?: string;
@@ -410,6 +537,26 @@ interface AuthGuardProps {
410
537
  */
411
538
  declare function AuthGuard({ children, permissions, roles, requireAll, fallback, loadingComponent, }: AuthGuardProps): react_jsx_runtime.JSX.Element;
412
539
 
540
+ /**
541
+ * Mutable hint object shared between the Provider and `useUser()` to describe
542
+ * *why* the next user-change event is expected to happen.
543
+ *
544
+ * The Provider writes into this object as it observes `login` / `logout` /
545
+ * `external` events; `useUser()` reads and clears it when the SWR cache value
546
+ * actually transitions. Unset/expired hint → the change is a passive
547
+ * `revalidate` or the very first `initial` load.
548
+ *
549
+ * Using a mutable ref-like object (instead of React state) avoids re-renders
550
+ * and is safe because it is written synchronously from event callbacks and
551
+ * read synchronously during `useUser()`'s `useEffect`.
552
+ *
553
+ * @internal
554
+ */
555
+ interface UserChangeHint {
556
+ source: UserChangeSource | null;
557
+ /** `Date.now()` when the hint was written. Stale hints (>1s) are ignored. */
558
+ timestamp: number;
559
+ }
413
560
  /** Internal context value passed through SWRLoginProvider */
414
561
  interface AuthContextValue {
415
562
  pluginManager: PluginManager;
@@ -418,6 +565,8 @@ interface AuthContextValue {
418
565
  stateMachine: AuthStateMachine;
419
566
  broadcastSync: BroadcastSync | null;
420
567
  config: SWRLoginConfig;
568
+ /** @internal shared hint for the next user-change source */
569
+ userChangeHint: UserChangeHint;
421
570
  }
422
571
  /**
423
572
  * Internal hook to access auth context.
@@ -426,4 +575,4 @@ interface AuthContextValue {
426
575
  */
427
576
  declare function useAuthContext(): AuthContextValue;
428
577
 
429
- export { AUTH_KEY, type AuthContextValue, AuthGuard, type AuthGuardProps, SWRLoginProvider, type SWRLoginProviderProps, type SessionInfo, type UseAdapterReturn, type UseLoginOptions, type UseLoginReturn, type UseLogoutOptions, type UseLogoutReturn, type UseMultiStepLoginReturn, type UsePermissionReturn, type UseUserReturn, useAdapter, useAuthContext, useAuthInjector, useLogin, useLogout, useMultiStepLogin, usePermission, useSession, useUser };
578
+ export { AUTH_KEY, type AuthContextValue, AuthGuard, type AuthGuardProps, SWRLoginProvider, type SWRLoginProviderProps, type SessionInfo, type UseAdapterReturn, type UseLoginOptions, type UseLoginReturn, type UseLogoutOptions, type UseLogoutReturn, type UseMultiStepLoginReturn, type UsePermissionReturn, type UseUserReturn, useAdapter, useAuthContext, useAuthInjector, useLogin, useLogout, useMultiStepLogin, usePermission, useSession, useUser, useUserChange, useUserChangeEffect, useUserChangeOn };
package/dist/index.js CHANGED
@@ -24,6 +24,13 @@ function SWRLoginProvider({ config, children }) {
24
24
  pluginManager.register(...config.plugins);
25
25
  const enableSync = config.security?.enableBroadcastSync !== false;
26
26
  const broadcastSync = enableSync && typeof window !== "undefined" ? new BroadcastSync() : null;
27
+ const userChangeHint = { source: null, timestamp: 0 };
28
+ const markHint = (source) => {
29
+ userChangeHint.source = source;
30
+ userChangeHint.timestamp = Date.now();
31
+ };
32
+ emitter.on("login", () => markHint("login"));
33
+ emitter.on("logout", () => markHint("logout"));
27
34
  if (config.onLogin) {
28
35
  emitter.on("login", ({ user }) => config.onLogin?.(user));
29
36
  }
@@ -39,7 +46,8 @@ function SWRLoginProvider({ config, children }) {
39
46
  emitter,
40
47
  stateMachine,
41
48
  broadcastSync,
42
- config
49
+ config,
50
+ userChangeHint
43
51
  };
44
52
  }, [config]);
45
53
  useEffect(() => {
@@ -67,15 +75,22 @@ function SWRLoginProvider({ config, children }) {
67
75
  emitter.emit("token-expired", void 0);
68
76
  }
69
77
  if (broadcastSync) {
78
+ const { userChangeHint } = contextValue;
79
+ const markExternal = () => {
80
+ userChangeHint.source = "external";
81
+ userChangeHint.timestamp = Date.now();
82
+ };
70
83
  const unsubscribe = broadcastSync.onMessage((message) => {
71
84
  switch (message.type) {
72
85
  case "LOGOUT":
73
86
  tokenManager.clearTokens();
74
87
  stateMachine.transition("unauthenticated");
75
88
  emitter.emit("logout", void 0);
89
+ markExternal();
76
90
  break;
77
91
  case "LOGIN":
78
92
  case "TOKEN_REFRESH":
93
+ markExternal();
79
94
  if (cfg.cacheAdapter) {
80
95
  cfg.cacheAdapter.revalidate();
81
96
  }
@@ -116,8 +131,9 @@ function SWRLoginProvider({ config, children }) {
116
131
  return /* @__PURE__ */ jsx(AuthContext.Provider, { value: contextValue, children });
117
132
  }
118
133
  var AUTH_KEY = "__swr_login_user__";
134
+ var USER_CHANGE_HINT_TTL_MS = 1e3;
119
135
  function useUser() {
120
- const { tokenManager, stateMachine, config } = useAuthContext();
136
+ const { tokenManager, stateMachine, config, emitter, userChangeHint } = useAuthContext();
121
137
  const lastErrorRef = useRef(void 0);
122
138
  const retryCountRef = useRef(0);
123
139
  const [, setTick] = useState(0);
@@ -126,6 +142,8 @@ function useUser() {
126
142
  lastErrorRef.current = void 0;
127
143
  forceUpdate();
128
144
  }, [forceUpdate]);
145
+ const previousUserRef = useRef(void 0);
146
+ const [lastChangeEvent, setLastChangeEvent] = useState(null);
129
147
  const fetcher = async () => {
130
148
  const token = tokenManager.getAccessToken();
131
149
  if (!token) return null;
@@ -150,10 +168,41 @@ function useUser() {
150
168
  isLoading,
151
169
  mutate: swrMutate
152
170
  } = useSWR(AUTH_KEY, fetcher, {
153
- revalidateOnFocus: true,
154
- revalidateOnReconnect: true,
155
- shouldRetryOnError: false
171
+ revalidateOnFocus: config.swrOptions?.revalidateOnFocus ?? true,
172
+ revalidateOnReconnect: config.swrOptions?.revalidateOnReconnect ?? true,
173
+ shouldRetryOnError: false,
174
+ ...config.swrOptions?.dedupingInterval !== void 0 && {
175
+ dedupingInterval: config.swrOptions.dedupingInterval
176
+ },
177
+ ...config.swrOptions?.focusThrottleInterval !== void 0 && {
178
+ focusThrottleInterval: config.swrOptions.focusThrottleInterval
179
+ },
180
+ ...config.swrOptions?.refreshInterval !== void 0 && {
181
+ refreshInterval: config.swrOptions.refreshInterval
182
+ }
156
183
  });
184
+ useEffect(() => {
185
+ if (data === void 0) return;
186
+ const prev = previousUserRef.current;
187
+ if (Object.is(prev, data)) return;
188
+ let source;
189
+ if (prev === void 0) {
190
+ source = "initial";
191
+ } else {
192
+ const now = Date.now();
193
+ const hintFresh = userChangeHint.source !== null && now - userChangeHint.timestamp <= USER_CHANGE_HINT_TTL_MS;
194
+ source = hintFresh ? userChangeHint.source : "revalidate";
195
+ }
196
+ const event = {
197
+ source,
198
+ user: data,
199
+ previousUser: prev,
200
+ timestamp: Date.now()
201
+ };
202
+ previousUserRef.current = data;
203
+ setLastChangeEvent(event);
204
+ emitter.emit("user-change", event);
205
+ }, [data, emitter, userChangeHint]);
157
206
  useEffect(() => {
158
207
  if (error) {
159
208
  lastErrorRef.current = error;
@@ -190,7 +239,9 @@ function useUser() {
190
239
  error,
191
240
  lastError: lastErrorRef.current,
192
241
  clearError,
193
- mutate: mutate2
242
+ mutate: mutate2,
243
+ lastChangeSource: lastChangeEvent?.source ?? null,
244
+ lastChangeEvent
194
245
  };
195
246
  }
196
247
 
@@ -403,6 +454,40 @@ function useAuthInjector() {
403
454
  }, [tokenManager, emitter, stateMachine, config]);
404
455
  return { injectAuth, injectLogout };
405
456
  }
457
+ function useUserChange() {
458
+ const { emitter } = useAuthContext();
459
+ const [event, setEvent] = useState(null);
460
+ useEffect(() => {
461
+ const unsubscribe = emitter.on("user-change", (payload) => {
462
+ setEvent(payload);
463
+ });
464
+ return unsubscribe;
465
+ }, [emitter]);
466
+ return event;
467
+ }
468
+ function useUserChangeEffect(listener) {
469
+ const { emitter } = useAuthContext();
470
+ const listenerRef = useRef(listener);
471
+ listenerRef.current = listener;
472
+ useEffect(() => {
473
+ const unsubscribe = emitter.on("user-change", (payload) => {
474
+ try {
475
+ listenerRef.current(payload);
476
+ } catch (err) {
477
+ console.error("[swr-login] Error in useUserChangeEffect listener:", err);
478
+ }
479
+ });
480
+ return unsubscribe;
481
+ }, [emitter]);
482
+ }
483
+ function useUserChangeOn(source, listener) {
484
+ const sources = Array.isArray(source) ? source : [source];
485
+ useUserChangeEffect((event) => {
486
+ if (sources.includes(event.source)) {
487
+ listener(event);
488
+ }
489
+ });
490
+ }
406
491
  function useLogout() {
407
492
  const { pluginManager, stateMachine, broadcastSync } = useAuthContext();
408
493
  const [isLoading, setIsLoading] = useState(false);
@@ -537,6 +622,6 @@ function AuthGuard({
537
622
  return /* @__PURE__ */ jsx(Fragment, { children });
538
623
  }
539
624
 
540
- export { AUTH_KEY, AuthGuard, SWRLoginProvider, useAdapter, useAuthContext, useAuthInjector, useLogin, useLogout, useMultiStepLogin, usePermission, useSession, useUser };
625
+ export { AUTH_KEY, AuthGuard, SWRLoginProvider, useAdapter, useAuthContext, useAuthInjector, useLogin, useLogout, useMultiStepLogin, usePermission, useSession, useUser, useUserChange, useUserChangeEffect, useUserChangeOn };
541
626
  //# sourceMappingURL=index.js.map
542
627
  //# sourceMappingURL=index.js.map