From be08a4bfdfd0693f6709bf9a87179a8fd3023eac Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Tue, 11 Aug 2026 18:38:57 +0800 Subject: [PATCH] fix(oauth): persist successful login status across navigation --- src/pages/ManagementPages.tsx | 67 +++++++++++---------------------- src/services/oauthLoginState.ts | 14 +++++++ tests/oauthLoginState.test.ts | 22 +++++++++++ 3 files changed, 58 insertions(+), 45 deletions(-) create mode 100644 src/services/oauthLoginState.ts create mode 100644 tests/oauthLoginState.test.ts diff --git a/src/pages/ManagementPages.tsx b/src/pages/ManagementPages.tsx index 0b69ea3..ad256e2 100644 --- a/src/pages/ManagementPages.tsx +++ b/src/pages/ManagementPages.tsx @@ -20,6 +20,10 @@ import { type AuthFileSnapshot, } from '../services/authFiles'; import { managementApi, responseList } from '../services/managementApi'; +import { + createOAuthLoginSuccessCache, + shouldShowOAuthLoginStatus, +} from '../services/oauthLoginState'; import { AuthFileManagementPage } from './AuthFileManagementPage'; import { QuotaPage } from './QuotaPage'; @@ -65,8 +69,15 @@ const OAUTH_CALLBACK_SUPPORTED = new Set([ 'xai', ]); const XAI_CALLBACK_URL = 'http://127.0.0.1:56121/callback'; -const OAUTH_SUCCESS_RESET_MS = 5000; const OAUTH_POLL_INTERVAL_MS = 3000; +const oauthLoginSuccessCache = createOAuthLoginSuccessCache(); + +const cachedOAuthProviderStates = (): Partial> => ( + oauthLoginSuccessCache.snapshot().reduce>>( + (states, provider) => ({ ...states, [provider]: { status: 'success' } }), + {}, + ) +); export function OAuthManagementPage() { const { t } = useI18n(); @@ -115,14 +126,15 @@ export function OAuthManagementPage() { export function OAuthLoginPage() { const { t } = useI18n(); - const [states, setStates] = useState>>({}); + const [states, setStates] = useState>>( + cachedOAuthProviderStates, + ); const [notice, setNotice] = useState<{ message: string; tone: 'success' | 'error' | 'info'; } | null>(null); const pollingTimers = useRef>>({}); const pollingRequests = useRef>>({}); - const successResetTimers = useRef>>({}); const credentialSnapshots = useRef>>({}); const noticeTimerRef = useRef(null); @@ -156,32 +168,10 @@ export function OAuthLoginPage() { delete pollingRequests.current[provider]; }, []); - const clearSuccessResetTimer = useCallback((provider: OAuthProviderId) => { - const timer = successResetTimers.current[provider]; - if (timer !== undefined) window.clearTimeout(timer); - delete successResetTimers.current[provider]; - }, []); - - const clearProviderTimers = useCallback( - (provider: OAuthProviderId) => { - clearPollingTimer(provider); - clearSuccessResetTimer(provider); - }, - [clearPollingTimer, clearSuccessResetTimer], - ); - - const resetProviderAttempt = useCallback( - (provider: OAuthProviderId) => { - clearProviderTimers(provider); - delete credentialSnapshots.current[provider]; - setStates((current) => ({ ...current, [provider]: { status: 'idle' } })); - }, - [clearProviderTimers], - ); - const completeProviderAuth = useCallback( (provider: OAuthProviderId) => { - clearProviderTimers(provider); + clearPollingTimer(provider); + oauthLoginSuccessCache.mark(provider); updateProviderState(provider, { url: undefined, state: undefined, @@ -193,11 +183,8 @@ export function OAuthLoginPage() { callbackStatus: undefined, callbackError: undefined, }); - successResetTimers.current[provider] = window.setTimeout(() => { - resetProviderAttempt(provider); - }, OAUTH_SUCCESS_RESET_MS); }, - [clearProviderTimers, resetProviderAttempt, updateProviderState], + [clearPollingTimer, updateProviderState], ); const captureCredentialSnapshot = useCallback(async (provider: OAuthProviderId) => { @@ -281,15 +268,12 @@ export function OAuthLoginPage() { Object.values(pollingTimers.current).forEach((timer) => { if (timer !== undefined) window.clearInterval(timer); }); - Object.values(successResetTimers.current).forEach((timer) => { - if (timer !== undefined) window.clearTimeout(timer); - }); if (noticeTimerRef.current !== null) window.clearTimeout(noticeTimerRef.current); }; }, []); const startLogin = async (provider: OAuthProviderId) => { - clearProviderTimers(provider); + clearPollingTimer(provider); updateProviderState(provider, { url: undefined, state: undefined, @@ -434,9 +418,9 @@ export function OAuthLoginPage() {

{provider.name}

- - {oauthStatusLabel(state, t)} - + {shouldShowOAuthLoginStatus(state.status) ? ( + {t('oauth.status.completed')} + ) : null}
@@ -510,13 +494,6 @@ export function OAuthLoginPage() { ); } -function oauthStatusLabel(state: OAuthProviderState, t: ReturnType['t']) { - if (state.status === 'success') return t('oauth.status.completed'); - if (state.status === 'error') return t('oauth.status.failed'); - if (state.status === 'waiting' || state.polling) return t('oauth.status.waiting'); - return t('oauth.status.signedOut'); -} - function providerLabel(provider: OAuthProviderId) { return oauthProviders.find((item) => item.id === provider)?.name ?? provider; } diff --git a/src/services/oauthLoginState.ts b/src/services/oauthLoginState.ts new file mode 100644 index 0000000..6c7136b --- /dev/null +++ b/src/services/oauthLoginState.ts @@ -0,0 +1,14 @@ +export const shouldShowOAuthLoginStatus = (status: string) => status === 'success'; + +export const createOAuthLoginSuccessCache = () => { + const providers = new Set(); + + return { + mark(provider: T) { + providers.add(provider); + }, + snapshot() { + return Array.from(providers); + }, + }; +}; diff --git a/tests/oauthLoginState.test.ts b/tests/oauthLoginState.test.ts new file mode 100644 index 0000000..a3c9793 --- /dev/null +++ b/tests/oauthLoginState.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from 'bun:test'; +import { + createOAuthLoginSuccessCache, + shouldShowOAuthLoginStatus, +} from '../src/services/oauthLoginState'; + +describe('OAuth login display state', () => { + test('only successful login states display a status badge', () => { + expect(shouldShowOAuthLoginStatus('idle')).toBeFalse(); + expect(shouldShowOAuthLoginStatus('waiting')).toBeFalse(); + expect(shouldShowOAuthLoginStatus('error')).toBeFalse(); + expect(shouldShowOAuthLoginStatus('success')).toBeTrue(); + }); + + test('successful providers remain cached across page remounts', () => { + const cache = createOAuthLoginSuccessCache<'codex' | 'claude'>(); + cache.mark('codex'); + + expect(cache.snapshot()).toEqual(['codex']); + expect(cache.snapshot()).toEqual(['codex']); + }); +});