diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 0a3c548e..a557107d 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -210,6 +210,15 @@ function emitRunEvent(event: z.infer): void { } } +export function emitOAuthEvent(event: { provider: string; success: boolean; error?: string }): void { + const windows = BrowserWindow.getAllWindows(); + for (const win of windows) { + if (!win.isDestroyed() && win.webContents) { + win.webContents.send('oauth:didConnect', event); + } + } +} + let runsWatcher: (() => void) | null = null; export async function startRunsWatcher(): Promise { if (runsWatcher) { diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index 006c383d..3e694daa 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -9,6 +9,7 @@ import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js'; import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js'; import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js'; import { triggerSync as triggerFirefliesSync } from '@x/core/dist/knowledge/sync_fireflies.js'; +import { emitOAuthEvent } from './ipc.js'; const REDIRECT_URI = 'http://localhost:8080/oauth/callback'; @@ -160,8 +161,13 @@ export async function connectProvider(provider: string): Promise<{ success: bool } else if (provider === 'fireflies-ai') { triggerFirefliesSync(); } + + // Emit success event to renderer + emitOAuthEvent({ provider, success: true }); } catch (error) { console.error('OAuth token exchange failed:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + emitOAuthEvent({ provider, success: false, error: errorMessage }); throw error; } finally { // Clean up @@ -178,6 +184,7 @@ export async function connectProvider(provider: string): Promise<{ success: bool console.log(`[OAuth] Cleaning up abandoned OAuth flow for ${provider} (timeout)`); activeFlows.delete(state); server.close(); + emitOAuthEvent({ provider, success: false, error: 'OAuth flow timed out' }); } }, 5 * 60 * 1000); // 5 minutes diff --git a/apps/x/apps/renderer/src/components/connectors-popover.tsx b/apps/x/apps/renderer/src/components/connectors-popover.tsx index cc8ddc28..145997dd 100644 --- a/apps/x/apps/renderer/src/components/connectors-popover.tsx +++ b/apps/x/apps/renderer/src/components/connectors-popover.tsx @@ -127,6 +127,32 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) } }, [open, providers, refreshAllStatuses]) + // Listen for OAuth completion events + useEffect(() => { + const cleanup = window.ipc.on('oauth:didConnect', (event) => { + const { provider, success, error } = event + + setProviderStates(prev => ({ + ...prev, + [provider]: { + isConnected: success, + isLoading: false, + isConnecting: false, + } + })) + + if (success) { + toast(`Successfully connected to ${provider}`, 'success') + // Refresh status to ensure consistency + refreshAllStatuses() + } else { + toast(error || `Failed to connect to ${provider}`, 'error') + } + }) + + return cleanup + }, [refreshAllStatuses]) + // Connect to a provider const handleConnect = useCallback(async (provider: string) => { setProviderStates(prev => ({ @@ -138,18 +164,10 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps) const result = await window.ipc.invoke('oauth:connect', { provider }) if (result.success) { - toast(`Successfully connected to ${provider}`, 'success') - // Refresh the status after successful connection - const checkResult = await window.ipc.invoke('oauth:is-connected', { provider }) - setProviderStates(prev => ({ - ...prev, - [provider]: { - isConnected: checkResult.isConnected, - isLoading: false, - isConnecting: false, - } - })) + // OAuth flow started - keep isConnecting state, wait for event + // Event listener will handle the actual completion } else { + // Immediate failure (e.g., couldn't start flow) toast(result.error || `Failed to connect to ${provider}`, 'error') setProviderStates(prev => ({ ...prev, diff --git a/apps/x/apps/renderer/src/hooks/useOAuth.ts b/apps/x/apps/renderer/src/hooks/useOAuth.ts index 10d69e6b..b2777c6c 100644 --- a/apps/x/apps/renderer/src/hooks/useOAuth.ts +++ b/apps/x/apps/renderer/src/hooks/useOAuth.ts @@ -9,11 +9,6 @@ export function useOAuth(provider: string) { const [isLoading, setIsLoading] = useState(true); const [isConnecting, setIsConnecting] = useState(false); - // Check connection status on mount and when provider changes - useEffect(() => { - checkConnection(); - }, [provider]); - const checkConnection = useCallback(async () => { try { setIsLoading(true); @@ -27,23 +22,52 @@ export function useOAuth(provider: string) { } }, [provider]); + // Check connection status on mount and when provider changes + useEffect(() => { + checkConnection(); + }, [provider, checkConnection]); + + // Listen for OAuth completion events + useEffect(() => { + const cleanup = window.ipc.on('oauth:didConnect', (event) => { + if (event.provider !== provider) { + return; // Ignore events for other providers + } + + setIsConnected(event.success); + setIsConnecting(false); + setIsLoading(false); + + if (event.success) { + toast(`Successfully connected to ${provider}`, 'success'); + // Refresh connection status to ensure consistency + checkConnection(); + } else { + toast(event.error || `Failed to connect to ${provider}`, 'error'); + } + }); + + return cleanup; + }, [provider, checkConnection]); + const connect = useCallback(async () => { try { setIsConnecting(true); const result = await window.ipc.invoke('oauth:connect', { provider }); if (result.success) { - toast(`Successfully connected to ${provider}`, 'success'); - await checkConnection(); + // OAuth flow started - keep isConnecting state, wait for event + // Event listener will handle the actual completion } else { + // Immediate failure (e.g., couldn't start flow) toast(result.error || `Failed to connect to ${provider}`, 'error'); + setIsConnecting(false); } } catch (error) { console.error('Failed to connect:', error); toast(`Failed to connect to ${provider}`, 'error'); - } finally { setIsConnecting(false); } - }, [provider, checkConnection]); + }, [provider]); const disconnect = useCallback(async () => { try { diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 2f6e2f49..8e3d2c1f 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -209,6 +209,14 @@ const ipcSchemas = { providers: z.array(z.string()), }), }, + 'oauth:didConnect': { + req: z.object({ + provider: z.string(), + success: z.boolean(), + error: z.string().optional(), + }), + res: z.null(), + }, 'granola:getConfig': { req: z.null(), res: z.object({