From f86a80bfb2290f14ea3fb8792d9e79604f5dfdfd Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Wed, 15 Apr 2026 23:18:56 +0800 Subject: [PATCH] fixed UI out of sync and multiple token expiry alert --- src/auth/index.js | 12 ++++++++---- src/layout/MainLayout/Header/index.js | 5 ++++- .../authentication/auth-forms/AuthLoginCustom.js | 2 -- src/routes/index.js | 16 ++++++++++++++++ src/store/reducers/authRevision.js | 15 +++++++++++++++ src/store/reducers/index.js | 3 ++- 6 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 src/store/reducers/authRevision.js diff --git a/src/auth/index.js b/src/auth/index.js index 5bf7bc3..9ff9474 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -45,6 +45,8 @@ export const handleLogin = data => { localStorage.setItem(windowCount, '0') localStorage.setItem(predictProductionQty, '0') localStorage.setItem(predictUsageCount, '0') + localStorage.removeItem('expiredAlertShown') + expiredAlertShownInMemory = false } } @@ -100,8 +102,9 @@ export const handleLogoutFunction = () => { localStorage.removeItem('transactionid') localStorage.removeItem('searchCriteria') //localStorage.removeItem(config.storageUserRoleKeyName) - localStorage.removeItem('expiredAlertShown'); - expiredAlertShownInMemory = false; + // Do not clear expiredAlertShown / expiredAlertShownInMemory here: logout runs right after + // the token-expired alert, and parallel 401s would each show another popup if we reset. + // Reset those only on successful login (handleLogin). localStorage.removeItem(refreshIntervalName) localStorage.removeItem(windowCount) localStorage.removeItem(predictProductionQty) @@ -147,7 +150,8 @@ export const SetupAxiosInterceptors = () => { }, async (error) => { // const { config, response: { status } } = error - if (error.response.status === 401 && error.config.url !== apiPath + REFRESH_TOKEN) { + const status = error.response?.status + if (status === 401 && error.config?.url !== apiPath + REFRESH_TOKEN) { // Make a request to refresh the access token const refreshToken = localStorage.getItem('refreshToken'); if (isRefreshToken) { @@ -210,7 +214,7 @@ export const SetupAxiosInterceptors = () => { await window.location.reload(); } - if (error.response.status === 500){ + if (error.response?.status === 500){ //setIsUploading(false); } // console.log(error) diff --git a/src/layout/MainLayout/Header/index.js b/src/layout/MainLayout/Header/index.js index f0ed1e0..335cc59 100644 --- a/src/layout/MainLayout/Header/index.js +++ b/src/layout/MainLayout/Header/index.js @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import React, { useState, useContext, useEffect, useRef } from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useNavigate, Link } from "react-router-dom"; import { SysContext } from "components/SysSettingProvider"; @@ -65,6 +65,9 @@ function Header(props) { const { sysSetting } = useContext(SysContext); const { window } = props; + /** Subscribe to auth changes (LOGIN/LOGOUT/cross-tab storage); Header reads localStorage and must re-render. */ + useSelector((state) => state.authRevision); + const [mobileOpen, setMobileOpen] = useState(false); const dispatch = useDispatch(); const navigate = useNavigate(); diff --git a/src/pages/authentication/auth-forms/AuthLoginCustom.js b/src/pages/authentication/auth-forms/AuthLoginCustom.js index ae0527d..b33a90e 100644 --- a/src/pages/authentication/auth-forms/AuthLoginCustom.js +++ b/src/pages/authentication/auth-forms/AuthLoginCustom.js @@ -42,7 +42,6 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { useDispatch } from "react-redux"; import { handleLogin } from "auth/index"; import useJwt from "auth/jwt/useJwt"; -import { handleLogoutFunction } from 'auth/index'; import { FormattedMessage, useIntl } from "react-intl"; import { SysContext } from "components/SysSettingProvider" @@ -85,7 +84,6 @@ const AuthLoginCustom = () => { return; } loginInFlightRef.current = true; - dispatch(handleLogoutFunction()); setOnLogin(true) useJwt .login({ username: values.username, password: values.password }) diff --git a/src/routes/index.js b/src/routes/index.js index 178822c..1f0410c 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,3 +1,6 @@ +import { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' + // project import import LoginRoutes from './LoginRoutes' // import MainRoutes from './MainRoutes' @@ -22,6 +25,19 @@ import AfterLoginRoutes from './AfterLoginRoutes'; // ==============================|| ROUTING RENDER ||============================== // export default function ThemeRoutes() { + const dispatch = useDispatch() + /** Re-run route config when auth changes (same-tab LOGIN/LOGOUT or another tab touching userData). */ + useSelector((state) => state.authRevision) + + useEffect(() => { + const onStorage = (e) => { + if (e.key === 'userData') { + dispatch({ type: 'AUTH_STORAGE_SYNC' }) + } + } + window.addEventListener('storage', onStorage) + return () => window.removeEventListener('storage', onStorage) + }, [dispatch]) if (isUserLoggedIn()) { //auto logout if token not valid diff --git a/src/store/reducers/authRevision.js b/src/store/reducers/authRevision.js new file mode 100644 index 0000000..7e1c11c --- /dev/null +++ b/src/store/reducers/authRevision.js @@ -0,0 +1,15 @@ +/** + * Bumps when auth-related storage or Redux auth actions change so components + * that read localStorage (e.g. Header) re-render. LOGIN/LOGOUT were previously + * dispatched without any reducer, so the UI could stay stale vs localStorage. + */ +export default function authRevision(state = 0, action) { + switch (action.type) { + case 'LOGIN': + case 'LOGOUT': + case 'AUTH_STORAGE_SYNC': + return state + 1; + default: + return state; + } +} diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js index e5c3261..1850cbd 100644 --- a/src/store/reducers/index.js +++ b/src/store/reducers/index.js @@ -3,9 +3,10 @@ import { combineReducers } from 'redux'; // project import import menu from './menu'; +import authRevision from './authRevision'; // ==============================|| COMBINE REDUCERS ||============================== // -const reducers = combineReducers({ menu }); +const reducers = combineReducers({ menu, authRevision }); export default reducers;