@salesforce/webapp-template-app-react-sample-b2x-experimental 1.79.2 → 1.80.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 (44) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/classes/WebAppAuthUtils.cls +68 -0
  3. package/dist/force-app/main/default/classes/WebAppAuthUtils.cls-meta.xml +5 -0
  4. package/dist/force-app/main/default/classes/WebAppChangePassword.cls +77 -0
  5. package/dist/force-app/main/default/classes/WebAppChangePassword.cls-meta.xml +5 -0
  6. package/dist/force-app/main/default/classes/WebAppForgotPassword.cls +71 -0
  7. package/dist/force-app/main/default/classes/WebAppForgotPassword.cls-meta.xml +5 -0
  8. package/dist/force-app/main/default/classes/WebAppLogin.cls +105 -0
  9. package/dist/force-app/main/default/classes/WebAppLogin.cls-meta.xml +5 -0
  10. package/dist/force-app/main/default/classes/WebAppRegistration.cls +162 -0
  11. package/dist/force-app/main/default/classes/WebAppRegistration.cls-meta.xml +5 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +15 -6
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/app.tsx +4 -1
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +155 -88
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/api/userProfileApi.ts +81 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/authHelpers.ts +73 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/authenticationConfig.ts +61 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/context/AuthContext.tsx +95 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/footers/footer-link.tsx +36 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/forms/auth-form.tsx +81 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/forms/submit-button.tsx +49 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/hooks/form.tsx +120 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/hooks/useCountdownTimer.ts +266 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/hooks/useRetryWithBackoff.ts +109 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layout/card-skeleton.tsx +38 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layout/centered-page-layout.tsx +87 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layouts/AuthAppLayout.tsx +12 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layouts/authenticationRouteLayout.tsx +21 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/layouts/privateRouteLayout.tsx +36 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/ChangePassword.tsx +107 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/ForgotPassword.tsx +73 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/Login.tsx +97 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/Profile.tsx +139 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/Register.tsx +133 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/pages/ResetPassword.tsx +107 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +616 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/sessionTimeout/sessionTimeService.ts +161 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/sessionTimeout/sessionTimeoutConfig.ts +77 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/authentication/utils/helpers.ts +121 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +201 -114
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +67 -13
  43. package/dist/package.json +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,616 @@
