react-session.manager.sk 2.0.0 → 2.0.2

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/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import React, { createContext, useState, useEffect, useCallback } from "react";
2
2
  import versionCompare from "./components/versionCompare";
3
3
  import getDeviceFingerprint from "./components/FingerPrint";
4
+ import ErrorBoundary from "./components/ErrorBoundary";
5
+ import ThemeWrapper from "./components/ThemeWrapper";
4
6
  // Notifications
5
7
  import { ToastContainer, toast } from "react-toastify";
6
8
  import "react-toastify/dist/ReactToastify.css";
@@ -40,9 +42,26 @@ const SessionManagerProvider = ({
40
42
 
41
43
  useEffect(() => {
42
44
  if (!deviceUID) {
43
- const uid = getDeviceFingerprint();
44
- setDeviceUID(uid);
45
- AuthenticatedAxiosObject.defaults.headers.common["deviceUID"] = uid;
45
+ try {
46
+ const uid = getDeviceFingerprint();
47
+ if (uid) {
48
+ setDeviceUID(uid);
49
+ localStorage.setItem("deviceUID", uid);
50
+ AuthenticatedAxiosObject.defaults.headers.common["deviceUID"] = uid;
51
+ }
52
+ } catch (err) {
53
+ console.error("Failed to generate device fingerprint:", err);
54
+ // Generate a fallback UUID
55
+ const fallbackUID =
56
+ "fallback-" +
57
+ Date.now() +
58
+ "-" +
59
+ Math.random().toString(36).substr(2, 9);
60
+ setDeviceUID(fallbackUID);
61
+ localStorage.setItem("deviceUID", fallbackUID);
62
+ AuthenticatedAxiosObject.defaults.headers.common["deviceUID"] =
63
+ fallbackUID;
64
+ }
46
65
  }
47
66
  }, [deviceUID, AuthenticatedAxiosObject]);
48
67
 
@@ -60,7 +79,7 @@ const SessionManagerProvider = ({
60
79
  setTimeout(() => {
61
80
  refreshToken()
62
81
  .then((data) => {
63
- if (data.refreshed) {
82
+ if (data?.refreshed) {
64
83
  const token = `Bearer ${data.access_token}`;
65
84
  setCurrent(token);
66
85
  AuthenticatedAxiosObject.defaults.headers.common[
@@ -71,10 +90,35 @@ const SessionManagerProvider = ({
71
90
  }
72
91
  })
73
92
  .catch((err) => {
74
- console.log(err);
75
- console.log("Missing/Invalid Token");
93
+ console.error("Token refresh failed:", err);
94
+
95
+ // Track auth failures
96
+ if (err?.code === "ERR_NETWORK" || err?.code === "ERR_CONNECTION_REFUSED") {
97
+ setAuthError(true);
98
+ }
99
+
100
+ // Clear invalid tokens
76
101
  localStorage.removeItem("Authorization");
77
102
  sessionStorage.removeItem("Authorization");
103
+ setCurrent("");
104
+ setCurrentLoggedIn(false);
105
+
106
+ // Only show toast for actual network/server errors, not auth failures
107
+ if (err?.code === "ERR_NETWORK" || err?.response?.status >= 500) {
108
+ // Don't spam with toasts during retries
109
+ const lastToast = sessionStorage.getItem("lastRefreshErrorToast");
110
+ const now = Date.now();
111
+ if (!lastToast || now - parseInt(lastToast) > 30000) { // 30 seconds
112
+ toast.error(
113
+ "Unable to refresh your session. Please login again.",
114
+ {
115
+ toastId: "token-refresh-error",
116
+ icon: <Logout />,
117
+ }
118
+ );
119
+ sessionStorage.setItem("lastRefreshErrorToast", now.toString());
120
+ }
121
+ }
78
122
  });
79
123
  }, 100);
80
124
  },
@@ -87,8 +131,8 @@ const SessionManagerProvider = ({
87
131
 
88
132
  if (local_auth) fromPrevious(local_auth, true);
89
133
  else if (sess_auth) fromPrevious(sess_auth, false);
90
- return () => clearInterval(fromPrevious);
91
- }, [current, fromPrevious]);
134
+ // No cleanup needed here since fromPrevious doesn't create intervals
135
+ }, [fromPrevious]);
92
136
 
93
137
  // Wrap setHeader to store new header names in localStorage
94
138
  const setHeader = (header) => {
@@ -99,18 +143,42 @@ const SessionManagerProvider = ({
99
143
  const [isAdmin, setIsAdmin] = useState(false);
100
144
  const [userInfo, setUserInfo] = useState({});
101
145
  const [loadingUser, setLoadingUser] = useState(true);
146
+ const [authError, setAuthError] = useState(false);
102
147
 
103
148
  useEffect(() => {
104
149
  setTimeout(() => {
105
150
  userLoader()
106
151
  .then((res) => {
107
- const data = res.data;
108
- setCurrentLoggedIn(data.logged_in);
109
- setIsAdmin(data.is_admin);
110
- setUserInfo(data.Info);
152
+ const data = res?.data;
153
+ if (data) {
154
+ setCurrentLoggedIn(data.logged_in);
155
+ setIsAdmin(data.is_admin);
156
+ setUserInfo(data.Info || {});
157
+ setAuthError(false);
158
+ }
111
159
  })
112
160
  .catch((err) => {
113
- console.log(err);
161
+ console.error("User data loading failed:", err);
162
+
163
+ // Check if this is a persistent auth error
164
+ if (err?.code === "ERR_NETWORK" || err?.code === "ERR_CONNECTION_REFUSED") {
165
+ setAuthError(true);
166
+ // Don't show toast immediately, let retries happen first
167
+ } else if (err?.response?.status !== 401 && err?.response?.status !== 403) {
168
+ if (err?.response?.status >= 500) {
169
+ toast.error(
170
+ "Unable to load user information. Some features may be limited.",
171
+ {
172
+ toastId: "user-load-error",
173
+ icon: <GppBad />,
174
+ }
175
+ );
176
+ }
177
+ }
178
+ // Set safe defaults
179
+ setCurrentLoggedIn(false);
180
+ setIsAdmin(false);
181
+ setUserInfo({});
114
182
  })
115
183
  .finally(() => {
116
184
  setLoadingUser(false);
@@ -138,18 +206,23 @@ const SessionManagerProvider = ({
138
206
  clearInterval(interval);
139
207
  }
140
208
  return () => clearInterval(interval);
141
- }, [current, fromPrevious, currentLoggin, refreshTimer]);
209
+ }, [fromPrevious, currentLoggin, refreshTimer]);
142
210
 
143
211
  useEffect(() => {
144
212
  const customInterceptor = () => {
145
213
  AuthenticatedAxiosObject.interceptors.response.use(
146
214
  (response) => {
215
+ // Reset connection failure count on successful response
216
+ sessionStorage.setItem("connectionFailures", "0");
147
217
  return response;
148
218
  },
149
219
  (error) => {
220
+ console.error("Axios interceptor caught error:", error);
221
+
222
+ // Handle session expiry (455)
150
223
  if (error?.response?.status === 455) {
151
224
  try {
152
- const status = error.response.data.logged_in || false;
225
+ const status = error.response.data?.logged_in || false;
153
226
  if (!status) {
154
227
  console.log("Session ended");
155
228
  setCurrentLoggedIn(false);
@@ -157,50 +230,91 @@ const SessionManagerProvider = ({
157
230
  "Authorization"
158
231
  ] = ``;
159
232
  toast.info(
160
- "Your session in no longer valid, please login again.",
233
+ "Your session is no longer valid, please login again.",
161
234
  { toastId: "Forced_log_out", icon: <Logout /> }
162
235
  );
163
236
  }
164
237
  } catch (err) {
165
- throw err;
238
+ console.error("Error handling session expiry:", err);
239
+ // Don't throw here - just log and continue
166
240
  }
241
+ return Promise.reject(error);
167
242
  }
243
+
244
+ // Handle version mismatch (426)
168
245
  if (error?.response?.status === 426) {
169
- sessionStorage.setItem("appVersionOld", true);
170
- sessionStorage.setItem(
171
- "requiredVersion",
172
- error.response.data.minVersion
173
- );
174
- let reloads = sessionStorage.getItem("appReloads") || 0;
175
- if (reloads < 2) {
176
- sessionStorage.setItem("appReloads", reloads + 1);
177
- setTimeout(() => {
178
- window.location.reload();
179
- }, 1000 * reloads);
180
- } else {
181
- toast.warning(
182
- "The application needs to be updated please wait for some time then reload the page.",
183
- {
184
- toastId: "appReloadError",
185
- icon: <Update />,
186
- }
246
+ try {
247
+ sessionStorage.setItem("appVersionOld", true);
248
+ sessionStorage.setItem(
249
+ "requiredVersion",
250
+ error.response.data?.minVersion || "unknown"
187
251
  );
252
+ let reloads = parseInt(
253
+ sessionStorage.getItem("appReloads") || "0"
254
+ );
255
+ if (reloads < 2) {
256
+ sessionStorage.setItem("appReloads", (reloads + 1).toString());
257
+ setTimeout(() => {
258
+ window.location.reload();
259
+ }, 1000 * reloads);
260
+ } else {
261
+ toast.warning(
262
+ "The application needs to be updated. Please wait for some time then reload the page.",
263
+ {
264
+ toastId: "appReloadError",
265
+ icon: <Update />,
266
+ }
267
+ );
268
+ }
269
+ } catch (err) {
270
+ console.error("Error handling version mismatch:", err);
188
271
  }
272
+ return Promise.reject(error);
189
273
  }
190
274
 
275
+ // Handle network errors and connection issues
191
276
  if (!error?.response?.status) {
192
- if (error?.code !== "ERR_CANCELED" && error?.message !== "canceled")
193
- toast.error(
194
- "The server is not responding, please reload or try again later.",
195
- { toastId: "ERR_CONNECTION_REFUSED", icon: <GppBad /> }
196
- );
277
+ if (
278
+ error?.code !== "ERR_CANCELED" &&
279
+ error?.message !== "canceled"
280
+ ) {
281
+ const errorMessage =
282
+ error?.code === "ERR_NETWORK"
283
+ ? "Unable to connect to the server. Please check your internet connection and try again."
284
+ : "The server is not responding, please reload or try again later.";
285
+
286
+ // Only show toast after multiple failures to avoid spam
287
+ const failureCount = parseInt(sessionStorage.getItem("connectionFailures") || "0");
288
+ sessionStorage.setItem("connectionFailures", (failureCount + 1).toString());
289
+
290
+ if (failureCount >= 3) {
291
+ toast.error(errorMessage, {
292
+ toastId: `${error?.code || "ERR_CONNECTION_REFUSED"}`,
293
+ icon: <GppBad />,
294
+ });
295
+ // Reset counter after showing toast
296
+ sessionStorage.setItem("connectionFailures", "0");
297
+ }
298
+ }
299
+ return Promise.reject(error);
197
300
  }
198
- throw error;
301
+
302
+ // For all other errors, just reject without throwing
303
+ return Promise.reject(error);
199
304
  }
200
305
  );
201
306
  };
202
- return customInterceptor;
203
- }, [AuthenticatedAxiosObject, currentLoggin]);
307
+
308
+ // Call the interceptor setup
309
+ customInterceptor();
310
+
311
+ // Cleanup function to remove interceptors
312
+ return () => {
313
+ if (AuthenticatedAxiosObject?.interceptors?.response) {
314
+ AuthenticatedAxiosObject.interceptors.response.clear();
315
+ }
316
+ };
317
+ }, [AuthenticatedAxiosObject, currentLoggin, setCurrentLoggedIn]);
204
318
 
205
319
  // We will use the below to refresh our data about the user when ever we flag refreshData as true
206
320
  const [refreshData, setRefreshFlag] = useState(false);
@@ -217,11 +331,20 @@ const SessionManagerProvider = ({
217
331
 
218
332
  useEffect(() => {
219
333
  if (refreshData) {
220
- userLoader().then((res) => {
221
- const data = res.data;
222
- setUserInfo(data.Info);
223
- setRefreshData(false);
224
- });
334
+ userLoader()
335
+ .then((res) => {
336
+ const data = res?.data;
337
+ if (data) {
338
+ setUserInfo(data.Info || {});
339
+ }
340
+ })
341
+ .catch((err) => {
342
+ console.error("User data refresh failed:", err);
343
+ // Silently fail for data refresh - don't overwhelm user with toasts
344
+ })
345
+ .finally(() => {
346
+ setRefreshData(false);
347
+ });
225
348
  }
226
349
  }, [refreshData, userLoader]);
227
350
 
@@ -230,10 +353,21 @@ const SessionManagerProvider = ({
230
353
  };
231
354
 
232
355
  // Check if user has specific role
233
-
234
356
  const hasRole = (roles) => {
235
- const found = roles.some((r) => userInfo?.roles?.indexOf(r) >= 0);
236
- return found;
357
+ if (
358
+ !roles ||
359
+ !Array.isArray(roles) ||
360
+ !userInfo?.roles ||
361
+ !Array.isArray(userInfo.roles)
362
+ ) {
363
+ return false;
364
+ }
365
+ try {
366
+ return roles.some((r) => userInfo.roles.indexOf(r) >= 0);
367
+ } catch (err) {
368
+ console.error("Error checking user roles:", err);
369
+ return false;
370
+ }
237
371
  };
238
372
 
239
373
  const contextValue = {
@@ -248,46 +382,234 @@ const SessionManagerProvider = ({
248
382
  hasRole: hasRole,
249
383
  deviceUID: deviceUID,
250
384
  loadingUser: loadingUser,
385
+ authError: authError,
251
386
  };
252
387
 
388
+ // Show loading state while initializing
389
+ if (loadingUser) {
390
+ return (
391
+ <ThemeWrapper>
392
+ <ErrorBoundary>
393
+ <SessionManager.Provider value={contextValue}>
394
+ <div style={{
395
+ display: 'flex',
396
+ flexDirection: 'column',
397
+ alignItems: 'center',
398
+ justifyContent: 'center',
399
+ minHeight: '100vh',
400
+ padding: '20px',
401
+ backgroundColor: '#f5f5f5',
402
+ fontFamily: 'Arial, sans-serif'
403
+ }}>
404
+ <div style={{
405
+ backgroundColor: 'white',
406
+ padding: '40px',
407
+ borderRadius: '8px',
408
+ boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
409
+ textAlign: 'center',
410
+ maxWidth: '400px'
411
+ }}>
412
+ <div style={{
413
+ width: '40px',
414
+ height: '40px',
415
+ border: '4px solid #f3f3f3',
416
+ borderTop: '4px solid #1976d2',
417
+ borderRadius: '50%',
418
+ animation: 'spin 1s linear infinite',
419
+ margin: '0 auto 16px'
420
+ }} />
421
+ <h3 style={{ color: '#333', marginBottom: '8px' }}>Loading...</h3>
422
+ <p style={{ color: '#666', margin: 0 }}>
423
+ Initializing session manager
424
+ </p>
425
+ </div>
426
+ <style>{`
427
+ @keyframes spin {
428
+ 0% { transform: rotate(0deg); }
429
+ 100% { transform: rotate(360deg); }
430
+ }
431
+ `}</style>
432
+ </div>
433
+ </SessionManager.Provider>
434
+ </ErrorBoundary>
435
+ </ThemeWrapper>
436
+ );
437
+ }
438
+ return (
439
+ <ThemeWrapper>
440
+ <ErrorBoundary>
441
+ <SessionManager.Provider value={contextValue}>
442
+ <ToastContainer
443
+ position="top-left"
444
+ autoClose={5000}
445
+ closeOnClick
446
+ pauseOnFocusLoss
447
+ pauseOnHover
448
+ newestOnTop={false}
449
+ toastClassName={"custToast materialToast"}
450
+ {...toastOptions}
451
+ />
452
+ <div style={{
453
+ display: 'flex',
454
+ flexDirection: 'column',
455
+ alignItems: 'center',
456
+ justifyContent: 'center',
457
+ minHeight: '100vh',
458
+ padding: '20px',
459
+ backgroundColor: '#f5f5f5',
460
+ fontFamily: 'Arial, sans-serif'
461
+ }}>
462
+ <div style={{
463
+ backgroundColor: 'white',
464
+ padding: '40px',
465
+ borderRadius: '8px',
466
+ boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
467
+ textAlign: 'center',
468
+ maxWidth: '400px'
469
+ }}>
470
+ <div style={{ fontSize: '48px', marginBottom: '16px' }}>⚠️</div>
471
+ <h2 style={{ color: '#d32f2f', marginBottom: '16px' }}>Connection Error</h2>
472
+ <p style={{ color: '#666', marginBottom: '24px' }}>
473
+ Unable to connect to the server. Please check your internet connection and try again.
474
+ </p>
475
+ <button
476
+ onClick={() => window.location.reload()}
477
+ style={{
478
+ backgroundColor: '#1976d2',
479
+ color: 'white',
480
+ border: 'none',
481
+ padding: '12px 24px',
482
+ borderRadius: '4px',
483
+ cursor: 'pointer',
484
+ fontSize: '16px'
485
+ }}
486
+ >
487
+ Retry
488
+ </button>
489
+ </div>
490
+ </div>
491
+ </SessionManager.Provider>
492
+ </ErrorBoundary>
493
+ </ThemeWrapper>
494
+ );
495
+ }
496
+
497
+ // If there's a persistent auth error, render a minimal error state
498
+ if (authError && !loadingUser) {
499
+ return (
500
+ <ThemeWrapper>
501
+ <ErrorBoundary>
502
+ <SessionManager.Provider value={contextValue}>
503
+ <ToastContainer
504
+ position="top-left"
505
+ autoClose={5000}
506
+ closeOnClick
507
+ pauseOnFocusLoss
508
+ pauseOnHover
509
+ newestOnTop={false}
510
+ toastClassName={"custToast materialToast"}
511
+ {...toastOptions}
512
+ />
513
+ <div style={{
514
+ display: 'flex',
515
+ flexDirection: 'column',
516
+ alignItems: 'center',
517
+ justifyContent: 'center',
518
+ minHeight: '100vh',
519
+ padding: '20px',
520
+ backgroundColor: '#f5f5f5',
521
+ fontFamily: 'Arial, sans-serif'
522
+ }}>
523
+ <div style={{
524
+ backgroundColor: 'white',
525
+ padding: '40px',
526
+ borderRadius: '8px',
527
+ boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
528
+ textAlign: 'center',
529
+ maxWidth: '400px'
530
+ }}>
531
+ <div style={{ fontSize: '48px', marginBottom: '16px' }}>⚠️</div>
532
+ <h2 style={{ color: '#d32f2f', marginBottom: '16px' }}>Connection Error</h2>
533
+ <p style={{ color: '#666', marginBottom: '24px' }}>
534
+ Unable to connect to the server. Please check your internet connection and try again.
535
+ </p>
536
+ <button
537
+ onClick={() => window.location.reload()}
538
+ style={{
539
+ backgroundColor: '#1976d2',
540
+ color: 'white',
541
+ border: 'none',
542
+ padding: '12px 24px',
543
+ borderRadius: '4px',
544
+ cursor: 'pointer',
545
+ fontSize: '16px'
546
+ }}
547
+ >
548
+ Retry
549
+ </button>
550
+ </div>
551
+ </div>
552
+ </SessionManager.Provider>
553
+ </ErrorBoundary>
554
+ </ThemeWrapper>
555
+ );
556
+ }
557
+
253
558
  return (
254
- <SessionManager.Provider value={contextValue}>
255
- <ToastContainer
256
- position="top-left"
257
- autoClose={5000}
258
- closeOnClick
259
- pauseOnFocusLoss
260
- pauseOnHover
261
- newestOnTop={false}
262
- toastClassName={"custToast materialToast"}
263
- {...toastOptions}
264
- />
265
- <VersionProtection appVersion={appVersion} />
266
- {children}
267
- </SessionManager.Provider>
559
+ <ThemeWrapper>
560
+ <ErrorBoundary>
561
+ <SessionManager.Provider value={contextValue}>
562
+ <ToastContainer
563
+ position="top-left"
564
+ autoClose={5000}
565
+ closeOnClick
566
+ pauseOnFocusLoss
567
+ pauseOnHover
568
+ newestOnTop={false}
569
+ toastClassName={"custToast materialToast"}
570
+ {...toastOptions}
571
+ />
572
+ <VersionProtection appVersion={appVersion} />
573
+ {children}
574
+ </SessionManager.Provider>
575
+ </ErrorBoundary>
576
+ </ThemeWrapper>
268
577
  );
269
578
  };
270
579
 
271
580
  function VersionProtection({ appVersion }) {
272
- const oldVersion = sessionStorage.getItem("appVersionOld") || false;
581
+ const [oldVersion, setOldVersion] = useState(false);
582
+
273
583
  useEffect(() => {
274
- if (
275
- oldVersion &&
276
- sessionStorage.getItem("requiredVersion") &&
277
- versionCompare(appVersion, sessionStorage.getItem("requiredVersion"))
278
- ) {
279
- console.log("Update Success Toast");
280
- toast.success("Your application has been updated", {
281
- toastId: "appReload",
282
- icon: <BrowserUpdated />,
283
- onClose: () => {
284
- sessionStorage.removeItem("appVersionOld");
285
- sessionStorage.removeItem("requiredVersion");
286
- sessionStorage.removeItem("appReloads");
287
- },
288
- });
584
+ try {
585
+ const isOldVersion = sessionStorage.getItem("appVersionOld") === "true";
586
+ setOldVersion(isOldVersion);
587
+
588
+ if (
589
+ isOldVersion &&
590
+ sessionStorage.getItem("requiredVersion") &&
591
+ appVersion &&
592
+ versionCompare(appVersion, sessionStorage.getItem("requiredVersion"))
593
+ ) {
594
+ console.log("Update Success Toast");
595
+ toast.success("Your application has been updated", {
596
+ toastId: "appReload",
597
+ icon: <BrowserUpdated />,
598
+ onClose: () => {
599
+ try {
600
+ sessionStorage.removeItem("appVersionOld");
601
+ sessionStorage.removeItem("requiredVersion");
602
+ sessionStorage.removeItem("appReloads");
603
+ } catch (err) {
604
+ console.error("Error cleaning up version storage:", err);
605
+ }
606
+ },
607
+ });
608
+ }
609
+ } catch (err) {
610
+ console.error("Error in version protection:", err);
289
611
  }
290
- }, [oldVersion]);
612
+ }, [appVersion]);
291
613
 
292
614
  return <></>;
293
615
  }