11/* eslint-disable no-undef */
2- import { Component , useEffect } from 'react' ;
2+ import { Component , useEffect , useState } from 'react' ;
33import { Provider , useSelector } from 'react-redux' ;
44import { BrowserRouter as Router , useLocation } from 'react-router-dom' ;
55import { PersistGate } from 'redux-persist/integration/react' ;
@@ -25,10 +25,8 @@ const queryClient = new QueryClient({
2525 } ,
2626} ) ;
2727
28- // Check for token
29- if ( process . env . NODE_ENV !== 'test' ) {
30- initAuth ( ) ;
31- }
28+ // Move auth initialization to after store is ready
29+ let authInitialized = false ;
3230
3331function UpdateDocumentTitle ( ) {
3432 const location = useLocation ( ) ;
@@ -126,36 +124,228 @@ function UpdateDocumentTitle() {
126124 try {
127125 initMessagingSocket ( token ) ;
128126 } catch ( error ) {
129- // console.error('WebSocket initialization failed:', error);
130- return error ;
127+ // eslint-disable-next-line no-console
128+ console . error ( 'WebSocket initialization failed:' , error ) ;
131129 }
132130 }
133- // else {
134- // // console.warn('No auth token found for WebSocket connection');
135- // }
136131 } , [ ] ) ;
137132
138133 return null ;
139134}
140135
136+ // Error Boundary Component
137+ class ErrorBoundary extends Component {
138+ constructor ( props ) {
139+ super ( props ) ;
140+ this . state = { hasError : false , error : null , errorInfo : null } ;
141+ }
142+
143+ static getDerivedStateFromError ( error ) {
144+ return { hasError : true } ;
145+ }
146+
147+ componentDidCatch ( error , errorInfo ) {
148+ this . setState ( {
149+ error : error ,
150+ errorInfo : errorInfo ,
151+ } ) ;
152+
153+ // Log the error
154+ logger . logError ( error ) ;
155+
156+ // Log additional error info for debugging
157+ console . error ( 'Error Boundary caught an error:' , error , errorInfo ) ;
158+ }
159+
160+ render ( ) {
161+ if ( this . state . hasError ) {
162+ return (
163+ < div
164+ className = "d-flex justify-content-center align-items-center"
165+ style = { { minHeight : '100vh' } }
166+ >
167+ < div className = "text-center" >
168+ < h2 > Something went wrong</ h2 >
169+ < p > We are sorry, but something unexpected happened. Please try refreshing the page.</ p >
170+ < button
171+ className = "btn btn-primary"
172+ onClick = { ( ) => {
173+ this . setState ( { hasError : false , error : null , errorInfo : null } ) ;
174+ window . location . reload ( ) ;
175+ } }
176+ >
177+ Refresh Page
178+ </ button >
179+ { process . env . NODE_ENV === 'development' && this . state . error && (
180+ < details style = { { whiteSpace : 'pre-wrap' , marginTop : '20px' } } >
181+ < summary > Error Details (Development)</ summary >
182+ < p > { this . state . error . toString ( ) } </ p >
183+ < p > { this . state . errorInfo . componentStack } </ p >
184+ </ details >
185+ ) }
186+ </ div >
187+ </ div >
188+ ) ;
189+ }
190+
191+ return this . props . children ;
192+ }
193+ }
194+
141195class App extends Component {
142196 constructor ( props ) {
143197 super ( props ) ;
144198 this . state = {
199+ storeReady : false ,
200+ authInitialized : false ,
145201 hasError : false ,
146202 error : null ,
147203 errorInfo : null ,
148- } ; // Moving state initialization into constructor as per linting rule.
204+ } ;
205+ this . isLoggingOut = false ; // Flag to prevent infinite logout loops
206+ this . broadcastChannel = null ; // BroadcastChannel for cross-tab communication
207+ this . tokenCheckInterval = null ; // Interval for periodic token checking
208+ this . lastTokenValue = null ; // Track last known token value
149209 }
150210
151- static getDerivedStateFromError ( error ) {
152- // Update state so the next render will show the fallback UI
153- return { hasError : true } ;
211+ componentDidMount ( ) {
212+ // Wait for store to be ready before initializing auth
213+ this . setState ( { storeReady : true } ) ;
214+
215+ // Initialize auth after a short delay to ensure store is fully ready
216+ setTimeout ( ( ) => {
217+ if ( process . env . NODE_ENV !== 'test' && ! authInitialized ) {
218+ try {
219+ initAuth ( ) ;
220+ authInitialized = true ;
221+ this . setState ( { authInitialized : true } ) ;
222+ // Store initial token value
223+ this . lastTokenValue = localStorage . getItem ( 'token' ) ;
224+ } catch ( error ) {
225+ console . error ( 'Error initializing auth:' , error ) ;
226+ logger . logError ( error ) ;
227+ }
228+ }
229+ } , 100 ) ;
230+
231+ // Listen for storage events to sync auth state between tabs
232+ this . handleStorageChange = this . handleStorageChange . bind ( this ) ;
233+ window . addEventListener ( 'storage' , this . handleStorageChange ) ;
234+
235+ // Use BroadcastChannel API for more reliable cross-tab communication
236+ if ( typeof BroadcastChannel !== 'undefined' ) {
237+ this . broadcastChannel = new BroadcastChannel ( 'auth_sync' ) ;
238+ this . broadcastChannel . onmessage = event => {
239+ if ( event . data && event . data . type === 'logout' ) {
240+ this . handleCrossTabLogout ( ) ;
241+ }
242+ } ;
243+ }
244+
245+ // Periodic token check as fallback (checks every 500ms)
246+ // This ensures we catch logout even if storage events don't fire
247+ this . tokenCheckInterval = setInterval ( ( ) => {
248+ this . checkTokenStatus ( ) ;
249+ } , 500 ) ;
250+ }
251+
252+ componentWillUnmount ( ) {
253+ // Clean up storage event listener
254+ if ( this . handleStorageChange ) {
255+ window . removeEventListener ( 'storage' , this . handleStorageChange ) ;
256+ }
257+ // Clean up BroadcastChannel
258+ if ( this . broadcastChannel ) {
259+ this . broadcastChannel . close ( ) ;
260+ }
261+ // Clean up token check interval
262+ if ( this . tokenCheckInterval ) {
263+ clearInterval ( this . tokenCheckInterval ) ;
264+ }
265+ }
266+
267+ checkTokenStatus ( ) {
268+ // Periodic check to detect if token was removed in another tab
269+ // This is a fallback for when storage events don't fire reliably
270+ if ( this . isLoggingOut ) return ; // Skip if already logging out
271+
272+ const currentToken = localStorage . getItem ( 'token' ) ;
273+ const wasAuthenticated = this . lastTokenValue !== null ;
274+ const isNowUnauthenticated = currentToken === null ;
275+
276+ // If we had a token before and now it's gone, we were logged out
277+ if ( wasAuthenticated && isNowUnauthenticated ) {
278+ this . handleCrossTabLogout ( ) ;
279+ }
280+
281+ // Update last known token value
282+ this . lastTokenValue = currentToken ;
283+ }
284+
285+ handleCrossTabLogout ( ) {
286+ // Handle cross-tab logout (called from storage events, BroadcastChannel, or token check)
287+ if ( ! this . isLoggingOut ) {
288+ this . isLoggingOut = true ;
289+
290+ // Immediately redirect to login - don't wait for Redux state update
291+ // This ensures the user sees the login page right away
292+ if (
293+ window . location . pathname !== '/login' &&
294+ ! window . location . pathname . startsWith ( '/login' ) &&
295+ ! window . location . pathname . startsWith ( '/forgotpassword' )
296+ ) {
297+ window . location . href = '/login' ;
298+ return ; // Exit early, let the redirect handle cleanup
299+ }
300+
301+ // If already on login page, just update Redux state
302+ if ( store && store . dispatch ) {
303+ const { logoutUser } = require ( '../actions/authActions' ) ;
304+ // Use a version of logoutUser that doesn't set the flag to prevent loops
305+ store . dispatch ( logoutUser ( true ) ) ; // Pass true to indicate cross-tab logout
306+ }
307+
308+ // Reset flag after a delay
309+ setTimeout ( ( ) => {
310+ this . isLoggingOut = false ;
311+ } , 1000 ) ;
312+ }
313+ }
314+
315+ handleStorageChange ( event ) {
316+ // Sync auth state when localStorage changes in other tabs
317+ if ( event . key === 'token' || event . key === 'authToken' ) {
318+ // Handle login sync - token was added in another tab
319+ if ( event . newValue && ! authInitialized ) {
320+ try {
321+ initAuth ( ) ;
322+ authInitialized = true ;
323+ this . setState ( { authInitialized : true } ) ;
324+ } catch ( error ) {
325+ console . error ( 'Error syncing auth from storage event:' , error ) ;
326+ }
327+ }
328+ // Handle logout sync - token was removed in another tab
329+ if ( event . newValue === null && event . oldValue !== null && ! this . isLoggingOut ) {
330+ // Token was removed in another tab, log out this tab
331+ this . handleCrossTabLogout ( ) ;
332+ }
333+ }
334+ // Also check for explicit logout flag
335+ if ( event . key === 'logoutFlag' ) {
336+ if ( event . newValue === 'true' && ! this . isLoggingOut ) {
337+ // Another tab triggered logout, log out this tab
338+ this . handleCrossTabLogout ( ) ;
339+ // Clear the flag
340+ localStorage . removeItem ( 'logoutFlag' ) ;
341+ }
342+ }
154343 }
155344
156345 componentDidCatch ( error , errorInfo ) {
157- // Log the error
158346 logger . logError ( error ) ;
347+ // eslint-disable-next-line no-console
348+ console . error ( 'App component caught an error:' , error , errorInfo ) ;
159349
160350 // Update state with error details
161351 this . setState ( {
@@ -226,15 +416,17 @@ class App extends Component {
226416 return (
227417 < Provider store = { store } >
228418 < PersistGate loading = { < Loading /> } persistor = { persistor } >
229- < QueryClientProvider client = { queryClient } >
230- < ModalProvider >
231- < Router >
232- < ThemeManager />
233- < UpdateDocumentTitle />
234- { routes }
235- </ Router >
236- </ ModalProvider >
237- </ QueryClientProvider >
419+ < ErrorBoundary >
420+ < QueryClientProvider client = { queryClient } >
421+ < ModalProvider >
422+ < Router >
423+ < ThemeManager />
424+ < UpdateDocumentTitle />
425+ { routes }
426+ </ Router >
427+ </ ModalProvider >
428+ </ QueryClientProvider >
429+ </ ErrorBoundary >
238430 </ PersistGate >
239431 </ Provider >
240432 ) ;
0 commit comments