Files
gridpilot.gg/apps/companion/renderer/App.tsx
2025-12-15 13:46:07 +01:00

285 lines
8.4 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { SessionCreationForm } from './components/SessionCreationForm';
import { SessionProgressMonitor } from './components/SessionProgressMonitor';
import { LoginPrompt } from './components/LoginPrompt';
import { BrowserModeToggle } from './components/BrowserModeToggle';
import { CheckoutConfirmationDialog } from './components/CheckoutConfirmationDialog';
import { RaceCreationSuccessScreen } from './components/RaceCreationSuccessScreen';
import type { HostedSessionConfig } from '../../../core/automation/domain/types/HostedSessionConfig';
import type { AuthenticationState } from '../../../core/automation/domain/value-objects/AuthenticationState';
import type { StartAutomationResponse } from '../main/preload';
interface SessionProgress {
sessionId: string;
currentStep: number;
state: string;
completedSteps: number[];
hasError: boolean;
errorMessage: string | null;
}
type AuthState = AuthenticationState | 'CHECKING';
type LoginStatus = 'idle' | 'waiting' | 'success' | 'error';
export function App() {
const [authState, setAuthState] = useState<AuthState>('CHECKING');
const [authError, setAuthError] = useState<string | undefined>(undefined);
const [sessionId, setSessionId] = useState<string | null>(null);
const [progress, setProgress] = useState<SessionProgress | null>(null);
const [isRunning, setIsRunning] = useState(false);
const [loginStatus, setLoginStatus] = useState<LoginStatus>('idle');
const [checkoutRequest, setCheckoutRequest] = useState<{
price: string;
state: 'ready' | 'insufficient_funds';
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
} | null>(null);
const [raceCreationResult, setRaceCreationResult] = useState<{
sessionId: string;
sessionName: string;
trackId: string;
carIds: string[];
finalPrice: string;
createdAt: Date;
} | null>(null);
const handleLogin = useCallback(async () => {
if (!window.electronAPI) return;
setAuthError(undefined);
setLoginStatus('waiting');
try {
// This now waits for login to complete (auto-detects and closes browser)
const result = await window.electronAPI.initiateLogin();
if (result.success) {
// Login completed successfully - browser closed automatically
setLoginStatus('success');
// Show success message for 2 seconds before transitioning
setTimeout(() => {
setAuthState('AUTHENTICATED');
}, 2000);
} else {
setLoginStatus('error');
setAuthError(result.error);
}
} catch (error) {
console.error('Login failed:', error);
setLoginStatus('error');
setAuthError(error instanceof Error ? error.message : 'Login failed');
}
}, []);
const handleRetryAuth = useCallback(async () => {
if (!window.electronAPI) return;
setAuthState('CHECKING');
setAuthError(undefined);
setLoginStatus('idle');
try {
const result = await window.electronAPI.checkAuth();
if (result.success && result.state) {
setAuthState(result.state);
} else {
setAuthError(result.error);
setAuthState('UNKNOWN');
}
} catch (error) {
console.error('Auth check failed:', error);
setAuthError(error instanceof Error ? error.message : 'Connection failed');
setAuthState('UNKNOWN');
}
}, []);
useEffect(() => {
if (!window.electronAPI) return;
const checkAuth = async () => {
try {
const result = await window.electronAPI.checkAuth();
if (result.success && result.state) {
setAuthState(result.state);
} else {
setAuthError(result.error || 'Failed to check authentication');
setAuthState('UNKNOWN');
}
} catch (error) {
setAuthError(error instanceof Error ? error.message : 'Failed to check authentication');
setAuthState('UNKNOWN');
}
};
// Subscribe to checkout confirmation requests
const unsubscribeCheckout = window.electronAPI.onCheckoutConfirmationRequest((request) => {
setCheckoutRequest(request);
});
checkAuth();
window.electronAPI.onSessionProgress((newProgress: SessionProgress) => {
setProgress(newProgress);
if (newProgress.state === 'COMPLETED' ||
newProgress.state === 'FAILED' ||
newProgress.state === 'STOPPED_AT_STEP_18') {
setIsRunning(false);
}
});
// Cleanup subscription on unmount
return () => {
unsubscribeCheckout?.();
};
}, []);
const handleStartAutomation = async (config: HostedSessionConfig) => {
setIsRunning(true);
const result: StartAutomationResponse = await window.electronAPI.startAutomation(config);
if (result.success && result.sessionId) {
setSessionId(result.sessionId);
return;
}
setIsRunning(false);
if (result.authRequired) {
const nextAuthState = result.authState;
setAuthState(nextAuthState ?? 'EXPIRED');
setAuthError(result.error ?? 'Authentication required before starting automation.');
return;
}
alert(`Failed to start automation: ${result.error}`);
};
const handleStopAutomation = async () => {
if (sessionId) {
const result = await window.electronAPI.stopAutomation(sessionId);
if (result.success) {
setIsRunning(false);
setProgress(prev => prev ? { ...prev, state: 'STOPPED', hasError: false, errorMessage: 'User stopped automation' } : null);
} else {
alert(`Failed to stop automation: ${result.error}`);
}
}
};
if (authState === 'CHECKING') {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
backgroundColor: '#1a1a1a',
}}>
<div style={{
width: '60px',
height: '60px',
border: '4px solid #333',
borderTopColor: '#007bff',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}} />
<style>{`
@keyframes spin {
to { transform: rotate(360deg); }
}
`}</style>
<p style={{ color: '#aaa', marginTop: '1.5rem', fontSize: '1.1rem' }}>
Checking authentication...
</p>
</div>
);
}
// Show checkout confirmation dialog if requested
if (checkoutRequest) {
return <CheckoutConfirmationDialog request={checkoutRequest} />;
}
// Show race creation success screen if completed
if (raceCreationResult) {
return <RaceCreationSuccessScreen result={raceCreationResult} />;
}
if (authState !== 'AUTHENTICATED') {
return (
<LoginPrompt
authState={authState}
errorMessage={authError}
onLogin={handleLogin}
onRetry={handleRetryAuth}
loginStatus={loginStatus}
/>
);
}
return (
<div style={{
display: 'flex',
minHeight: '100vh',
backgroundColor: '#1a1a1a'
}}>
<div style={{
flex: 1,
padding: '2rem',
borderRight: '1px solid #333',
display: 'flex',
flexDirection: 'column'
}}>
<div style={{ flex: 1 }}>
<h1 style={{ marginBottom: '2rem', color: '#fff' }}>
GridPilot Companion
</h1>
<p style={{ marginBottom: '2rem', color: '#aaa' }}>
Hosted Session Automation POC
</p>
<SessionCreationForm
onSubmit={handleStartAutomation}
disabled={isRunning}
/>
{isRunning && (
<button
onClick={handleStopAutomation}
style={{
marginTop: '1rem',
padding: '0.75rem 1.5rem',
backgroundColor: '#dc3545',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '1rem',
fontWeight: 'bold'
}}
>
Stop Automation
</button>
)}
</div>
<BrowserModeToggle />
</div>
<div style={{
flex: 1,
padding: '2rem',
backgroundColor: '#0d0d0d'
}}>
<SessionProgressMonitor
sessionId={sessionId}
progress={progress}
isRunning={isRunning}
/>
</div>
</div>
);
}