From 55ddc79bbdd726ff54354a5f2b2367670956d406 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Mon, 1 Sep 2025 19:43:21 +0100 Subject: [PATCH 1/8] refactored arcade integration --- .../(routes)/settings/connections/page.tsx | 169 +++++++++++++- .../connection/add-arcade-connection.tsx | 162 +++++++++++++ apps/mail/hooks/use-arcade-connection.ts | 73 ++++++ apps/server/src/main.ts | 2 + apps/server/src/routes/arcade.ts | 134 +++++++++++ apps/server/src/trpc/index.ts | 4 +- apps/server/src/trpc/routes/arcade.ts | 220 ++++++++++++++++++ 7 files changed, 756 insertions(+), 8 deletions(-) create mode 100644 apps/mail/components/connection/add-arcade-connection.tsx create mode 100644 apps/mail/hooks/use-arcade-connection.ts create mode 100644 apps/server/src/routes/arcade.ts create mode 100644 apps/server/src/trpc/routes/arcade.ts diff --git a/apps/mail/app/(routes)/settings/connections/page.tsx b/apps/mail/app/(routes)/settings/connections/page.tsx index 018a3c271a..0f0a6983e5 100644 --- a/apps/mail/app/(routes)/settings/connections/page.tsx +++ b/apps/mail/app/(routes)/settings/connections/page.tsx @@ -7,36 +7,78 @@ import { DialogTrigger, DialogClose, } from '@/components/ui/dialog'; +import { AddArcadeConnectionDialog } from '@/components/connection/add-arcade-connection'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { useArcadeConnections } from '@/hooks/use-arcade-connection'; import { SettingsCard } from '@/components/settings/settings-card'; import { AddConnectionDialog } from '@/components/connection/add'; - +import { Trash, Plus, Unplug, Sparkles } from 'lucide-react'; import { useSession, authClient } from '@/lib/auth-client'; import { useConnections } from '@/hooks/use-connections'; import { useTRPC } from '@/providers/query-provider'; import { Skeleton } from '@/components/ui/skeleton'; import { useMutation } from '@tanstack/react-query'; -import { Trash, Plus, Unplug } from 'lucide-react'; import { useThreads } from '@/hooks/use-threads'; import { useBilling } from '@/hooks/use-billing'; import { emailProviders } from '@/lib/constants'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { useState, useEffect } from 'react'; import { m } from '@/paraglide/messages'; import { useQueryState } from 'nuqs'; -import { useState } from 'react'; import { toast } from 'sonner'; export default function ConnectionsPage() { const { data, isLoading, refetch: refetchConnections } = useConnections(); + const { + connections: arcadeConnections, + isLoading: arcadeLoading, + refetch: refetchArcadeConnections, + revokeAuthorization, + } = useArcadeConnections(); const { refetch } = useSession(); const [openTooltip, setOpenTooltip] = useState(null); const trpc = useTRPC(); const { mutateAsync: deleteConnection } = useMutation(trpc.connections.delete.mutationOptions()); + const { mutateAsync: createArcadeConnection } = useMutation( + trpc.arcadeConnections.createConnection.mutationOptions(), + ); const [{ refetch: refetchThreads }] = useThreads(); const { isPro } = useBilling(); const [, setPricingDialog] = useQueryState('pricingDialog'); + const [arcadeAuthSuccess] = useQueryState('arcade_auth_success'); + const [toolkit] = useQueryState('toolkit'); + const [authId] = useQueryState('auth_id'); + const [error] = useQueryState('error'); + + useEffect(() => { + if (arcadeAuthSuccess === 'true' && toolkit && authId) { + createArcadeConnection({ toolkit, authId }) + .then(() => { + toast.success(`Successfully connected ${toolkit}`); + void refetchArcadeConnections(); + window.history.replaceState({}, document.title, window.location.pathname); + }) + .catch((err) => { + console.error('Failed to create Arcade connection:', err); + toast.error(`Failed to connect ${toolkit}`); + }); + } else if (error) { + let errorMessage = 'Authentication failed'; + if (error === 'arcade_auth_failed') { + errorMessage = 'Arcade authorization failed'; + } else if (error === 'arcade_auth_incomplete') { + errorMessage = 'Authorization was not completed'; + } else if (error === 'arcade_verification_failed') { + errorMessage = 'User verification failed'; + } else if (error === 'arcade_auth_error') { + errorMessage = 'An error occurred during authentication'; + } + toast.error(errorMessage); + window.history.replaceState({}, document.title, window.location.pathname); + } + }, [arcadeAuthSuccess, toolkit, authId, error, createArcadeConnection, refetchArcadeConnections]); const disconnectAccount = async (connectionId: string) => { await deleteConnection( { connectionId }, @@ -53,12 +95,11 @@ export default function ConnectionsPage() { void refetchThreads(); }; + console.log(arcadeConnections); + return (
- +
{isLoading ? (
@@ -228,6 +269,120 @@ export default function ConnectionsPage() {
+ + +
+ {arcadeLoading ? ( +
+ {[...Array(3)].map((n) => ( +
+
+ +
+ + +
+
+ +
+ ))} +
+ ) : arcadeConnections.length > 0 ? ( +
+ {arcadeConnections.map((connection) => ( +
+
+
+ + {connection.provider_id?.split('-')[0]} + +
+ + Connected + +
+
+
+ + + + + + + + Disconnect {connection.provider_id?.split('-')[0]} + + + Are you sure you want to disconnect this integration? + + +
+ + + + + + +
+
+
+
+ ))} +
+ ) : ( +
+ +

No integrations connected

+

+ Connect to external services to access powerful AI tools +

+
+ )} + +
+ void refetchArcadeConnections()}> + + +
+
+
); } diff --git a/apps/mail/components/connection/add-arcade-connection.tsx b/apps/mail/components/connection/add-arcade-connection.tsx new file mode 100644 index 0000000000..d58b9c050c --- /dev/null +++ b/apps/mail/components/connection/add-arcade-connection.tsx @@ -0,0 +1,162 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; +import { useArcadeConnections } from '@/hooks/use-arcade-connection'; +import { Loader2, CheckCircle2, Sparkles } from 'lucide-react'; +import { useTRPC } from '@/providers/query-provider'; +import { useMutation } from '@tanstack/react-query'; +import { GitHub } from '../icons/icons'; +import { Button } from '../ui/button'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +const toolkitIcons: Record> = { + github: GitHub, +}; + +export const AddArcadeConnectionDialog = ({ + children, + // onSuccess, +}: { + children?: React.ReactNode; + onSuccess?: () => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + const [connectingToolkit, setConnectingToolkit] = useState(null); + const { toolkits, connections, isLoading, authorizeToolkit } = useArcadeConnections(); + const trpc = useTRPC(); + const { mutateAsync: createConnection } = useMutation( + trpc.arcadeConnections.createConnection.mutationOptions(), + ); + + const handleConnect = async (toolkit: string) => { + setConnectingToolkit(toolkit); + try { + const authResult = await authorizeToolkit(toolkit.toLocaleLowerCase()); + + console.log('[AUTH RESULT]', authResult); + + if (authResult?.authUrl && authResult?.authId) { + const authWindow = window.open(authResult.authUrl, '_blank', 'width=600,height=600'); + + const checkInterval = setInterval(async () => { + if (authWindow?.closed) { + clearInterval(checkInterval); + + try { + await createConnection({ + toolkit, + authId: authResult.authId, + }); + + toast.success(`Successfully connected ${toolkit}`); + setConnectingToolkit(null); + // onSuccess?.(); + } catch { + console.log('Authorization not complete or failed'); + setConnectingToolkit(null); + } + } + }, 1000); + + setTimeout( + () => { + clearInterval(checkInterval); + setConnectingToolkit(null); + }, + 5 * 60 * 1000, + ); + } + } catch (error) { + console.error('Failed to connect toolkit:', error); + toast.error(`Failed to connect ${toolkit}`); + setConnectingToolkit(null); + } + }; + + const isConnected = (toolkit: string) => { + return connections.some((c) => c.toolkit === toolkit); + }; + + return ( + + {children} + + + Add Arcade Integration + + Connect to external services through Arcade to enhance Zero Mail with AI-powered tools + + + + {isLoading ? ( +
+ +
+ ) : toolkits.length === 0 ? ( +
+ +

No integrations available

+

Please check your Arcade API key configuration

+
+ ) : ( +
+ {toolkits.map((toolkit) => { + const Icon = toolkitIcons[toolkit.name] || Sparkles; + const connected = isConnected(toolkit.name); + + return ( +
+
+
+ +
+
+

{toolkit.name}

+

{toolkit.description}

+

+ {toolkit.toolCount} tools available +

+
+ {connected ? ( + + ) : ( + + )} +
+
+ ); + })} +
+ )} +
+
+ ); +}; diff --git a/apps/mail/hooks/use-arcade-connection.ts b/apps/mail/hooks/use-arcade-connection.ts new file mode 100644 index 0000000000..91e3a5b18b --- /dev/null +++ b/apps/mail/hooks/use-arcade-connection.ts @@ -0,0 +1,73 @@ +import { useQuery, useMutation } from '@tanstack/react-query'; +import { useTRPC } from '../providers/query-provider'; +import { useMemo } from 'react'; + +export interface ArcadeConnection { + id: string; + userId: string; + toolkit: string; + status: 'connected' | 'error'; + authorizedAt: string; + createdAt: string; + updatedAt: string; +} + +export interface ArcadeToolkit { + name: string; + description: string; + toolCount: number; + icon?: string; +} + +export function useArcadeConnections() { + const trpc = useTRPC(); + + const { data: toolkitsData, isLoading: toolkitsLoading } = useQuery( + trpc.arcadeConnections.toolkits.queryOptions(), + ); + + const toolkits = useMemo(() => { + return (toolkitsData?.toolkits || []).map((toolkit) => ({ + ...toolkit, + icon: getToolkitIcon(toolkit.name), + })); + }, [toolkitsData]); + + const { + data: connections, + isLoading: connectionsLoading, + refetch, + } = useQuery(trpc.arcadeConnections.list.queryOptions()); + + const { mutateAsync: getAuthUrl } = useMutation( + trpc.arcadeConnections.getAuthUrl.mutationOptions(), + ); + + const { mutateAsync: authorizeToolkit } = useMutation({ + mutationFn: async (toolkit: string) => { + const result = await getAuthUrl({ toolkit }); + return result; + }, + }); + + const { mutateAsync: revokeAuthorization } = useMutation( + trpc.arcadeConnections.revoke.mutationOptions(), + ); + + return { + toolkits, + connections: connections?.connections || [], + isLoading: connectionsLoading || toolkitsLoading, + refetch, + authorizeToolkit, + revokeAuthorization, + }; +} + +function getToolkitIcon(toolkit: string): string { + const icons: Record = { + github: 'github', + }; + + return icons[toolkit.toLowerCase()] || 'default'; +} diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 54d7d2f0d5..7205dbec49 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -37,6 +37,7 @@ import { defaultUserSettings } from './lib/schemas'; import { createLocalJWKSet, jwtVerify } from 'jose'; import { enableBrainFunction } from './lib/brain'; import { trpcServer } from '@hono/trpc-server'; +import { arcadeRouter } from './routes/arcade'; import { agentsMiddleware } from 'hono-agents'; import { ZeroMCP } from './routes/agent/mcp'; import { publicRouter } from './routes/auth'; @@ -706,6 +707,7 @@ const api = new Hono() .route('/ai', aiRouter) .route('/autumn', autumnApi) .route('/public', publicRouter) + .route('/arcade', arcadeRouter) .on(['GET', 'POST', 'OPTIONS'], '/auth/*', (c) => { return c.var.auth.handler(c.req.raw); }) diff --git a/apps/server/src/routes/arcade.ts b/apps/server/src/routes/arcade.ts new file mode 100644 index 0000000000..c5dae0a42f --- /dev/null +++ b/apps/server/src/routes/arcade.ts @@ -0,0 +1,134 @@ +import type { HonoContext } from '../ctx'; +import { createAuth } from '../lib/auth'; +import Arcade from '@arcadeai/arcadejs'; +import { env } from '../env'; +import { Hono } from 'hono'; + +export const arcadeRouter = new Hono() + .use('*', async (c, next) => { + // const { sessionUser } = c.var; + // c.set( + // 'customerData', + // !sessionUser + // ? null + // : { + // customerId: sessionUser.id, + // customerData: { + // name: sessionUser.name, + // email: sessionUser.email, + // }, + // }, + // ); + await next(); + }) + .get('/verify-user', async (c) => { + try { + const flowId = c.req.query('flow_id'); + + if (!flowId) { + console.error('[Arcade Verify User] Missing flow_id parameter'); + return c.json({ error: 'Missing required parameter: flow_id' }, 400); + } + + const auth = createAuth(); + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + if (!session || !session.user) { + console.error('[Arcade Verify User] No authenticated session found'); + return c.json({ error: 'Authentication required' }, 401); + } + + if (!env.ARCADE_API_KEY) { + console.error('[Arcade Verify User] ARCADE_API_KEY not configured'); + return c.json({ error: 'Arcade integration not configured' }, 500); + } + + const arcade = new Arcade({ apiKey: env.ARCADE_API_KEY }); + + try { + const result = await arcade.auth.confirmUser({ + flow_id: flowId, + user_id: session.user.id, + }); + + console.log('[Arcade Verify User] Successfully verified user', { + userId: session.user.id, + authId: result.auth_id, + user: result, + }); + + console.log('[Arcade Verify User] waiting for completion'); + + const authResponse = await arcade.auth.waitForCompletion(result.auth_id); + + console.log('[Arcade Verify User] authResponse', authResponse); + + if (authResponse.status === 'completed') { + // const { mutateAsync: createConnection } = + // trpc.arcadeConnections.createConnection.mutationOptions(); + + const toolkit = c.req.query('toolkit'); + + const params = new URLSearchParams(); + params.set('arcade_auth_success', 'true'); + if (toolkit) { + params.set('toolkit', toolkit); + } + params.set('auth_id', result.auth_id); + + const redirectUrl = `${env.VITE_PUBLIC_APP_URL}/settings/connections?${params.toString()}`; + return c.redirect(redirectUrl); + } else { + console.error('[Arcade Verify User] Authorization not completed', { + status: authResponse.status, + }); + + return c.redirect( + `${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_auth_incomplete`, + ); + } + } catch (error) { + console.error('[Arcade Verify User] Error confirming user with Arcade:', error); + + if (error && typeof error === 'object' && 'status' in error) { + const statusCode = (error as { status: number }).status; + const errorData = (error as { status: number; data?: unknown }).data; + + console.error('[Arcade Verify User] Arcade API error details:', { + statusCode, + errorData, + }); + + return c.redirect( + `${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_verification_failed`, + ); + } + + return c.redirect( + `${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_auth_error`, + ); + } + } catch (error) { + console.error('[Arcade Verify User] Unexpected error:', error); + return c.json({ error: 'Internal server error' }, 500); + } + }) + .get('/callback', async (c) => { + const success = c.req.query('success'); + const error = c.req.query('error'); + const toolkit = c.req.query('toolkit'); + + if (error) { + console.error('Arcade authorization error:', error); + return c.redirect(`${env.VITE_PUBLIC_APP_URL}/settings/connections?error=arcade_auth_failed`); + } + + const params = new URLSearchParams(); + if (success === 'true' && toolkit) { + params.set('arcade_connected', toolkit); + } + + return c.redirect(`${env.VITE_PUBLIC_APP_URL}/settings/connections?${params.toString()}`); + }); diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts index 2df9cf237a..f06559061d 100644 --- a/apps/server/src/trpc/index.ts +++ b/apps/server/src/trpc/index.ts @@ -3,9 +3,11 @@ import { cookiePreferencesRouter } from './routes/cookies'; import { connectionsRouter } from './routes/connections'; import { categoriesRouter } from './routes/categories'; import { templatesRouter } from './routes/templates'; +import { arcadeConnections } from './routes/arcade'; import { shortcutRouter } from './routes/shortcut'; import { settingsRouter } from './routes/settings'; import { getContext } from 'hono/context-storage'; +import { loggingRouter } from './routes/logging'; import { draftsRouter } from './routes/drafts'; import { labelsRouter } from './routes/label'; import { notesRouter } from './routes/notes'; @@ -17,10 +19,10 @@ import { bimiRouter } from './routes/bimi'; import type { HonoContext } from '../ctx'; import { aiRouter } from './routes/ai'; import { router } from './trpc'; -import { loggingRouter } from './routes/logging'; export const appRouter = router({ ai: aiRouter, + arcadeConnections: arcadeConnections, bimi: bimiRouter, brain: brainRouter, categories: categoriesRouter, diff --git a/apps/server/src/trpc/routes/arcade.ts b/apps/server/src/trpc/routes/arcade.ts new file mode 100644 index 0000000000..3d8aef7fde --- /dev/null +++ b/apps/server/src/trpc/routes/arcade.ts @@ -0,0 +1,220 @@ +import { createRateLimiterMiddleware, privateProcedure, router } from '../trpc'; +import type { ToolDefinition } from '@arcadeai/arcadejs/resources/tools/tools'; +import { Ratelimit } from '@upstash/ratelimit'; +import { Arcade } from '@arcadeai/arcadejs'; +import { TRPCError } from '@trpc/server'; +import { env } from '../../env'; +import { z } from 'zod'; + +const getArcadeClient = () => { + if (!env.ARCADE_API_KEY) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Arcade API key not configured', + }); + } + return new Arcade({ apiKey: env.ARCADE_API_KEY }); +}; + +interface ToolkitInfo { + name: string; + description: string; + toolCount: number; + tools: ToolDefinition[]; +} + +const authCache = new Map(); + +export const arcadeConnections = router({ + toolkits: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(60, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-toolkits-${sessionUser?.id}`, + }), + ) + .query(async () => { + try { + const arcade = getArcadeClient(); + const githubTools = await arcade.tools.list({ toolkit: 'github' }); + const linearTools = await arcade.tools.list({ toolkit: 'linear' }); + // const stripeTools = await arcade.tools.list({ toolkit: 'stripe' }); + + const allTools = [...githubTools.items, ...linearTools.items]; + + const groupedToolkits = allTools.reduce( + (acc: Record, tool: ToolDefinition) => { + const toolkitName = tool.toolkit?.name || 'default'; + if (!acc[toolkitName]) { + acc[toolkitName] = { + name: toolkitName, + description: tool.toolkit?.description || `${toolkitName} toolkit`, + toolCount: 0, + tools: [], + }; + } + acc[toolkitName].toolCount++; + acc[toolkitName].tools.push(tool); + return acc; + }, + {}, + ); + + return { toolkits: Object.values(groupedToolkits) }; + } catch (error) { + console.error('Failed to fetch Arcade toolkits:', error); + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to fetch toolkits', + }); + } + }), + + list: privateProcedure + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(60, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-connections-${sessionUser?.id}`, + }), + ) + .query(async ({ ctx }) => { + try { + const arcade = getArcadeClient(); + + const connections = await arcade.admin.userConnections.list({ user: ctx.sessionUser.id }); + + return { connections: connections.items }; + } catch (error) { + console.error('[Arcade List Connections] Unexpected error:', error); + + // Log the error details for debugging + if (error && typeof error === 'object') { + console.error('[Arcade] Error details:', { + message: (error as Error).message, + status: (error as { status?: number }).status, + data: (error as { data?: unknown }).data, + }); + } + + return { connections: [] }; + } + }), + + getAuthUrl: privateProcedure + .input(z.object({ toolkit: z.string() })) + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(30, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-auth-${sessionUser?.id}`, + }), + ) + .mutation(async ({ input, ctx }) => { + try { + const arcade = getArcadeClient(); + const authResponse = await arcade.auth.start(ctx.sessionUser.id, input.toolkit, { + providerType: 'oauth2', + scopes: [], + }); + + if (authResponse.status === 'completed') { + authCache.set(`${ctx.sessionUser.id}-${authResponse.id}`, input.toolkit); + return { status: 'completed' }; + } + + if (authResponse.id) { + authCache.set(`${ctx.sessionUser.id}-${authResponse.id}`, input.toolkit); + } + + return { + authUrl: authResponse.url || '', + authId: authResponse.id || ctx.sessionUser.id, + }; + } catch (error) { + console.error('Failed to start Arcade authorization:', error); + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to start authorization', + }); + } + }), + + createConnection: privateProcedure + .input( + z.object({ + toolkit: z.string(), + authId: z.string(), + }), + ) + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(30, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-create-${sessionUser?.id}`, + }), + ) + .mutation(async ({ input, ctx }) => { + try { + const arcade = getArcadeClient(); + + const authStatus = await arcade.auth.status({ id: input.authId }); + + if (authStatus.status !== 'completed') { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Authorization not completed or failed', + }); + } + + authCache.set(`${ctx.sessionUser.id}-${input.authId}`, input.toolkit); + + return { + success: true, + connection: { + id: `${ctx.sessionUser.id}-${input.toolkit}`, + userId: ctx.sessionUser.id, + toolkit: input.toolkit, + status: 'connected' as const, + authorizedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + }; + } catch (error) { + if (error instanceof TRPCError) throw error; + + console.error('Failed to create Arcade connection:', error); + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to create connection', + }); + } + }), + + revoke: privateProcedure + .input(z.object({ connectionId: z.string() })) + .use( + createRateLimiterMiddleware({ + limiter: Ratelimit.slidingWindow(30, '1m'), + generatePrefix: ({ sessionUser }) => `ratelimit:arcade-revoke-${sessionUser?.id}`, + }), + ) + .mutation(async ({ input, ctx }) => { + try { + const arcade = getArcadeClient(); + + await arcade.admin.userConnections.delete(input.connectionId, { + body: { + user: ctx.sessionUser.id, + provider: input.connectionId.split('-')[1], + }, + }); + + return { success: true }; + } catch (error) { + console.error('Failed to revoke Arcade authorization:', error); + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Failed to revoke authorization', + }); + } + }), +}); From 8a333d01ad18f50ed520658eedd130a7c87ce44a Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Tue, 2 Sep 2025 00:37:59 +0100 Subject: [PATCH 2/8] some ui fixes --- .../(routes)/settings/connections/page.tsx | 2 -- .../connection/add-arcade-connection.tsx | 9 ++--- apps/mail/components/icons/icons.tsx | 33 ++++++++++++------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/apps/mail/app/(routes)/settings/connections/page.tsx b/apps/mail/app/(routes)/settings/connections/page.tsx index 0f0a6983e5..dcdba30b45 100644 --- a/apps/mail/app/(routes)/settings/connections/page.tsx +++ b/apps/mail/app/(routes)/settings/connections/page.tsx @@ -95,8 +95,6 @@ export default function ConnectionsPage() { void refetchThreads(); }; - console.log(arcadeConnections); - return (
diff --git a/apps/mail/components/connection/add-arcade-connection.tsx b/apps/mail/components/connection/add-arcade-connection.tsx index d58b9c050c..e37120f592 100644 --- a/apps/mail/components/connection/add-arcade-connection.tsx +++ b/apps/mail/components/connection/add-arcade-connection.tsx @@ -10,13 +10,14 @@ import { useArcadeConnections } from '@/hooks/use-arcade-connection'; import { Loader2, CheckCircle2, Sparkles } from 'lucide-react'; import { useTRPC } from '@/providers/query-provider'; import { useMutation } from '@tanstack/react-query'; -import { GitHub } from '../icons/icons'; +import { GitHub, Linear } from '../icons/icons'; import { Button } from '../ui/button'; import { useState } from 'react'; import { toast } from 'sonner'; const toolkitIcons: Record> = { github: GitHub, + linear: Linear, }; export const AddArcadeConnectionDialog = ({ @@ -80,7 +81,7 @@ export const AddArcadeConnectionDialog = ({ }; const isConnected = (toolkit: string) => { - return connections.some((c) => c.toolkit === toolkit); + return connections.some((c) => c.provider_id!.split('-')[0] === toolkit.toLowerCase()); }; return ( @@ -105,9 +106,9 @@ export const AddArcadeConnectionDialog = ({

Please check your Arcade API key configuration

) : ( -
+
{toolkits.map((toolkit) => { - const Icon = toolkitIcons[toolkit.name] || Sparkles; + const Icon = toolkitIcons[toolkit.name.toLowerCase()] || Sparkles; const connected = isConnected(toolkit.name); return ( diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx index 0a13e7c352..628f00d659 100644 --- a/apps/mail/components/icons/icons.tsx +++ b/apps/mail/components/icons/icons.tsx @@ -170,9 +170,28 @@ export const Google = ({ className }: { className?: string }) => ( ); export const GitHub = ({ className }: { className?: string }) => ( - + GitHub - + + +); + +export const Linear = ({ className }: { className?: string }) => ( + + ); @@ -1033,15 +1052,7 @@ export const Figma = ({ className }: { className?: string }) => ( className={className} > - + From 2f2a79307338bee5769907ee7c4a2d501ff097b1 Mon Sep 17 00:00:00 2001 From: Ahmet Kilinc Date: Tue, 2 Sep 2025 11:59:43 +0100 Subject: [PATCH 3/8] ui fixes --- .../(routes)/settings/connections/page.tsx | 139 ++++++++++-------- .../connection/add-arcade-connection.tsx | 2 +- 2 files changed, 75 insertions(+), 66 deletions(-) diff --git a/apps/mail/app/(routes)/settings/connections/page.tsx b/apps/mail/app/(routes)/settings/connections/page.tsx index dcdba30b45..d3f0134cf5 100644 --- a/apps/mail/app/(routes)/settings/connections/page.tsx +++ b/apps/mail/app/(routes)/settings/connections/page.tsx @@ -9,6 +9,7 @@ import { } from '@/components/ui/dialog'; import { AddArcadeConnectionDialog } from '@/components/connection/add-arcade-connection'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { toolkitIcons } from '@/components/connection/add-arcade-connection'; import { useArcadeConnections } from '@/hooks/use-arcade-connection'; import { SettingsCard } from '@/components/settings/settings-card'; import { AddConnectionDialog } from '@/components/connection/add'; @@ -244,9 +245,9 @@ export default function ConnectionsPage() { {m['pages.settings.connections.addEmail']()} @@ -256,9 +257,9 @@ export default function ConnectionsPage() {
) : arcadeConnections.length > 0 ? (
- {arcadeConnections.map((connection) => ( -
-
-
- - {connection.provider_id?.split('-')[0]} - -
- - Connected - + {arcadeConnections.map((connection) => { + const Icon = toolkitIcons[connection.provider_id?.split('-')[0] || '']; + return ( +
+
+
+
+ +
+
+ + {connection.provider_id?.split('-')[0]} + +
+ + Connected + +
+
+ + + + + + + + Disconnect {connection.provider_id?.split('-')[0]} + + + Are you sure you want to disconnect this integration? + + +
+ + + + + + +
+
+
- - - - - - - - Disconnect {connection.provider_id?.split('-')[0]} - - - Are you sure you want to disconnect this integration? - - -
- - - - - - -
-
-
-
- ))} + ); + })}
) : (
@@ -370,9 +379,9 @@ export default function ConnectionsPage() { void refetchArcadeConnections()}> +
+ + + + + + + + + + AI Email Composer + + + Send AI-powered emails using Composio integration + + + +
+
+ + setEmailData(prev => ({ ...prev, to: e.target.value }))} + /> +
+
+ + setEmailData(prev => ({ ...prev, subject: e.target.value }))} + /> +
+
+ +