antaeus.keycloak.react 2.6.2 → 2.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,6 +30,7 @@ That's it! You now have full authentication with automatic token refresh.
30
30
  - Zero-config setup
31
31
  - Automatic token lifecycle management
32
32
  - Token refresh on app startup
33
+ - Token refresh when suspended mobile/PWA sessions resume
33
34
  - Silent token renewal
34
35
  - Session monitoring UI
35
36
  - Protected routes
@@ -152,6 +153,8 @@ const { isEnabled, isConfigured, reason } = useKeycloakStatus();
152
153
  - **on-demand**: Only when API returns 401
153
154
  - **both** (recommended): Startup + 401 retry
154
155
 
156
+ For mobile browsers and installed PWAs, token freshness is also checked when the app resumes from the background by default. Set `silentRenew.refreshOnResume` to `false` to disable this behavior.
157
+
155
158
  ## React Hooks
156
159
 
157
160
  ### useAuth()
package/dist/index.d.mts CHANGED
@@ -121,6 +121,12 @@ interface SilentRenewConfig {
121
121
  * @default true
122
122
  */
123
123
  useRefreshToken?: boolean;
124
+ /**
125
+ * Re-check token freshness when a suspended tab/PWA resumes.
126
+ * Useful for mobile browsers and installed PWAs where timers pause in the background.
127
+ * @default true
128
+ */
129
+ refreshOnResume?: boolean;
124
130
  }
125
131
  /**
126
132
  * Event callbacks for authentication lifecycle
package/dist/index.d.ts CHANGED
@@ -121,6 +121,12 @@ interface SilentRenewConfig {
121
121
  * @default true
122
122
  */
123
123
  useRefreshToken?: boolean;
124
+ /**
125
+ * Re-check token freshness when a suspended tab/PWA resumes.
126
+ * Useful for mobile browsers and installed PWAs where timers pause in the background.
127
+ * @default true
128
+ */
129
+ refreshOnResume?: boolean;
124
130
  }
