/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useEffect, useCallback } from 'react'; import { Box, Text } from 'ink'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { theme } from '../semantic-colors.js'; import { CliSpinner } from './CliSpinner.js'; import { openBrowserSecurely, shouldLaunchBrowser, type ValidationIntent, } from '@google/gemini-cli-core'; import { useKeypress } from '../hooks/useKeypress.js'; import { keyMatchers, Command } from '../keyMatchers.js'; interface ValidationDialogProps { validationLink?: string; validationDescription?: string; learnMoreUrl?: string; onChoice: (choice: ValidationIntent) => void; } type DialogState = 'choosing' | 'waiting' | 'complete' | 'error'; export function ValidationDialog({ validationLink, learnMoreUrl, onChoice, }: ValidationDialogProps): React.JSX.Element { const [state, setState] = useState('choosing'); const [errorMessage, setErrorMessage] = useState(''); const items = [ { label: 'Verify your account', value: 'verify' as const, key: 'verify', }, { label: 'Change authentication', value: 'change_auth' as const, key: 'change_auth', }, ]; // Handle keypresses globally for cancellation, and specific logic for waiting state useKeypress( (key) => { if (keyMatchers[Command.ESCAPE](key) || keyMatchers[Command.QUIT](key)) { onChoice('cancel'); return true; } else if (state === 'waiting' && keyMatchers[Command.RETURN](key)) { // User confirmed verification is complete - transition to 'complete' state setState('complete'); return true; } return false; }, { isActive: state !== 'complete' }, ); // When state becomes 'complete', show success message briefly then proceed useEffect(() => { if (state === 'complete') { const timer = setTimeout(() => { onChoice('verify'); }, 500); return () => clearTimeout(timer); } return undefined; }, [state, onChoice]); const handleSelect = useCallback( async (choice: ValidationIntent) => { if (choice === 'verify') { if (validationLink) { // Check if we're in an environment where we can launch a browser if (!shouldLaunchBrowser()) { // In headless mode, show the link and wait for user to manually verify setErrorMessage( `Please open this URL in a browser: ${validationLink}`, ); setState('waiting'); return; } try { await openBrowserSecurely(validationLink); setState('waiting'); } catch (error) { setErrorMessage( error instanceof Error ? error.message : 'Failed to open browser', ); setState('error'); } } else { // No validation link, just retry onChoice('verify'); } } else { // 'change_auth' or 'cancel' onChoice(choice); } }, [validationLink, onChoice], ); if (state === 'error') { return ( {errorMessage || 'Failed to open verification link. Please try again or change authentication.'} void handleSelect(choice as ValidationIntent)} /> ); } if (state === 'waiting') { return ( {' '} Waiting for verification... (Press ESC or CTRL+C to cancel) {errorMessage && ( {errorMessage} )} Press Enter when verification is complete. ); } if (state === 'complete') { return ( Verification complete ); } return ( Further action is required to use this service. void handleSelect(choice as ValidationIntent)} /> {learnMoreUrl && ( Learn more: {learnMoreUrl} )} ); }