/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useState, useCallback, useEffect } from 'react'; import type { LoadedSettings, SettingScope } from '../../config/settings.js'; import type { Config } from '@qwen-code/qwen-code-core'; import { AuthType, clearCachedCredentialFile, getErrorMessage, logAuth, AuthEvent, } from '@qwen-code/qwen-code-core'; import { AuthState } from '../types.js'; import { useQwenAuth } from '../hooks/useQwenAuth.js'; import type { OpenAICredentials } from '../components/OpenAIKeyPrompt.js'; export type { QwenAuthState } from '../hooks/useQwenAuth.js'; export const useAuthCommand = (settings: LoadedSettings, config: Config) => { const unAuthenticated = settings.merged.security?.auth?.selectedType === undefined; const [authState, setAuthState] = useState( unAuthenticated ? AuthState.Updating : AuthState.Unauthenticated, ); const [authError, setAuthError] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(unAuthenticated); const [pendingAuthType, setPendingAuthType] = useState( undefined, ); const { qwenAuthState, cancelQwenAuth } = useQwenAuth( pendingAuthType, isAuthenticating, ); const onAuthError = useCallback( (error: string | null) => { setAuthError(error); if (error) { setAuthState(AuthState.Updating); setIsAuthDialogOpen(true); } }, [setAuthError, setAuthState], ); const handleAuthFailure = useCallback( (error: unknown) => { setIsAuthenticating(false); const errorMessage = `Failed to authenticate. Message: ${getErrorMessage(error)}`; onAuthError(errorMessage); // Log authentication failure if (pendingAuthType) { const authEvent = new AuthEvent( pendingAuthType, 'manual', 'error', errorMessage, ); logAuth(config, authEvent); } }, [onAuthError, pendingAuthType, config], ); const handleAuthSuccess = useCallback( async ( authType: AuthType, scope: SettingScope, credentials?: OpenAICredentials, ) => { try { settings.setValue(scope, 'security.auth.selectedType', authType); // Only update credentials if not switching to QWEN_OAUTH, // so that OpenAI credentials are preserved when switching to QWEN_OAUTH. if (authType !== AuthType.QWEN_OAUTH && credentials) { if (credentials?.apiKey != null) { settings.setValue( scope, 'security.auth.apiKey', credentials.apiKey, ); } if (credentials?.baseUrl != null) { settings.setValue( scope, 'security.auth.baseUrl', credentials.baseUrl, ); } if (credentials?.model != null) { settings.setValue(scope, 'model.name', credentials.model); } await clearCachedCredentialFile(); } } catch (error) { handleAuthFailure(error); return; } setAuthError(null); setAuthState(AuthState.Authenticated); setPendingAuthType(undefined); setIsAuthDialogOpen(false); setIsAuthenticating(false); // Log authentication success const authEvent = new AuthEvent(authType, 'manual', 'success'); logAuth(config, authEvent); }, [settings, handleAuthFailure, config], ); const performAuth = useCallback( async ( authType: AuthType, scope: SettingScope, credentials?: OpenAICredentials, ) => { try { await config.refreshAuth(authType); handleAuthSuccess(authType, scope, credentials); } catch (e) { handleAuthFailure(e); } }, [config, handleAuthSuccess, handleAuthFailure], ); const handleAuthSelect = useCallback( async ( authType: AuthType | undefined, scope: SettingScope, credentials?: OpenAICredentials, ) => { if (!authType) { setIsAuthDialogOpen(false); setAuthError(null); return; } setPendingAuthType(authType); setAuthError(null); setIsAuthDialogOpen(false); setIsAuthenticating(true); if (authType === AuthType.USE_OPENAI) { if (credentials) { config.updateCredentials({ apiKey: credentials.apiKey, baseUrl: credentials.baseUrl, model: credentials.model, }); await performAuth(authType, scope, credentials); } return; } await performAuth(authType, scope); }, [config, performAuth], ); const openAuthDialog = useCallback(() => { setIsAuthDialogOpen(true); }, []); const cancelAuthentication = useCallback(() => { if (isAuthenticating && pendingAuthType === AuthType.QWEN_OAUTH) { cancelQwenAuth(); } // Log authentication cancellation if (isAuthenticating && pendingAuthType) { const authEvent = new AuthEvent(pendingAuthType, 'manual', 'cancelled'); logAuth(config, authEvent); } // Do not reset pendingAuthType here, persist the previously selected type. setIsAuthenticating(false); setIsAuthDialogOpen(true); setAuthError(null); }, [isAuthenticating, pendingAuthType, cancelQwenAuth, config]); /** /** * We previously used a useEffect to trigger authentication automatically when * settings.security.auth.selectedType changed. This caused problems: if authentication failed, * the UI could get stuck, since settings.json would update before success. Now, we * update selectedType in settings only when authentication fully succeeds. * Authentication is triggered explicitly—either during initial app startup or when the * user switches methods—not reactively through settings changes. This avoids repeated * or broken authentication cycles. */ useEffect(() => { const defaultAuthType = process.env['QWEN_DEFAULT_AUTH_TYPE']; if ( defaultAuthType && ![AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].includes( defaultAuthType as AuthType, ) ) { onAuthError( `Invalid QWEN_DEFAULT_AUTH_TYPE value: "${defaultAuthType}". Valid values are: ${[AuthType.QWEN_OAUTH, AuthType.USE_OPENAI].join(', ')}`, ); } }, [onAuthError]); return { authState, setAuthState, authError, onAuthError, isAuthDialogOpen, isAuthenticating, pendingAuthType, qwenAuthState, handleAuthSelect, openAuthDialog, cancelAuthentication, }; };