285 lines
8.4 KiB
TypeScript
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 '../main/automation/domain/types/HostedSessionConfig';
|
|
import type { AuthenticationState } from '../main/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>
|
|
);
|
|
} |