From bc12f8aabc90e9aa3e4ed09fd2374fdda2552609 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Wed, 18 Mar 2026 17:37:36 +0300 Subject: [PATCH] Fix WS offline on first login: sync token between api.ts and AuthContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/contexts/AuthContext.tsx | 39 +++++++++++++++++++++++++++++------- src/lib/api.ts | 2 ++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 3833121..4dd931c 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -39,20 +39,45 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { .then(r => r.ok ? r.json() : null) .then((data: { access_token?: string } | null) => { if (data?.access_token) { - const s: AuthSession = { ...session, token: data.access_token } - setSession(s) - localStorage.setItem('hotelsync-session', JSON.stringify(s)) + const tok = data.access_token + setSession(cur => { + if (!cur) return null + const s: AuthSession = { ...cur, token: tok } + localStorage.setItem('hotelsync-session', JSON.stringify(s)) + return s + }) } else { - setSession(null) - localStorage.removeItem('hotelsync-session') + // Guard: if login() set a fresh token while refresh was in-flight — keep it + setSession(cur => { + if (cur?.token) return cur + localStorage.removeItem('hotelsync-session') + return null + }) } }) .catch(() => { - setSession(null) - localStorage.removeItem('hotelsync-session') + // Guard: same race-condition protection + setSession(cur => { + if (cur?.token) return cur + localStorage.removeItem('hotelsync-session') + return null + }) }) }, []) // 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 => { try { const { access_token, user } = await api.auth.login(email, password) diff --git a/src/lib/api.ts b/src/lib/api.ts index d715bbb..44cfce0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -30,6 +30,8 @@ function saveToken(token: string) { const parsed = JSON.parse(s) as Record parsed.token = token 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 { // ignore }