Skip to content

Commit 26d9871

Browse files
Merge pull request #4024 from OneCommunityGlobal/kanishk_fix_for_white_screen_page
Kanishk attempted fix white screen bug on first visit to pages
2 parents f197457 + 385a010 commit 26d9871

9 files changed

Lines changed: 638 additions & 234 deletions

File tree

src/actions/authActions.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,40 @@ export const getHeaderData = userId => {
115115
};
116116
};
117117

118-
export const logoutUser = () => dispatch => {
118+
export const logoutUser = (isCrossTabLogout = false) => dispatch => {
119119
// Clear any active force-logout timer before logging out
120120
dispatch(stopForceLogout());
121+
122+
// Only signal other tabs if this is NOT a cross-tab logout
123+
// (to prevent infinite loops when responding to storage events)
124+
if (!isCrossTabLogout) {
125+
// Set a flag to signal other tabs to log out
126+
// This flag will trigger a storage event in other tabs
127+
localStorage.setItem('logoutFlag', 'true');
128+
129+
// Use BroadcastChannel API for more reliable cross-tab communication
130+
if (typeof BroadcastChannel !== 'undefined') {
131+
try {
132+
const channel = new BroadcastChannel('auth_sync');
133+
channel.postMessage({ type: 'logout', timestamp: Date.now() });
134+
// Close the channel after sending
135+
setTimeout(() => channel.close(), 100);
136+
} catch (error) {
137+
// eslint-disable-next-line no-console
138+
console.warn('BroadcastChannel not available:', error);
139+
}
140+
}
141+
142+
// Clear the logout flag after a short delay to allow other tabs to detect it
143+
// We use setTimeout to ensure the storage event fires first
144+
setTimeout(() => {
145+
localStorage.removeItem('logoutFlag');
146+
}, 500);
147+
}
148+
149+
// Remove the token
121150
localStorage.removeItem(tokenKey);
151+
122152
httpService.setjwt(false);
123153
dispatch(setCurrentUser(null));
124154
};

src/components/App.jsx

Lines changed: 216 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable no-undef */
2-
import { Component, useEffect } from 'react';
2+
import { Component, useEffect, useState } from 'react';
33
import { Provider, useSelector } from 'react-redux';
44
import { BrowserRouter as Router, useLocation } from 'react-router-dom';
55
import { 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

3331
function 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+
141195
class 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

Comments
 (0)