Fix WS offline on first login: sync token between api.ts and AuthContext
Two root causes fixed: 1. api.ts saveToken() updated only localStorage, not React state — WS kept reconnecting with the expired token. Now dispatches hotelsync:token-updated event; AuthContext listens and updates session state, triggering WS reconnect with the fresh token. 2. AuthContext mount effect could race with login(): if refresh failed while login() was concurrently setting a new token, catch() called setSession(null) and wiped the fresh session. Fixed with functional setSession updater that checks current state before clearing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,20 +39,45 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then((data: { access_token?: string } | null) => {
|
.then((data: { access_token?: string } | null) => {
|
||||||
if (data?.access_token) {
|
if (data?.access_token) {
|
||||||
const s: AuthSession = { ...session, token: data.access_token }
|
const tok = data.access_token
|
||||||
setSession(s)
|
setSession(cur => {
|
||||||
localStorage.setItem('hotelsync-session', JSON.stringify(s))
|
if (!cur) return null
|
||||||
|
const s: AuthSession = { ...cur, token: tok }
|
||||||
|
localStorage.setItem('hotelsync-session', JSON.stringify(s))
|
||||||
|
return s
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
setSession(null)
|
// Guard: if login() set a fresh token while refresh was in-flight — keep it
|
||||||
localStorage.removeItem('hotelsync-session')
|
setSession(cur => {
|
||||||
|
if (cur?.token) return cur
|
||||||
|
localStorage.removeItem('hotelsync-session')
|
||||||
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setSession(null)
|
// Guard: same race-condition protection
|
||||||
localStorage.removeItem('hotelsync-session')
|
setSession(cur => {
|
||||||
|
if (cur?.token) return cur
|
||||||
|
localStorage.removeItem('hotelsync-session')
|
||||||
|
return null
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Keep React state in sync when api.ts refreshes the token (e.g. after 401 auto-refresh)
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: Event) => {
|
||||||
|
const { token } = (e as CustomEvent<{ token: string }>).detail
|
||||||
|
setSession(cur => {
|
||||||
|
if (!cur) return cur
|
||||||
|
return { ...cur, token }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
window.addEventListener('hotelsync:token-updated', handler)
|
||||||
|
return () => window.removeEventListener('hotelsync:token-updated', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const login = async (email: string, password: string): Promise<User | null> => {
|
const login = async (email: string, password: string): Promise<User | null> => {
|
||||||
try {
|
try {
|
||||||
const { access_token, user } = await api.auth.login(email, password)
|
const { access_token, user } = await api.auth.login(email, password)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ function saveToken(token: string) {
|
|||||||
const parsed = JSON.parse(s) as Record<string, unknown>
|
const parsed = JSON.parse(s) as Record<string, unknown>
|
||||||
parsed.token = token
|
parsed.token = token
|
||||||
localStorage.setItem('hotelsync-session', JSON.stringify(parsed))
|
localStorage.setItem('hotelsync-session', JSON.stringify(parsed))
|
||||||
|
// Notify AuthContext so React state stays in sync (WS reconnects with fresh token)
|
||||||
|
window.dispatchEvent(new CustomEvent('hotelsync:token-updated', { detail: { token } }))
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user