From 1506a1dc318673a8a6ce6790618afdd1e96ff418 Mon Sep 17 00:00:00 2001 From: "balyan.sid@gmail.com" Date: Thu, 5 Jun 2025 15:35:29 -0700 Subject: [PATCH] Add connection status tracking to toolkit connections --- app/api/connections/initiate/route.ts | 3 + app/api/connections/status/route.ts | 63 +++++++++++ components/toolbar.tsx | 155 +++++++++++++++++++++++++- 3 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 app/api/connections/status/route.ts diff --git a/app/api/connections/initiate/route.ts b/app/api/connections/initiate/route.ts index c2f4dcc..ad35bbc 100644 --- a/app/api/connections/initiate/route.ts +++ b/app/api/connections/initiate/route.ts @@ -35,6 +35,9 @@ export async function POST(request: Request) { const connectionRequest = (await composio.connectedAccounts.initiate( session.user.id, authConfigId, + // { + // callbackUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/connections/callback`, + // }, )) as ConnectionRequestResponse; return NextResponse.json({ diff --git a/app/api/connections/status/route.ts b/app/api/connections/status/route.ts new file mode 100644 index 0000000..983df4d --- /dev/null +++ b/app/api/connections/status/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; +import { auth } from '@/app/(auth)/auth'; +import composio from '@/lib/services/composio'; +import { ChatSDKError } from '@/lib/errors'; + +// Type for the connection status response +type ConnectionStatus = { + id: string; + status: 'INITIALIZING' | 'INITIATED' | 'ACTIVE' | 'FAILED' | 'EXPIRED'; + authConfig: { + id: string; + isComposioManaged: boolean; + isDisabled: boolean; + }; + data: Record; + params?: Record; +}; + +export async function GET(request: Request) { + const session = await auth(); + + if (!session?.user?.id) { + return new ChatSDKError('unauthorized:chat').toResponse(); + } + + const { searchParams } = new URL(request.url); + const connectionId = searchParams.get('connectionId'); + + if (!connectionId) { + return new ChatSDKError( + 'bad_request:api', + 'Connection ID is required', + ).toResponse(); + } + + try { + // Wait for connection to complete (with timeout) + const connection = (await composio.connectedAccounts.waitForConnection( + connectionId, + // Optional: Add timeout configuration if supported + )) as ConnectionStatus; + + return NextResponse.json({ + id: connection.id, + status: connection.status, + authConfig: connection.authConfig, + data: connection.data, + params: connection.params, + }); + } catch (error) { + console.error('Failed to get connection status:', error); + + if (error instanceof Error && 'code' in error) { + // Handle Composio specific errors + return NextResponse.json({ error: error.message }, { status: 400 }); + } + + return NextResponse.json( + { error: 'Failed to get connection status' }, + { status: 500 }, + ); + } +} diff --git a/components/toolbar.tsx b/components/toolbar.tsx index 8d69bb4..e20ce9e 100644 --- a/components/toolbar.tsx +++ b/components/toolbar.tsx @@ -88,8 +88,91 @@ function ToolCard({ const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [isConnecting, setIsConnecting] = React.useState(false); const [isDeleting, setIsDeleting] = React.useState(false); + const [connectionStatus, setConnectionStatus] = React.useState< + 'idle' | 'connecting' | 'checking' | 'active' | 'failed' | 'expired' + >('idle'); + const [activeConnectionId, setActiveConnectionId] = React.useState< + string | null + >(null); + const pollingTimeoutRef = React.useRef(null); const { toast } = useToast(); + // Reset connection status when toolkit connection changes + React.useEffect(() => { + if (toolkit.isConnected) { + setConnectionStatus('idle'); + setActiveConnectionId(null); + } + }, [toolkit.isConnected]); + + // Cleanup on unmount + React.useEffect(() => { + return () => { + setConnectionStatus('idle'); + setActiveConnectionId(null); + // Clear any pending polling timeouts + if (pollingTimeoutRef.current) { + clearTimeout(pollingTimeoutRef.current); + } + }; + }, []); + + const checkConnectionStatus = React.useCallback( + async (connectionId: string) => { + setConnectionStatus('checking'); + try { + const response = await fetch( + `/api/connections/status?connectionId=${connectionId}`, + ); + + if (!response.ok) { + throw new Error('Failed to check connection status'); + } + + const data = await response.json(); + + switch (data.status) { + case 'ACTIVE': + setConnectionStatus('active'); + toast({ + title: 'Connection successful', + description: `${toolkit.name} has been connected successfully.`, + }); + // Refresh the toolkit list to update connection status + onConnectionDeleted?.(); + break; + case 'FAILED': + setConnectionStatus('failed'); + toast({ + title: 'Connection failed', + description: `Failed to connect ${toolkit.name}. Please try again.`, + variant: 'destructive', + }); + break; + case 'EXPIRED': + setConnectionStatus('expired'); + toast({ + title: 'Connection expired', + description: `The connection request has expired. Please try again.`, + variant: 'destructive', + }); + break; + case 'INITIALIZING': + case 'INITIATED': + // Still in progress, check again after delay + pollingTimeoutRef.current = setTimeout(() => { + checkConnectionStatus(connectionId); + }, 2000); + break; + } + } catch (error) { + console.error('Failed to check connection status:', error); + setConnectionStatus('failed'); + } + }, + [toolkit.name, onConnectionDeleted, toast, pollingTimeoutRef], + ); + const handleConnect = async () => { if (!userId) { toast({ @@ -112,6 +195,7 @@ function ToolCard({ } setIsConnecting(true); + setConnectionStatus('connecting'); try { const response = await fetch('/api/connections/initiate', { method: 'POST', @@ -126,20 +210,32 @@ function ToolCard({ } const data = await response.json(); - const { redirectUrl } = data; + const { redirectUrl, connectionId } = data; - if (redirectUrl) { + if (redirectUrl && connectionId) { + // Store the connection ID for status tracking + setActiveConnectionId(connectionId); + + // Open OAuth window window.open(redirectUrl, '_blank'); + + // Close dialog but keep tracking status + setIsDialogOpen(false); + toast({ title: 'Authorization started', description: `Complete the authorization in the opened window for ${toolkit.name}.`, }); + + // Start checking connection status after a short delay + setTimeout(() => checkConnectionStatus(connectionId), 3000); } else { toast({ title: 'Connection issue', description: 'No authorization URL received. Please try again.', variant: 'destructive', }); + setConnectionStatus('failed'); } } catch (error) { console.error('Failed to initiate connection:', error); @@ -148,9 +244,9 @@ function ToolCard({ description: `Failed to connect ${toolkit.name}. Please try again.`, variant: 'destructive', }); + setConnectionStatus('failed'); } finally { setIsConnecting(false); - setIsDialogOpen(false); } }; @@ -228,7 +324,38 @@ function ToolCard({ - {toolkit.isConnected ? ( + {/* Show different UI based on connection status */} + {connectionStatus === 'connecting' || + connectionStatus === 'checking' ? ( +
+ + +
+ ) : connectionStatus === 'active' || toolkit.isConnected ? (
+ ) : connectionStatus === 'failed' || connectionStatus === 'expired' ? ( +
+
+ {connectionStatus === 'failed' + ? 'Connection failed' + : 'Connection expired'} +
+ +
) : (