antaeus.keycloak.react 2.4.0 → 2.5.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.mjs CHANGED
@@ -149,6 +149,14 @@ var useAuth = useAuth$1;
149
149
 
150
150
  // src/hooks/useTokenLifecycle.ts
151
151
  var STORAGE_KEY_PREFIX = "keycloak_first_login_";
152
+ var isNavigatorOffline = () => typeof navigator !== "undefined" && "onLine" in navigator && navigator.onLine === false;
153
+ var isTransientNetworkError = (error) => {
154
+ const message = error.message?.toLowerCase?.() ?? "";
155
+ if (!message) {
156
+ return false;
157
+ }
158
+ return message.includes("network") || message.includes("failed to fetch") || message.includes("fetch") || message.includes("timeout") || message.includes("temporarily unavailable") || message.includes("dns") || message.includes("offline");
159
+ };
152
160
  function useTokenLifecycle(options) {
153
161
  const {
154
162
  config,
@@ -163,6 +171,11 @@ function useTokenLifecycle(options) {
163
171
  const [isRefreshTokenExpired, setIsRefreshTokenExpired] = useState(false);
164
172
  const hasCheckedOnStartup = useRef(false);
165
173
  const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
174
+ const [pendingRetry, setPendingRetry] = useState(null);
175
+ const deferredRefreshReasonRef = useRef(null);
176
+ const enqueueRetry = useCallback((reason) => {
177
+ setPendingRetry((current) => current === reason ? current : reason);
178
+ }, []);
166
179
  const log = useCallback(
167
180
  (message, ...args) => {
168
181
  if (debug) {
@@ -213,6 +226,7 @@ function useTokenLifecycle(options) {
213
226
  return;
214
227
  }
215
228
  setIsRefreshing(true);
229
+ deferredRefreshReasonRef.current = null;
216
230
  try {
217
231
  if (checkRefreshTokenLifetime()) {
218
232
  log("Refresh token exceeded max lifetime, cannot refresh");
@@ -220,6 +234,12 @@ function useTokenLifecycle(options) {
220
234
  onRefreshTokenExpired?.();
221
235
  return;
222
236
  }
237
+ if (isNavigatorOffline()) {
238
+ log("Device offline, deferring token refresh until connection is restored");
239
+ deferredRefreshReasonRef.current = "offline";
240
+ enqueueRetry("offline");
241
+ return;
242
+ }
223
243
  onRefreshStart?.();
224
244
  log("Starting token refresh");
225
245
  await auth.signinSilent();
@@ -227,6 +247,21 @@ function useTokenLifecycle(options) {
227
247
  onRefreshSuccess?.();
228
248
  } catch (error) {
229
249
  const err = error instanceof Error ? error : new Error(String(error));
250
+ if (isTransientNetworkError(err)) {
251
+ const reason = isNavigatorOffline() ? "offline" : "network";
252
+ deferredRefreshReasonRef.current = reason;
253
+ if (reason === "offline") {
254
+ log("Token refresh deferred because device is offline");
255
+ } else {
256
+ log(
257
+ "Token refresh failed due to transient network issue, will retry automatically:",
258
+ err.message
259
+ );
260
+ }
261
+ enqueueRetry(reason);
262
+ onRefreshError?.(err);
263
+ return;
264
+ }
230
265
  log("Token refresh failed:", err.message);
231
266
  onRefreshError?.(err);
232
267
  throw err;
@@ -241,6 +276,7 @@ function useTokenLifecycle(options) {
241
276
  onRefreshSuccess,
242
277
  onRefreshError,
243
278
  onRefreshTokenExpired,
279
+ enqueueRetry,
244
280
  log
245
281
  ]);
246
282
  const checkAndRefreshTokens = useCallback(async () => {
@@ -279,6 +315,14 @@ function useTokenLifecycle(options) {
279
315
  if (timeUntilExpiry <= threshold) {
280
316
  try {
281
317
  await refreshToken();
318
+ if (deferredRefreshReasonRef.current) {
319
+ log(
320
+ "Token refresh was deferred due to pending retry:",
321
+ deferredRefreshReasonRef.current
322
+ );
323
+ deferredRefreshReasonRef.current = null;
324
+ return false;
325
+ }
282
326
  return true;
283
327
  } catch (error) {
284
328
  log("Failed to refresh tokens:", error);
@@ -296,6 +340,55 @@ function useTokenLifecycle(options) {
296
340
  getStorageKey,
297
341
  log
298
342
  ]);
343
+ useEffect(() => {
344
+ if (pendingRetry === null) {
345
+ return;
346
+ }
347
+ if (typeof window === "undefined") {
348
+ return;
349
+ }
350
+ let cancelled = false;
351
+ const executeRetry = () => {
352
+ if (cancelled) {
353
+ return;
354
+ }
355
+ if (isRefreshing) {
356
+ log("Skipping scheduled token refresh retry because a refresh is already in progress");
357
+ return;
358
+ }
359
+ refreshToken().catch((err) => {
360
+ log("Scheduled token refresh retry failed:", err);
361
+ });
362
+ };
363
+ if (pendingRetry === "offline") {
364
+ log("Waiting for online event to retry token refresh");
365
+ const handleOnline = () => {
366
+ if (cancelled) {
367
+ return;
368
+ }
369
+ log("Online event detected, retrying deferred token refresh");
370
+ setPendingRetry(null);
371
+ executeRetry();
372
+ };
373
+ window.addEventListener("online", handleOnline, { once: true });
374
+ return () => {
375
+ cancelled = true;
376
+ window.removeEventListener("online", handleOnline);
377
+ };
378
+ }
379
+ const timeoutId = window.setTimeout(() => {
380
+ if (cancelled) {
381
+ return;
382
+ }
383
+ log("Retrying token refresh after transient network failure delay");
384
+ setPendingRetry(null);
385
+ executeRetry();
386
+ }, 5e3);
387
+ return () => {
388
+ cancelled = true;
389
+ window.clearTimeout(timeoutId);
390
+ };
391
+ }, [pendingRetry, refreshToken, isRefreshing, log]);
299
392
  useEffect(() => {
300
393
  const strategy = config.silentRenew?.refreshStrategy || "both";
301
394
  const shouldCheckOnStartup = strategy === "startup" || strategy === "both";