Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 175 additions & 11 deletions apps/mail/app/(routes)/settings/connections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,79 @@ import {
DialogTrigger,
DialogClose,
} 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';

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<string | null>(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 },
Expand All @@ -53,12 +96,11 @@ export default function ConnectionsPage() {
void refetchThreads();
};

console.log('arcadeConnections', arcadeConnections);

return (
<div className="grid gap-6">
<SettingsCard
title={m['pages.settings.connections.title']()}
description={m['pages.settings.connections.description']()}
>
<SettingsCard title="Email Connections" description="Connect your email accounts to Zero.">
<div className="space-y-6">
{isLoading ? (
<div className="grid gap-4 md:grid-cols-3">
Expand Down Expand Up @@ -205,9 +247,9 @@ export default function ConnectionsPage() {
<AddConnectionDialog>
<Button
variant="outline"
className="group relative w-9 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
className="group relative w-10 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
>
<Plus className="absolute left-2 h-4 w-4" />
<Plus className="absolute h-4 w-4 group-hover:hidden" />
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
{m['pages.settings.connections.addEmail']()}
</span>
Expand All @@ -217,9 +259,9 @@ export default function ConnectionsPage() {
<Button
onClick={() => setPricingDialog('true')}
variant="outline"
className="group relative w-9 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
className="group relative w-10 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
>
<Plus className="absolute left-2 h-4 w-4" />
<Plus className="absolute h-4 w-4 group-hover:hidden" />
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
{m['pages.settings.connections.addEmail']()}
</span>
Expand All @@ -228,6 +270,128 @@ export default function ConnectionsPage() {
</div>
</div>
</SettingsCard>

<SettingsCard
title="Arcade Integrations"
description="Connect to external services through Arcade to enhance Zero Mail with AI-powered tools."
>
<div className="space-y-6">
{arcadeLoading ? (
<div className="grid gap-4 md:grid-cols-3">
{[...Array(3)].map((n) => (
<div
key={n}
className="bg-popover flex items-center justify-between rounded-lg border p-4"
>
<div className="flex min-w-0 items-center gap-4">
<Skeleton className="h-12 w-12 rounded-lg" />
<div className="flex flex-col gap-1">
<Skeleton className="h-4 w-full lg:w-32" />
<Skeleton className="h-3 w-full lg:w-48" />
</div>
</div>
<Skeleton className="ml-4 h-8 w-8 rounded-full" />
</div>
))}
</div>
) : arcadeConnections.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2">
{arcadeConnections.map((connection) => {
const Icon = toolkitIcons[connection?.provider_id?.split('-')[0] || ''] || Sparkles;
return (
<div
key={connection?.id ?? ''}
className="bg-popover flex items-center justify-between rounded-lg border p-4"
>
<div className="flex min-w-0 items-center gap-4">
<div className="flex min-w-0 flex-row gap-4">
<div className="bg-primary/10 flex h-12 w-12 shrink-0 items-center justify-center rounded-lg">
<Icon className="h-6 w-6" />
</div>
<div className="flex flex-col gap-1">
<span className="truncate text-sm font-medium capitalize">
{connection?.provider_id?.split('-')[0]}
</span>
<div className="flex items-center gap-2">
<Badge variant="default" className="bg-green-500 text-xs">
Connected
</Badge>
</div>
</div>
</div>
</div>
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="ml-4 shrink-0 text-red-500 hover:text-red-600"
>
<Trash className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent showOverlay>
<DialogHeader>
<DialogTitle>
Disconnect {connection?.provider_id?.split('-')[0]}
</DialogTitle>
<DialogDescription>
Are you sure you want to disconnect this integration?
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-4">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
onClick={async () => {
try {
await revokeAuthorization({
connectionId: connection?.connection_id,
});
toast.success('Integration disconnected');
void refetchArcadeConnections();
} catch {
toast.error('Failed to disconnect integration');
}
}}
>
Disconnect
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</div>
);
})}
</div>
) : (
<div className="text-muted-foreground py-8 text-center">
<Sparkles className="mx-auto mb-4 h-12 w-12 opacity-50" />
<p className="text-sm">No integrations connected</p>
<p className="mt-1 text-xs">
Connect to external services to access powerful AI tools
</p>
</div>
)}

<div className="flex items-center justify-start">
<AddArcadeConnectionDialog onSuccess={() => void refetchArcadeConnections()}>
<Button
variant="outline"
className="group relative w-10 overflow-hidden duration-200 hover:w-full sm:hover:w-[32.5%]"
>
<Plus className="absolute h-4 w-4 group-hover:hidden" />
<span className="whitespace-nowrap pl-7 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
Add Integration
</span>
</Button>
</AddArcadeConnectionDialog>
</div>
</div>
</SettingsCard>
</div>
);
}
Loading
Loading