125
131
  /**
126
132
  * Event callbacks for authentication lifecycle
package/dist/index.js CHANGED
@@ -36,7 +36,8 @@ var defaultConfig = {
36
36
  // 30 days in seconds
37
37
  showLoadingIndicator: false,
38
38
  loadingMessage: "Refreshing session...",
39
- useRefreshToken: true
39
+ useRefreshToken: true,
40
+ refreshOnResume: true
40
41
  },
41
42
  advanced: {
42
43
  responseType: "code",
@@ -191,6 +192,7 @@ function useTokenLifecycle(options) {
191
192
  const auth = useAuth();
192
193
  const [isRefreshing, setIsRefreshing] = react.useState(false);
193
194
  const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
195
+ const isRefreshingRef = react.useRef(false);
194
196
  const hasCheckedOnStartup = react.useRef(false);
195
197
  const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
196
198
  const [pendingRetry, setPendingRetry] = react.useState(null);
@@ -243,10 +245,11 @@ function useTokenLifecycle(options) {
243
245
  return elapsedSeconds >= maxLifetime;
244
246
  }, [config.silentRenew?.maxRefreshTokenLifetime, getStorageKey, log, storeFirstLoginTime]);
245
247
  const refreshToken = react.useCallback(async () => {
246
- if (isRefreshing) {
248
+ if (isRefreshingRef.current) {
247
249
  log("Refresh already in progress, skipping");
248
250
  return;
249
251
  }
252
+ isRefreshingRef.current = true;
250
253
  setIsRefreshing(true);
251
254
  deferredRefreshReasonRef.current = null;
252
255
  try {
@@ -302,10 +305,10 @@ function useTokenLifecycle(options) {
302
305
  onRefreshError?.(err);
303
306
  throw err;
304
307
  } finally {
308
+ isRefreshingRef.current = false;
305
309
  setIsRefreshing(false);
306
310
  }
307
311
  }, [
308
- isRefreshing,
309
312
  auth,
310
313
  checkRefreshTokenLifetime,
311
314
  onRefreshStart,
@@ -316,6 +319,38 @@ function useTokenLifecycle(options) {
316
319
  getStorageKey,
317
320
  log
318
321
  ]);
322
+ const shouldRefreshOnResume = react.useCallback((reason) => {
323
+ const silentRenewEnabled = config.silentRenew?.enabled !== false;
324
+ const refreshOnResume = config.silentRenew?.refreshOnResume !== false;
325
+ const strategy = config.silentRenew?.refreshStrategy || "both";
326
+ const strategyAllowsResume = strategy === "startup" || strategy === "both";
327
+ const hasRefreshToken = Boolean(auth.user?.refresh_token);
328
+ if (!silentRenewEnabled || !refreshOnResume || !strategyAllowsResume) {
329
+ return false;
330
+ }
331
+ if (auth.isLoading) {
332
+ return false;
333
+ }
334
+ if (!auth.user) {
335
+ return false;
336
+ }
337
+ if (supportsRefreshTokens && !hasRefreshToken) {
338
+ log("Skipping resume token check because no refresh token is available:", reason);
339
+ return false;
340
+ }
341
+ if (reason !== "online" && typeof document !== "undefined" && document.visibilityState === "hidden") {
342
+ return false;
343
+ }
344
+ return true;
345
+ }, [
346
+ auth.isLoading,
347
+ auth.user,
348
+ config.silentRenew?.enabled,
349
+ config.silentRenew?.refreshOnResume,
350
+ config.silentRenew?.refreshStrategy,
351
+ supportsRefreshTokens,
352
+ log
353
+ ]);
319
354
  const checkAndRefreshTokens = react.useCallback(async () => {
320
355
  if (!auth.user) {
321
356
  log("No user session, skipping token check");
@@ -463,6 +498,38 @@ function useTokenLifecycle(options) {
463
498
  checkAndRefreshTokens,
464
499
  log
465
500
  ]);
501
+ react.useEffect(() => {
502
+ if (typeof window === "undefined" || typeof document === "undefined") {
503
+ return;
504
+ }
505
+ const runResumeCheck = (reason) => {
506
+ if (!shouldRefreshOnResume(reason)) {
507
+ return;
508
+ }
509
+ log("Running resume token check after:", reason);
510
+ checkAndRefreshTokens().catch((error) => {
511
+ log("Resume token check failed:", error);
512
+ });
513
+ };
514
+ const handleVisibilityChange = () => {
515
+ if (document.visibilityState === "visible") {
516
+ runResumeCheck("visibilitychange");
517
+ }
518
+ };
519
+ const handlePageShow = () => runResumeCheck("pageshow");
520
+ const handleFocus = () => runResumeCheck("focus");
521
+ const handleOnline = () => runResumeCheck("online");
522
+ document.addEventListener("visibilitychange", handleVisibilityChange);
523
+ window.addEventListener("pageshow", handlePageShow);
524
+ window.addEventListener("focus", handleFocus);
525
+ window.addEventListener("online", handleOnline);
526
+ return () => {
527
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
528
+ window.removeEventListener("pageshow", handlePageShow);
529
+ window.removeEventListener("focus", handleFocus);
530
+ window.removeEventListener("online", handleOnline);
531
+ };
532
+ }, [checkAndRefreshTokens, log, shouldRefreshOnResume]);
466
533
  react.useEffect(() => {
467
534
  if (hasCheckedOnStartup.current) {
468
535
  const hasUser = Boolean(auth.user);
@@ -885,7 +952,8 @@ var CONFIG_PRESETS = {
885
952
  maxRefreshTokenLifetime: 2592e3,
886
953
  // 30 days
887
954
  showLoadingIndicator: false,
888
- loadingMessage: "Refreshing session..."
955
+ loadingMessage: "Refreshing session...",
956
+ refreshOnResume: true
889
957
  },
890
958
  advanced: {
891
959
  responseType: "code",
@@ -921,7 +989,8 @@ var CONFIG_PRESETS = {
921
989
  // 30 days
922
990
  showLoadingIndicator: true,
923
991
  // Show loading for better UX
924
- loadingMessage: "Restoring your session..."
992
+ loadingMessage: "Restoring your session...",
993
+ refreshOnResume: true
925
994
  },
926
995
  advanced: {
927
996
  responseType: "code",
@@ -958,7 +1027,8 @@ var CONFIG_PRESETS = {
958
1027
  // 24 hours only
959
1028
  showLoadingIndicator: false,
960
1029
  // Silent for security
961
- loadingMessage: "Validating session..."
1030
+ loadingMessage: "Validating session...",
1031
+ refreshOnResume: true
962
1032
  },
963
1033
  advanced: {
964
1034
  responseType: "code",
@@ -992,7 +1062,8 @@ var CONFIG_PRESETS = {
992
1062
  maxRefreshTokenLifetime: 604800,
993
1063
  // 7 days
994
1064
  showLoadingIndicator: false,
995
- loadingMessage: "Refreshing session..."
1065
+ loadingMessage: "Refreshing session...",
1066
+ refreshOnResume: true
996
1067
  },
997
1068
  advanced: {
998
1069
  responseType: "code",
@@ -1029,7 +1100,8 @@ var CONFIG_PRESETS = {
1029
1100
  // No limit (for convenience)
1030
1101
  showLoadingIndicator: true,
1031
1102
  // Show for debugging
1032
- loadingMessage: "[DEV] Refreshing tokens..."
1103
+ loadingMessage: "[DEV] Refreshing tokens...",
1104
+ refreshOnResume: true
1033
1105
  },
1034
1106
  advanced: {
1035
1107
  responseType: "code",