import { ipcMain } from 'electron'; import type { BrowserWindow, IpcMainInvokeEvent } from 'electron'; import { DIContainer } from './di-container'; import type { HostedSessionConfig } from 'apps/companion/main/automation/domain/types/HostedSessionConfig'; import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState'; import { ElectronCheckoutConfirmationAdapter } from 'core/automation/infrastructure//ipc/ElectronCheckoutConfirmationAdapter'; import type { OverlayAction } from 'apps/companion/main/automation/application/ports/OverlaySyncPort'; import type { IAutomationLifecycleEmitter } from 'core/automation/infrastructure//AutomationLifecycleEmitter'; let progressMonitorInterval: NodeJS.Timeout | null = null; let lifecycleSubscribed = false; export function setupIpcHandlers(mainWindow: BrowserWindow): void { const container = DIContainer.getInstance(); const logger = container.getLogger(); // Setup checkout confirmation adapter and wire it into the container const checkoutConfirmationAdapter = new ElectronCheckoutConfirmationAdapter(mainWindow); container.setConfirmCheckoutUseCase(checkoutConfirmationAdapter); // Authentication handlers ipcMain.handle('auth:check', async () => { try { logger.info('Checking authentication status'); const checkAuthUseCase = container.getCheckAuthenticationUseCase(); if (!checkAuthUseCase) { logger.error('Authentication use case not available'); return { success: false, error: 'Authentication not available - check system configuration' }; } // NO browser connection needed - cookie check reads JSON file directly const result = await checkAuthUseCase.execute(); if (result.isErr()) { logger.error('Auth check failed', result.unwrapErr()); return { success: false, error: result.unwrapErr().message }; } const state = result.unwrap(); logger.info('Authentication check complete', { state }); return { success: true, state }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Auth check failed', err); return { success: false, error: err.message }; } }); ipcMain.handle('auth:login', async () => { try { logger.info('Starting iRacing login flow (will wait for completion)'); const authService = container.getAuthenticationService(); if (!authService) { // Mock mode - no actual login needed logger.warn('Auth service not available in mock mode'); return { success: true, message: 'Mock mode - login bypassed' }; } // Use the Playwright browser for login (same browser used for automation) // This now waits for login to complete, auto-detects success, and closes browser const initiateLoginUseCase = container.getInitiateLoginUseCase(); if (!initiateLoginUseCase) { logger.warn('Initiate login use case not available'); return { success: false, error: 'Login not available' }; } // This call now blocks until login is complete or times out const result = await initiateLoginUseCase.execute(); if (result.isErr()) { logger.error('Login failed or timed out', result.unwrapErr()); return { success: false, error: result.unwrapErr().message }; } logger.info('Login completed successfully'); return { success: true, message: 'Login completed successfully' }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Login flow failed', err); return { success: false, error: err.message }; } }); ipcMain.handle('auth:confirmLogin', async () => { try { logger.info('User confirmed login completion'); const authService = container.getAuthenticationService(); if (!authService) { logger.warn('Auth service not available in mock mode'); return { success: true, state: AuthenticationState.AUTHENTICATED }; } // Call confirmLoginComplete on the adapter if it exists if ('confirmLoginComplete' in authService && typeof authService.confirmLoginComplete === 'function') { const result = await authService.confirmLoginComplete(); if (result.isErr()) { logger.error('Confirm login failed', result.unwrapErr()); return { success: false, error: result.unwrapErr().message }; } } logger.info('Login confirmation recorded'); return { success: true, state: AuthenticationState.AUTHENTICATED }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Failed to confirm login', err); return { success: false, error: err.message }; } }); ipcMain.handle('auth:logout', async () => { try { logger.info('Clearing session (logout)'); const clearSessionUseCase = container.getClearSessionUseCase(); if (!clearSessionUseCase) { logger.warn('Logout not available in mock mode'); return { success: true, message: 'Mock mode - logout bypassed' }; } const result = await clearSessionUseCase.execute(); if (result.isErr()) { logger.error('Logout failed', result.unwrapErr()); return { success: false, error: result.unwrapErr().message }; } logger.info('Session cleared successfully'); return { success: true }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Logout failed', err); return { success: false, error: err.message }; } }); ipcMain.handle('auth:getState', async () => { try { const authService = container.getAuthenticationService(); if (!authService) { return { success: true, state: AuthenticationState.AUTHENTICATED }; } return { success: true, state: authService.getState() }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Failed to get auth state', err); return { success: false, error: err.message }; } }); ipcMain.handle('start-automation', async (_event: IpcMainInvokeEvent, config: HostedSessionConfig) => { try { const container = DIContainer.getInstance(); const startAutomationUseCase = container.getStartAutomationUseCase(); const sessionRepository = container.getSessionRepository(); const automationEngine = container.getAutomationEngine(); logger.info('Starting automation', { sessionName: config.sessionName }); if (progressMonitorInterval) { clearInterval(progressMonitorInterval); progressMonitorInterval = null; } const connectionResult = await container.initializeBrowserConnection(); if (!connectionResult.success) { logger.error('Browser connection failed', undefined, { errorMessage: connectionResult.error }); return { success: false, error: connectionResult.error }; } logger.info('Browser connection established'); const checkAuthUseCase = container.getCheckAuthenticationUseCase(); if (checkAuthUseCase) { const authResult = await checkAuthUseCase.execute({ verifyPageContent: true, }); if (authResult.isOk()) { const authState = authResult.unwrap(); if (authState !== AuthenticationState.AUTHENTICATED) { logger.warn('Not authenticated or session expired - automation cannot proceed', { authState }); return { success: false, error: 'Not authenticated or session expired. Please login first.', authRequired: true, authState, }; } logger.info('Authentication verified (cookies and page state)'); } else { logger.warn('Auth check failed, proceeding anyway', { error: authResult.unwrapErr().message }); } } const result = await startAutomationUseCase.execute(config); logger.info('Automation session created', { sessionId: result.sessionId }); const session = await sessionRepository.findById(result.sessionId); if (session) { logger.info('Executing step 1'); await automationEngine.executeStep(StepId.create(1), config); } progressMonitorInterval = setInterval(async () => { const containerForProgress = DIContainer.getInstance(); const repoForProgress = containerForProgress.getSessionRepository(); const updatedSession = await repoForProgress.findById(result.sessionId); if (!updatedSession) { if (progressMonitorInterval) { clearInterval(progressMonitorInterval); progressMonitorInterval = null; } return; } mainWindow.webContents.send('session-progress', { sessionId: result.sessionId, currentStep: updatedSession.currentStep.value, state: updatedSession.state.value, completedSteps: Array.from({ length: updatedSession.currentStep.value - 1 }, (_, i) => i + 1), hasError: updatedSession.errorMessage !== undefined, errorMessage: updatedSession.errorMessage || null }); if (updatedSession.state.value === 'COMPLETED' || updatedSession.state.value === 'FAILED' || updatedSession.state.value === 'STOPPED_AT_STEP_18') { logger.info('Automation ended', { state: updatedSession.state.value }); if (progressMonitorInterval) { clearInterval(progressMonitorInterval); progressMonitorInterval = null; } } }, 100); return { success: true, sessionId: result.sessionId }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Automation failed', err); return { success: false, error: err.message }; } }); ipcMain.handle('get-session-status', async (_event: IpcMainInvokeEvent, sessionId: string) => { const container = DIContainer.getInstance(); const sessionRepository = container.getSessionRepository(); const session = await sessionRepository.findById(sessionId); if (!session) { return { found: false }; } return { found: true, currentStep: session.currentStep.value, state: session.state.value, completedSteps: Array.from({ length: session.currentStep.value - 1 }, (_, i) => i + 1), hasError: session.errorMessage !== undefined, errorMessage: session.errorMessage || null }; }); ipcMain.handle('pause-automation', async (_event: IpcMainInvokeEvent, _sessionId: string) => { return { success: false, error: 'Pause not implemented in POC' }; }); ipcMain.handle('resume-automation', async (_event: IpcMainInvokeEvent, _sessionId: string) => { return { success: false, error: 'Resume not implemented in POC' }; }); ipcMain.handle('stop-automation', async (_event: IpcMainInvokeEvent, sessionId: string) => { try { const container = DIContainer.getInstance(); const automationEngine = container.getAutomationEngine(); const sessionRepository = container.getSessionRepository(); logger.info('Stopping automation', { sessionId }); if (progressMonitorInterval) { clearInterval(progressMonitorInterval); progressMonitorInterval = null; logger.info('Progress monitor cleared'); } automationEngine.stopAutomation(); logger.info('Automation engine stopped'); const session = await sessionRepository.findById(sessionId); if (session) { session.fail('User stopped automation'); await sessionRepository.update(session); logger.info('Session marked as failed', { sessionId }); } return { success: true }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Stop automation failed', err); return { success: false, error: err.message }; } }); // Browser mode control handlers ipcMain.handle('browser-mode:get', async () => { try { const loader = container.getBrowserModeConfigLoader(); if (process.env.NODE_ENV === 'development') { return { mode: loader.getDevelopmentMode(), isDevelopment: true }; } return { mode: 'headless', isDevelopment: false }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Failed to get browser mode', err); return { mode: 'headless', isDevelopment: false }; } }); ipcMain.handle('browser-mode:set', async (_event: IpcMainInvokeEvent, mode: 'headed' | 'headless') => { try { if (process.env.NODE_ENV === 'development') { const loader = container.getBrowserModeConfigLoader(); loader.setDevelopmentMode(mode); // Ensure runtime automation wiring reflects the new browser mode if ('refreshBrowserAutomation' in container) { // Call method to refresh /use-cases that depend on browser mode container.refreshBrowserAutomation(); } logger.info('Browser mode updated', { mode }); return { success: true, mode }; } logger.warn('Browser mode change requested but not in development mode'); return { success: false, error: 'Only available in development mode' }; } catch (error) { const err = error instanceof Error ? error : new Error('Unknown error'); logger.error('Failed to set browser mode', err); return { success: false, error: err.message }; } }); // Handle overlay action requests from renderer and forward to the OverlaySyncService ipcMain.handle('overlay-action-request', async (_event: IpcMainInvokeEvent, action: OverlayAction) => { try { const overlayPort = 'getOverlaySyncPort' in container ? container.getOverlaySyncPort() : null; if (!overlayPort) { logger.warn('OverlaySyncPort not available'); return { id: action?.id ?? 'unknown', status: 'failed', reason: 'OverlaySyncPort not available' }; } const ack = await overlayPort.startAction(action); return ack; } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); logger.error('Overlay action request failed', err); const id = typeof action === 'object' && action !== null && 'id' in action ? // eslint-disable-next-line @typescript-eslint/no-explicit-any (action as { id?: string }).id ?? 'unknown' : 'unknown'; return { id, status: 'failed', reason: err.message }; } }); // Subscribe to automation adapter lifecycle events and relay to renderer try { if (!lifecycleSubscribed) { const browserAutomation = container.getBrowserAutomation(); const candidate = browserAutomation as Partial; if (typeof candidate.onLifecycle === 'function' && typeof candidate.offLifecycle === 'function') { candidate.onLifecycle((ev) => { try { if (mainWindow && mainWindow.webContents) { mainWindow.webContents.send('automation-event', ev); } } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); logger.debug?.('Failed to forward automation-event', { error }); } }); lifecycleSubscribed = true; logger.debug('Subscribed to adapter lifecycle events for renderer relay'); } else { logger.debug?.('Browser automation does not expose lifecycle events; skipping subscription'); } } } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); logger.debug?.('Failed to subscribe to adapter lifecycle events', { error }); } }