diff --git a/packages/web-ui/src/api.ts b/packages/web-ui/src/api.ts index 5422c18f..57e7bad5 100644 --- a/packages/web-ui/src/api.ts +++ b/packages/web-ui/src/api.ts @@ -2026,34 +2026,63 @@ export function ensureHubAuth(method?: string): Promise { } let settled = false; + let finalCheckTimer: ReturnType | null = null; + + const statusUrl = () => + `/hub/auth/connect-status?session=${encodeURIComponent(sessionId)}&_t=${Date.now()}`; + const cleanup = () => { clearInterval(pollTimer); clearInterval(closedTimer); + if (finalCheckTimer) clearInterval(finalCheckTimer); + document.removeEventListener('visibilitychange', onVisible); _hubAuthPromise = null; }; - const pollTimer = setInterval(async () => { + const handleReady = (data: { token: string; user: HubUser }) => { if (settled) return; + settled = true; + saveHubAuth(data.token, data.user); + cleanup(); + popup?.close(); + resolve(); + }; + + const pollStatus = async () => { try { - const data = await request<{ ready?: boolean; token?: string; user?: HubUser }>( - `/hub/auth/connect-status?session=${encodeURIComponent(sessionId)}` - ); + const data = await request<{ ready?: boolean; token?: string; user?: HubUser }>(statusUrl()); if (data.ready && data.token && data.user) { - settled = true; - saveHubAuth(data.token, data.user); - cleanup(); - popup?.close(); - resolve(); + handleReady(data as { token: string; user: HubUser }); } } catch { /* Hub might be offline, keep polling */ } + }; + + const pollTimer = setInterval(() => { + if (settled) return; + pollStatus(); }, 1500); + const onVisible = () => { + if (settled || document.visibilityState !== 'visible') return; + pollStatus(); + }; + document.addEventListener('visibilitychange', onVisible); + const closedTimer = setInterval(() => { if (settled) return; if (popup?.closed) { - settled = true; - cleanup(); - if (getHubToken()) { resolve(); } else { reject(new Error('Hub login cancelled')); } + clearInterval(closedTimer); + let retries = 0; + finalCheckTimer = setInterval(async () => { + if (settled) { clearInterval(finalCheckTimer!); return; } + retries++; + await pollStatus(); + if (retries >= 5 && !settled) { + settled = true; + cleanup(); + if (getHubToken()) { resolve(); } else { reject(new Error('Hub login cancelled')); } + } + }, 1000); } }, 500); }); diff --git a/packages/web-ui/src/locales/en/common.json b/packages/web-ui/src/locales/en/common.json index c846cc06..691447b8 100644 --- a/packages/web-ui/src/locales/en/common.json +++ b/packages/web-ui/src/locales/en/common.json @@ -103,6 +103,7 @@ "hoursAgo": "{{count}}h", "daysAgo": "{{count}}d" }, + "signOut": "Sign Out", "confirmDelete": "Confirm Delete", "userPlaceholder": "User", "profile": { diff --git a/packages/web-ui/src/locales/zh-CN/common.json b/packages/web-ui/src/locales/zh-CN/common.json index 0da5c855..c1776778 100644 --- a/packages/web-ui/src/locales/zh-CN/common.json +++ b/packages/web-ui/src/locales/zh-CN/common.json @@ -103,6 +103,7 @@ "hoursAgo": "{{count}} 小时前", "daysAgo": "{{count}} 天前" }, + "signOut": "退出登录", "confirmDelete": "确认删除", "userPlaceholder": "用户", "profile": { diff --git a/packages/web-ui/src/pages/Settings.tsx b/packages/web-ui/src/pages/Settings.tsx index 5155fa96..c5e38e70 100644 --- a/packages/web-ui/src/pages/Settings.tsx +++ b/packages/web-ui/src/pages/Settings.tsx @@ -943,7 +943,7 @@ export function Settings({ theme, onThemeChange, authUser, onLogout, onUserUpdat className="w-full flex items-center gap-2.5 px-4 py-2 text-sm text-red-500 hover:bg-red-500/10 transition-colors" > - {t('common:signOut', { defaultValue: 'Sign Out' })} + {t('common:signOut')} )}