1
+ /**
2
+ * Session timeout validator component
3
+ * Main orchestrator for session monitoring and timeout warnings
4
+ */
5
+
6
+ import { useEffect, useState, useCallback, useRef } from "react";
7
+ import { useLocation } from "react-router";
8
+ import { X } from "lucide-react";
9
+ import { useAuth } from "../context/AuthContext";
10
+ import { pollSessionTimeServlet, extendSessionTime } from "./sessionTimeService";
11
+ import { useCountdownTimer } from "../hooks/useCountdownTimer";
12
+ import { useRetryWithBackoff } from "../hooks/useRetryWithBackoff";
13
+ import { Alert, AlertTitle, AlertDescription } from "../../../components/ui/alert";
14
+ import { Button } from "../../../components/ui/button";
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ DialogDescription,
21
+ DialogFooter,
22
+ } from "../../../components/ui/dialog";
23
+ import {
24
+ STORAGE_KEYS,
25
+ LABELS,
26
+ INITIAL_RETRY_DELAY,
27
+ MAX_RETRY_ATTEMPTS,
28
+ MAX_RETRY_DELAY,
29
+ SESSION_WARNING_TIME,
30
+ } from "./sessionTimeoutConfig";
31
+ import { ROUTES } from "../authenticationConfig";
32
+
33
+ /**
34
+ * Configuration for session timeout monitoring
35
+ */
36
+ interface SessionTimeoutConfig {
37
+ /** Community base path (e.g., "/sfsites/c/") */
38
+ basePath: string;
39
+ /** Whether current user is a guest user */
40
+ isGuest: boolean;
41
+ }
42
+
43
+ /**
44
+ * Return value from useSessionTimeout hook
45
+ */
46
+ interface SessionTimeoutResult {
47
+ /** Seconds remaining in current session (null if no response received yet) */
48
+ timeLeftInSession: number | null;
49
+ /** Whether warning modal should be displayed */
50
+ showWarningModal: boolean;
51
+ /** Function to extend the session */
52
+ extendSession: () => Promise<void>;
53
+ /** Function to logout the user */
54
+ logout: () => void;
55
+ /** Function to check session status via API */
56
+ checkSession: () => Promise<void>;
57
+ /** Number of failed retry attempts */
58
+ retryAttempts: number;
59
+ /** Whether currently polling the session API */
60
+ isPolling: boolean;
61
+ }
62
+
63
+ /**
64
+ * Props for SessionTimeoutValidator component
65
+ */
66
+ export interface SessionTimeoutValidatorProps {
67
+ /** Community base path */
68
+ basePath: string;
69
+
70
+ // Optional callbacks
71
+ /** Called when session expires and user is logged out */
72
+ onSessionExpired?: () => void;
73
+ /** Called when session is extended with new time remaining */
74
+ onSessionExtended?: (newTimeRemaining: number) => void;
75
+ }
76
+
77
+ /**
78
+ * Props for SessionWarningModal component
79
+ */
80
+ interface SessionWarningModalProps {
81
+ /** Whether modal is visible */
82
+ isOpen: boolean;
83
+ /** Seconds remaining until session expires */
84
+ timeRemaining: number;
85
+ /** Called when user clicks "Continue Working" button */
86
+ onExtendSession: () => void;
87
+ /** Called when user clicks "Log Out" button */
88
+ onLogout: () => void;
89
+ /** Called when countdown timer reaches 0 (before logout) */
90
+ onCountdownExpire: () => void;
91
+ }
92
+
93
+ /**
94
+ * Props for SessionExpiredAlert component
95
+ */
96
+ interface SessionExpiredAlertProps {
97
+ /** Whether alert should be shown */
98
+ show: boolean;
99
+ /** Called when user dismisses the alert */
100
+ onDismiss: () => void;
101
+ /** Custom message to display (optional) */
102
+ message?: string;
103
+ }
104
+
105
+ /**
106
+ * Custom hook for session timeout monitoring
107
+ * Polls SessionTimeServlet at calculated intervals, handles retry with exponential backoff
108
+ *
109
+ * @internal
110
+ */
111
+ function useSessionTimeout(config: SessionTimeoutConfig): SessionTimeoutResult {
112
+ const { basePath, isGuest } = config;
113
+
114
+ // Session state
115
+ const [timeLeftInSession, setTimeLeftInSession] = useState<number | null>(null);
116
+ const [showWarningModal, setShowWarningModal] = useState<boolean>(false);
117
+ const [isPolling, setIsPolling] = useState<boolean>(false);
118
+
119
+ // Retry logic with exponential backoff
120
+ const retry = useRetryWithBackoff({
121
+ initialDelay: INITIAL_RETRY_DELAY,
122
+ maxAttempts: MAX_RETRY_ATTEMPTS,
123
+ maxDelay: MAX_RETRY_DELAY,
124
+ });
125
+
126
+ // Refs for timer management (prevents closure issues)
127
+ const pollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
128
+ const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
129
+ const isPollingRef = useRef<boolean>(false);
130
+ const checkSessionRef = useRef<(() => Promise<void>) | null>(null);
131
+ const extendSessionRef = useRef<(() => Promise<void>) | null>(null);
132
+
133
+ /**
134
+ * Clear the current polling timeout
135
+ */
136
+ const clearPollTimeout = useCallback(() => {
137
+ if (pollTimeoutRef.current) {
138
+ clearTimeout(pollTimeoutRef.current);
139
+ pollTimeoutRef.current = null;
140
+ }
141
+ }, []);
142
+
143
+ /**
144
+ * Clear the current retry timeout
145
+ */
146
+ const clearRetryTimeout = useCallback(() => {
147
+ if (retryTimeoutRef.current) {
148
+ clearTimeout(retryTimeoutRef.current);
149
+ retryTimeoutRef.current = null;
150
+ }
151
+ }, []);
152
+
153
+ /**
154
+ * Schedule the next session check
155
+ * Clears any existing poll or retry timeouts to prevent concurrent polling
156
+ *
157
+ * @param delay - Milliseconds to wait before next check
158
+ */
159
+ const scheduleCheck = useCallback(
160
+ (delay: number) => {
161
+ clearPollTimeout();
162
+ clearRetryTimeout();
163
+ pollTimeoutRef.current = setTimeout(() => {
164
+ checkSessionRef.current?.();
165
+ }, delay);
166
+ },
167
+ [clearPollTimeout, clearRetryTimeout],
168
+ );
169
+
170
+ /**
171
+ * Handle retry with exponential backoff
172
+ * Prevents concurrent retries by checking if a poll is already in progress
173
+ */
174
+ const handleRetryWithBackoff = useCallback(
175
+ (retryAction: () => void) => {
176
+ // Don't schedule retry if max attempts reached
177
+ if (retry.maxRetriesReached) {
178
+ console.error("[useSessionTimeout] Max retry attempts reached. Stopping polling.");
179
+ setIsPolling(false);
180
+ isPollingRef.current = false;
181
+ return;
182
+ }
183
+
184
+ // Don't schedule retry if a poll is already in progress
185
+ if (isPollingRef.current) {
186
+ console.warn("[useSessionTimeout] Poll already in progress, skipping retry scheduling");
187
+ return;
188
+ }
189
+
190
+ // Clear any existing retry timeout before scheduling new one
191
+ clearRetryTimeout();
192
+
193
+ // Calculate delay and schedule retry
194
+ const delay = retry.currentRetryDelay;
195
+ console.warn(
196
+ `[useSessionTimeout] Scheduling retry attempt ${retry.retryAttempts + 1}/${MAX_RETRY_ATTEMPTS} in ${delay}ms`,
197
+ );
198
+
199
+ // Let retry.scheduleRetry handle both the timeout and counter increment
200
+ const timeoutId = retry.scheduleRetry(() => {
201
+ retryTimeoutRef.current = null;
202
+ retryAction();
203
+ });
204
+
205
+ if (timeoutId) {
206
+ retryTimeoutRef.current = timeoutId;
207
+ }
208
+ },
209
+ [retry, clearRetryTimeout],
210
+ );
211
+
212
+ /**
213
+ * Process the session timeout response and schedule next check
214
+ *
215
+ * @param secondsRemaining - Seconds remaining in session
216
+ */
217
+ const processTimeoutResponse = useCallback(
218
+ (secondsRemaining: number) => {
219
+ setTimeLeftInSession(secondsRemaining);
220
+
221
+ // Session expired
222
+ if (secondsRemaining <= 0) {
223
+ setShowWarningModal(false);
224
+ // Note: logout() will be called by the component
225
+ return;
226
+ }
227
+
228
+ const shouldShowWarning = secondsRemaining <= SESSION_WARNING_TIME;
229
+
230
+ if (shouldShowWarning) {
231
+ // Show warning modal and schedule check for when session expires
232
+ setShowWarningModal(true);
233
+ scheduleCheck(secondsRemaining * 1000);
234
+ } else {
235
+ // Schedule check for when warning should appear
236
+ const timeUntilWarning = (secondsRemaining - SESSION_WARNING_TIME) * 1000;
237
+ setShowWarningModal(false);
238
+ scheduleCheck(timeUntilWarning);
239
+ }
240
+ },
241
+ [scheduleCheck],
242
+ );
243
+
244
+ /**
245
+ * Check session status via API
246
+ */
247
+ const checkSession = useCallback(async () => {
248
+ // Prevent concurrent polling
249
+ if (isPollingRef.current) {
250
+ return;
251
+ }
252
+
253
+ isPollingRef.current = true;
254
+ setIsPolling(true);
255
+
256
+ try {
257
+ const response = await pollSessionTimeServlet(basePath);
258
+
259
+ // Success - reset retry state and process response
260
+ isPollingRef.current = false;
261
+ setIsPolling(false);
262
+ retry.resetRetry();
263
+ processTimeoutResponse(response.sr);
264
+ } catch (error) {
265
+ console.error("[useSessionTimeout] Poll failed:", error);
266
+ // Reset polling flags before retry so handleRetryWithBackoff doesn't skip
267
+ isPollingRef.current = false;
268
+ setIsPolling(false);
269
+ handleRetryWithBackoff(() => checkSessionRef.current?.());
270
+ }
271
+ }, [basePath, retry, processTimeoutResponse, handleRetryWithBackoff]);
272
+
273
+ /**
274
+ * Extend the session (called when user clicks "Continue Working")
275
+ */
276
+ const extendSession = useCallback(async () => {
277
+ try {
278
+ const response = await extendSessionTime(basePath);
279
+
280
+ // Reset retry state and process the new session time
281
+ retry.resetRetry();
282
+ processTimeoutResponse(response.sr);
283
+ } catch (error) {
284
+ console.error("[useSessionTimeout] Failed to extend session:", error);
285
+ // On failure, retry extending session (not checkSession)
286
+ handleRetryWithBackoff(() => extendSessionRef.current?.());
287
+ }
288
+ }, [basePath, retry, processTimeoutResponse, handleRetryWithBackoff]);
289
+
290
+ // Update refs to always point to latest functions
291
+ useEffect(() => {
292
+ checkSessionRef.current = checkSession;
293
+ }, [checkSession]);
294
+
295
+ useEffect(() => {
296
+ extendSessionRef.current = extendSession;
297
+ }, [extendSession]);
298
+
299
+ /**
300
+ * Logout the user
301
+ * Note: Navigation is handled by the component, not this hook
302
+ */
303
+ const logout = useCallback(() => {
304
+ clearPollTimeout();
305
+ clearRetryTimeout();
306
+ setShowWarningModal(false);
307
+ setTimeLeftInSession(null);
308
+ setIsPolling(false);
309
+ isPollingRef.current = false;
310
+ }, [clearPollTimeout, clearRetryTimeout]);
311
+
312
+ // Initialize polling on mount (if authenticated)
313
+ useEffect(() => {
314
+ if (isGuest) {
315
+ return;
316
+ }
317
+
318
+ checkSessionRef.current?.();
319
+
320
+ // Cleanup on unmount
321
+ return () => {
322
+ clearPollTimeout();
323
+ clearRetryTimeout();
324
+ isPollingRef.current = false;
325
+ };
326
+ }, [isGuest, clearPollTimeout, clearRetryTimeout]);
327
+
328
+ return {
329
+ timeLeftInSession,
330
+ showWarningModal,
331
+ extendSession,
332
+ logout,
333
+ checkSession,
334
+ retryAttempts: retry.retryAttempts,
335
+ isPolling,
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Session Expired Alert
341
+ * Toast-like alert shown on login page after user is redirected due to session expiration
342
+ *
343
+ * @internal
344
+ */
345
+ function SessionExpiredAlert({
346
+ show,
347
+ onDismiss,
348
+ message = LABELS.invalidSessionMessage,
349
+ }: SessionExpiredAlertProps) {
350
+ if (!show) {
351
+ return null;
352
+ }
353
+
354
+ return (
355
+ <div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-md px-4">
356
+ <Alert variant="destructive" role="alert">
357
+ <AlertTitle className="flex items-center justify-between">
358
+ <span>{LABELS.sessionWarningTitle}</span>
359
+ <Button
360
+ variant="ghost"
361
+ size="sm"
362
+ onClick={onDismiss}
363
+ aria-label={LABELS.closeLabel}
364
+ className="h-auto p-1 hover:bg-destructive/10"
365
+ >
366
+ <X className="h-4 w-4" />
367
+ </Button>
368
+ </AlertTitle>
369
+ <AlertDescription>{message}</AlertDescription>
370
+ </Alert>
371
+ </div>
372
+ );
373
+ }
374
+
375
+ /**
376
+ * Session Warning Modal
377
+ * Modal dialog with countdown timer shown when session is near expiration
378
+ *
379
+ * @internal
380
+ */
381
+ function SessionWarningModal({
382
+ isOpen,
383
+ timeRemaining,
384
+ onExtendSession,
385
+ onLogout,
386
+ onCountdownExpire,
387
+ }: SessionWarningModalProps) {
388
+ const continueButtonRef = useRef<HTMLButtonElement>(null);
389
+
390
+ // Countdown timer with accessibility
391
+ const timer = useCountdownTimer({
392
+ initialTime: timeRemaining,
393
+ onExpire: onCountdownExpire,
394
+ });
395
+
396
+ const { start, stop, reset, formattedTime, isoTime, accessibilityAnnouncement } = timer;
397
+
398
+ // Consolidated timer management: handle both open/close and timeRemaining changes
399
+ useEffect(() => {
400
+ if (!isOpen) {
401
+ stop();
402
+ return;
403
+ }
404
+
405
+ // Modal is open: reset and start timer
406
+ reset();
407
+ start();
408
+ }, [isOpen, timeRemaining, start, stop, reset]);
409
+
410
+ // Focus the continue button when dialog opens
411
+ useEffect(() => {
412
+ if (isOpen) {
413
+ continueButtonRef.current?.focus();
414
+ }
415
+ }, [isOpen]);
416
+
417
+ // Handle "Continue Working" button click
418
+ const handleContinue = useCallback(() => {
419
+ stop();
420
+ onExtendSession();
421
+ }, [stop, onExtendSession]);
422
+
423
+ // Handle "Log Out" button click
424
+ const handleLogoutClick = useCallback(() => {
425
+ stop();
426
+ onLogout();
427
+ }, [stop, onLogout]);
428
+
429
+ return (
430
+ <Dialog open={isOpen} onOpenChange={() => {}}>
431
+ <DialogContent
432
+ showCloseButton={false}
433
+ onEscapeKeyDown={(e) => e.preventDefault()}
434
+ onPointerDownOutside={(e) => e.preventDefault()}
435
+ onInteractOutside={(e) => e.preventDefault()}
436
+ className="max-w-md"
437
+ >
438
+ <DialogHeader>
439
+ <DialogTitle id="session-warning-title">{LABELS.sessionWarningTitle}</DialogTitle>
440
+ <DialogDescription id="session-warning-description">
441
+ {LABELS.sessionWarningMessage}
442
+ </DialogDescription>
443
+ </DialogHeader>
444
+
445
+ <div className="flex flex-col items-center gap-4 py-6">
446
+ {/* Countdown Timer Display */}
447
+ <time
448
+ dateTime={isoTime}
449
+ role="timer"
450
+ aria-live="off"
451
+ aria-atomic="true"
452
+ aria-describedby="session-warning-description"
453
+ className="text-5xl font-bold text-center tabular-nums text-destructive"
454
+ >
455
+ {formattedTime}
456
+ </time>
457
+
458
+ {/* Accessibility Announcement Region */}
459
+ <div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
460
+ {accessibilityAnnouncement}
461
+ </div>
462
+ </div>
463
+
464
+ <DialogFooter className="flex gap-4 justify-end">
465
+ <Button variant="outline" onClick={handleLogoutClick} tabIndex={2}>
466
+ {LABELS.logoutButton}
467
+ </Button>
468
+ <Button variant="default" onClick={handleContinue} tabIndex={1} ref={continueButtonRef}>
469
+ {LABELS.continueButton}
470
+ </Button>
471
+ </DialogFooter>
472
+ </DialogContent>
473
+ </Dialog>
474
+ );
475
+ }
476
+
477
+ /**
478
+ * Session Timeout Validator
479
+ * Main component that monitors user session and displays warnings before expiration
480
+ *
481
+ * Features:
482
+ * - Polls SessionTimeServlet to monitor session status
483
+ * - Shows warning modal when session nears expiration
484
+ * - Handles session extension when user clicks "Continue Working"
485
+ * - Uses AuthContext.logout() for centralized logout handling
486
+ * - Shows expired message on login page after timeout redirect
487
+ * - Implements exponential backoff retry for failed API calls
488
+ * - Skips monitoring for guest users
489
+ *
490
+ * @example
491
+ * <SessionTimeoutValidator
492
+ * basePath={basePath}
493
+ * />
494
+ */
495
+ export default function SessionTimeoutValidator({
496
+ basePath,
497
+ onSessionExpired,
498
+ onSessionExtended,
499
+ }: SessionTimeoutValidatorProps) {
500
+ // Get authentication state and logout method
501
+ const { isAuthenticated, logout } = useAuth();
502
+ const isGuest = !isAuthenticated;
503
+
504
+ // Get current location from React Router
505
+ const location = useLocation();
506
+
507
+ // State for session expired alert
508
+ const [showExpiredAlert, setShowExpiredAlert] = useState(false);
509
+
510
+ // Session timeout monitoring hook
511
+ const sessionTimeout = useSessionTimeout({
512
+ basePath,
513
+ isGuest,
514
+ });
515
+
516
+ /**
517
+ * Check if we should show expired session message
518
+ * Called on mount and whenever pathname changes
519
+ */
520
+ useEffect(() => {
521
+ // Check if we're on the login page and should show expired message
522
+ const isLoginPage = location.pathname === ROUTES.LOGIN.PATH;
523
+ const shouldShowMessage = sessionStorage.getItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE) === "true";
524
+
525
+ if (isLoginPage && shouldShowMessage) {
526
+ setShowExpiredAlert(true);
527
+ // Clear the flag immediately after reading
528
+ sessionStorage.removeItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE);
529
+ }
530
+ }, [location.pathname]);
531
+
532
+ /**
533
+ * Handle session extension
534
+ * Called when user clicks "Continue Working" in warning modal
535
+ */
536
+ const handleExtendSession = useCallback(async () => {
537
+ await sessionTimeout.extendSession();
538
+
539
+ // Call optional callback
540
+ if (onSessionExtended && sessionTimeout.timeLeftInSession !== null) {
541
+ onSessionExtended(sessionTimeout.timeLeftInSession);
542
+ }
543
+ }, [sessionTimeout, onSessionExtended]);
544
+
545
+ /**
546
+ * Handle countdown expiration
547
+ * Checks session status with server before logging out to prevent premature logout
548
+ */
549
+ const handleCountdownExpired = useCallback(async () => {
550
+ await sessionTimeout.checkSession();
551
+ }, [sessionTimeout]);
552
+
553
+ /**
554
+ * Handle logout
555
+ * Called when session expires or user clicks "Log Out"
556
+ */
557
+ const handleLogout = useCallback(() => {
558
+ // Set flag in sessionStorage to show message on login page
559
+ sessionStorage.setItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE, "true");
560
+
561
+ // Stop session monitoring (clears timeouts, resets hook state)
562
+ sessionTimeout.logout();
563
+
564
+ // Call optional callback
565
+ if (onSessionExpired) {
566
+ onSessionExpired();
567
+ }
568
+
569
+ // Use centralized logout from AuthContext
570
+ // This clears auth state and redirects to logout URL
571
+ // Pass current location as retUrl to redirect back after logout
572
+ // Use window.location.pathname to include the base path
573
+ logout(window.location.pathname);
574
+ }, [sessionTimeout, logout, onSessionExpired]);
575
+
576
+ /**
577
+ * Handle session timeout (automatic logout when countdown reaches 0)
578
+ * Only trigger logout if we've received a response (not null) and session is expired
579
+ */
580
+ useEffect(() => {
581
+ if (
582
+ sessionTimeout.timeLeftInSession !== null &&
583
+ sessionTimeout.timeLeftInSession <= 0 &&
584
+ !isGuest
585
+ ) {
586
+ handleLogout();
587
+ }
588
+ }, [sessionTimeout.timeLeftInSession, isGuest, handleLogout]);
589
+
590
+ /**
591
+ * Dismiss expired session alert
592
+ */
593
+ const handleDismissAlert = useCallback(() => {
594
+ setShowExpiredAlert(false);
595
+ }, []);
596
+
597
+ return (
598
+ <>
599
+ {/* Session Warning Modal - only render when open */}
600
+ {sessionTimeout.showWarningModal && (
601
+ <SessionWarningModal
602
+ isOpen={sessionTimeout.showWarningModal}
603
+ timeRemaining={sessionTimeout.timeLeftInSession ?? 0}
604
+ onExtendSession={handleExtendSession}
605
+ onLogout={handleLogout}
606
+ onCountdownExpire={handleCountdownExpired}
607
+ />
608
+ )}
609
+
610
+ {/* Session Expired Alert (shown on login page) - only render when showExpiredAlert is true */}
611
+ {showExpiredAlert && (
612
+ <SessionExpiredAlert show={showExpiredAlert} onDismiss={handleDismissAlert} />
613
+ )}
614
+ </>
615
+ );
616
+ }