import { $fetch } from "@lib/api" import { authClient } from "@lib/auth" import { useAuth } from "@lib/auth-context" import { generateId } from "@lib/generate-id" import { ADD_MEMORY_SHORTCUT_URL, RAYCAST_EXTENSION_URL, SEARCH_MEMORY_SHORTCUT_URL, } from "@repo/lib/constants" import { fetchConnectionsFeature } from "@repo/lib/queries" import { Button } from "@repo/ui/components/button" import { Dialog, DialogContent, DialogHeader, DialogPortal, DialogTitle, } from "@repo/ui/components/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@repo/ui/components/dropdown-menu" import { Input } from "@repo/ui/components/input" import { Skeleton } from "@repo/ui/components/skeleton" import type { ConnectionResponseSchema } from "@repo/validation/api" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { Check, ChevronDown, Copy, DownloadIcon, FolderIcon, KeyIcon, Loader, Plus, Smartphone, Trash2, } from "lucide-react" import { motion } from "motion/react" import Image from "next/image" import { useSearchParams } from "next/navigation" import { startTransition, useEffect, useId, useMemo, useState } from "react" import { toast } from "sonner" import type { z } from "zod" import { analytics } from "@/lib/analytics" import { useProject } from "@/stores" type Connection = z.infer interface Project { id: string name: string containerTag: string createdAt: string updatedAt: string isExperimental?: boolean } const CONNECTORS = { "google-drive": { title: "Google Drive", description: "Connect your Google Docs, Sheets, and Slides", icon: GoogleDrive, }, notion: { title: "Notion", description: "Import your Notion pages and databases", icon: Notion, }, onedrive: { title: "OneDrive", description: "Access your Microsoft Office documents", icon: OneDrive, }, "more-coming": { title: "More Coming Soon", description: "Additional integrations are in development", icon: () => ( More Coming Soon Icon ), }, } as const type ConnectorProvider = keyof typeof CONNECTORS const COMING_SOON_CONNECTOR = "more-coming" as const const ChromeIcon = ({ className }: { className?: string }) => ( Google Chrome Icon ) const getRelativeTime = (timestamp: number): string => { const now = Date.now() const diff = now - timestamp const minutes = Math.floor(diff / (1000 * 60)) const hours = Math.floor(diff / (1000 * 60 * 60)) const days = Math.floor(diff / (1000 * 60 * 60 * 24)) if (minutes < 1) return "Just now" if (minutes < 60) return `${minutes}m ago` if (hours < 24) return `${hours}h ago` if (days < 7) return `${days}d ago` return new Date(timestamp).toLocaleDateString() } export function IntegrationsView() { const { org } = useAuth() const queryClient = useQueryClient() const { selectedProject } = useProject() const autumn = useCustomer() const searchParams = useSearchParams() const [showApiKeyModal, setShowApiKeyModal] = useState(false) const [apiKey, setApiKey] = useState("") const [copied, setCopied] = useState(false) const [isProUser, setIsProUser] = useState(false) const [selectedShortcutType, setSelectedShortcutType] = useState< "add" | "search" | null >(null) const [showRaycastApiKeyModal, setShowRaycastApiKeyModal] = useState(false) const [raycastApiKey, setRaycastApiKey] = useState("") const [raycastCopied, setRaycastCopied] = useState(false) const [hasTriggeredRaycast, setHasTriggeredRaycast] = useState(false) const [selectedProjectForConnection, setSelectedProjectForConnection] = useState>({}) const [showCreateProjectForm, setShowCreateProjectForm] = useState(false) const [newProjectName, setNewProjectName] = useState("") const [creatingProjectForConnector, setCreatingProjectForConnector] = useState(null) const [connectingProvider, setConnectingProvider] = useState( null, ) const apiKeyId = useId() const raycastApiKeyId = useId() const handleUpgrade = async () => { try { await autumn.attach({ productId: "consumer_pro", successUrl: "https://app.supermemory.ai/", }) window.location.reload() } catch (error) { console.error(error) } } useEffect(() => { if (!autumn.isLoading) { setIsProUser( autumn.customer?.products.some( (product) => product.id === "consumer_pro", ) ?? false, ) } }, [autumn.isLoading, autumn.customer]) const { data: connectionsCheck } = fetchConnectionsFeature( autumn, !autumn.isLoading, ) const connectionsUsed = connectionsCheck?.balance ?? 0 const connectionsLimit = connectionsCheck?.included_usage ?? 0 const canAddConnection = connectionsUsed < connectionsLimit const { data: connections = [], isLoading: connectionsLoading, error: connectionsError, } = useQuery({ queryKey: ["connections"], queryFn: async () => { const response = await $fetch("@post/connections/list", { body: { containerTags: [], }, }) if (response.error) { throw new Error(response.error?.message || "Failed to load connections") } return response.data as Connection[] }, staleTime: 30 * 1000, refetchInterval: 60 * 1000, refetchIntervalInBackground: true, }) const { data: projects = [] } = useQuery({ queryKey: ["projects"], queryFn: async () => { const response = await $fetch("@get/projects") if (response.error) { throw new Error(response.error?.message || "Failed to load projects") } return response.data?.projects || [] }, staleTime: 30 * 1000, }) useEffect(() => { if (connectionsError) { toast.error("Failed to load connections", { description: connectionsError instanceof Error ? connectionsError.message : "Unknown error", }) } }, [connectionsError]) useEffect(() => { if (selectedProject) { setSelectedProjectForConnection((prev) => { const updatedProjects = { ...prev } let hasChanges = false Object.keys(CONNECTORS).forEach((provider) => { if (!updatedProjects[provider]) { updatedProjects[provider] = selectedProject hasChanges = true } }) return hasChanges ? updatedProjects : prev }) } }, [selectedProject]) const addConnectionMutation = useMutation({ mutationFn: async (provider: ConnectorProvider) => { if (provider === COMING_SOON_CONNECTOR) { throw new Error("This integration is coming soon!") } if (provider === "google-drive") { throw new Error( "Google Drive connections are temporarily disabled. This will be resolved soon.", ) } if (!canAddConnection && !isProUser) { throw new Error( "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.", ) } const projectToUse = selectedProjectForConnection[provider] || selectedProject const response = await $fetch("@post/connections/:provider", { params: { provider }, body: { redirectUrl: window.location.href, containerTags: [projectToUse], }, }) // biome-ignore lint/style/noNonNullAssertion: its fine if ("data" in response && !("error" in response.data!)) { return response.data } throw new Error(response.error?.message || "Failed to connect") }, onSuccess: (data, provider) => { analytics.connectionAdded(provider) analytics.connectionAuthStarted() if (data?.authLink) { setConnectingProvider(provider) window.location.href = data.authLink } else { setConnectingProvider(null) } }, onError: (error, provider) => { analytics.connectionAuthFailed() setConnectingProvider(null) toast.error(`Failed to connect ${provider}`, { description: error instanceof Error ? error.message : "Unknown error", }) }, }) const deleteConnectionMutation = useMutation({ mutationFn: async (connectionId: string) => { await $fetch(`@delete/connections/${connectionId}`) }, onSuccess: () => { analytics.connectionDeleted() toast.success( "Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.", ) queryClient.invalidateQueries({ queryKey: ["connections"] }) }, onError: (error) => { toast.error("Failed to remove connection", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) const createProjectMutation = useMutation({ mutationFn: async (name: string) => { const response = await $fetch("@post/projects", { body: { name }, }) if (response.error) { throw new Error(response.error?.message || "Failed to create project") } return response.data }, onSuccess: (newProject) => { toast.success("Project created successfully!") startTransition(() => { setNewProjectName("") setShowCreateProjectForm(false) if (newProject?.containerTag && creatingProjectForConnector) { setSelectedProjectForConnection((prev) => ({ ...prev, [creatingProjectForConnector]: newProject.containerTag, })) } setCreatingProjectForConnector(null) }) queryClient.invalidateQueries({ queryKey: ["projects"] }) }, onError: (error) => { toast.error("Failed to create project", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) const createApiKeyMutation = useMutation({ mutationFn: async () => { const res = await authClient.apiKey.create({ metadata: { organizationId: org?.id, type: "ios-shortcut", }, name: `ios-${generateId().slice(0, 8)}`, prefix: `sm_${org?.id}_`, }) return res.key }, onSuccess: (apiKey) => { setApiKey(apiKey) setShowApiKeyModal(true) setCopied(false) handleCopyApiKey(apiKey) }, onError: (error) => { toast.error("Failed to create API key", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) const createRaycastApiKeyMutation = useMutation({ mutationFn: async () => { if (!org?.id) { throw new Error("Organization ID is required") } const res = await authClient.apiKey.create({ metadata: { organizationId: org?.id, type: "raycast-extension", }, name: `raycast-${generateId().slice(0, 8)}`, prefix: `sm_${org?.id}_`, }) return res.key }, onSuccess: (apiKey) => { setRaycastApiKey(apiKey) setShowRaycastApiKeyModal(true) setRaycastCopied(false) handleCopyApiKey(apiKey) }, onError: (error) => { toast.error("Failed to create Raycast API key", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) useEffect(() => { const qParam = searchParams.get("q") if ( qParam === "raycast" && !hasTriggeredRaycast && !createRaycastApiKeyMutation.isPending && org?.id ) { setHasTriggeredRaycast(true) createRaycastApiKeyMutation.mutate() } }, [searchParams, hasTriggeredRaycast, createRaycastApiKeyMutation, org]) const handleShortcutClick = (shortcutType: "add" | "search") => { setSelectedShortcutType(shortcutType) createApiKeyMutation.mutate() } const handleCopyApiKey = async (apiKey: string) => { try { await navigator.clipboard.writeText(apiKey) setCopied(true) toast.success("API key copied to clipboard!") setTimeout(() => setCopied(false), 2000) } catch { toast.error("Failed to copy API key") } } const handleOpenShortcut = () => { if (!selectedShortcutType) { toast.error("No shortcut type selected") return } if (selectedShortcutType === "add") { window.open(ADD_MEMORY_SHORTCUT_URL, "_blank") } else if (selectedShortcutType === "search") { window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank") } } const handleDialogClose = (open: boolean) => { setShowApiKeyModal(open) if (!open) { // Reset state when dialog closes setSelectedShortcutType(null) setApiKey("") setCopied(false) } } const handleRaycastDialogClose = (open: boolean) => { setShowRaycastApiKeyModal(open) if (!open) { setRaycastApiKey("") setRaycastCopied(false) } } const handleRaycastClick = () => { createRaycastApiKeyMutation.mutate() } const validateProjectName = (name: string): string | null => { const trimmed = name.trim() if (!trimmed) { return "Project name is required" } if (trimmed.length < 2) { return "Project name must be at least 2 characters" } if (trimmed.length > 50) { return "Project name must be less than 50 characters" } // Allow alphanumeric, spaces, hyphens, and underscores if (!/^[a-zA-Z0-9\s\-_]+$/.test(trimmed)) { return "Project name can only contain letters, numbers, spaces, hyphens, and underscores" } return null } const handleCreateProject = () => { const validationError = validateProjectName(newProjectName) if (validationError) { toast.error(validationError) return } createProjectMutation.mutate(newProjectName.trim()) } const handleCreateProjectCancel = () => { setShowCreateProjectForm(false) setNewProjectName("") setCreatingProjectForConnector(null) } const getProjectName = (containerTag: string) => { if (containerTag === selectedProject) { return "Default Project" } const project = projects.find( (p: Project) => p.containerTag === containerTag, ) return project?.name || "Unknown Project" } const updateProjectForConnector = (provider: string, projectTag: string) => { setSelectedProjectForConnection((prev) => ({ ...prev, [provider]: projectTag, })) } const filteredProjects = useMemo( () => projects.filter( (project: Project) => project.containerTag !== selectedProject && project.name !== "Default Project", ), [projects, selectedProject], ) return (
{/* iOS Shortcuts */}

Apple shortcuts

Add memories directly from iPhone, iPad or Mac.

{/* Raycast Extension */}
Raycast Icon

Raycast Extension

Add and search memories directly from Raycast on Mac and Windows.

{/* Chrome Extension */}

Chrome Extension

Save any webpage to supermemory

Import All your Twitter Bookmarks

Bring all your chatGPT memories to Supermemory

{/* Connections Section */}
Connection Link Icon

Connections

Connect your accounts to sync document.

{!isProUser && (

Connections require a Pro subscription

)}
{/* Show upgrade prompt for free users */} {!autumn.isLoading && !isProUser && (

🔌 Connections are a Pro feature

Connect Google Drive, Notion, OneDrive and more to automatically sync your documents.

)} {/* All Connections with Status */} {connectionsLoading ? (
{Object.keys(CONNECTORS).map((_, _i) => (
))}
) : (
{Object.entries(CONNECTORS).map(([provider, config]) => { const Icon = config.icon const connection = connections.find( (conn) => conn.provider === provider, ) console.log(connection) const isConnected = !!connection const isMoreComing = provider === COMING_SOON_CONNECTOR return (

{config.title}

{isMoreComing ? (
Coming Soon
) : isConnected ? (
{connection.metadata?.syncInProgress ? ( <> Syncing... ) : (
Connected
)}
) : provider === "google-drive" ? (
Temporarily Disabled
) : (
Disconnected
)}
{isConnected && !isMoreComing && ( )}

{config.description}

{!isConnected && !isMoreComing && provider === "google-drive" && (

Google Drive connections are temporarily disabled. This will be resolved soon.

)} {isConnected && !isMoreComing && (
{connection?.email && (

Email: {connection.email}

)} {connection?.metadata?.lastSyncedAt && (

Last synced:{" "} {getRelativeTime(connection.metadata.lastSyncedAt)}

)}
)} {!isConnected && !isMoreComing && (
updateProjectForConnector( provider, selectedProject, ) } className="flex items-center gap-2" > Default Project {(selectedProjectForConnection[provider] || selectedProject) === selectedProject && ( )} {filteredProjects.length > 0 ? ( filteredProjects.map((project: Project) => ( updateProjectForConnector( provider, project.containerTag, ) } className="flex items-center gap-2" > {project.name} {(selectedProjectForConnection[provider] || selectedProject) === project.containerTag && ( )} )) ) : ( No additional projects available )} { setCreatingProjectForConnector(provider) setShowCreateProjectForm(true) }} className="flex items-center gap-2 text-muted-foreground" > Create New Project
)} {isMoreComing && (
)}
) })}
)}

More integrations are coming soon! Have a suggestion? Share it with us on{" "} X .

{/* API Key Modal */} Setup{" "} {selectedShortcutType === "add" ? "Add Memory" : selectedShortcutType === "search" ? "Search Memory" : "iOS"}{" "} Shortcut
{/* API Key Section */}
{/* Steps */}

Follow these steps:

1

Click "Add to Shortcuts" below to open the shortcut

2

Paste your API key when prompted

3

Start using your shortcut!

Setup Raycast Extension
{/* API Key Section */}
{/* Steps */}

Follow these steps:

1

Install the Raycast extension from the Raycast Store

2

Open Raycast preferences and paste your API key

3

Use "Add Memory" or "Search Memories" commands!

{ if (!open) { handleCreateProjectCancel() } setShowCreateProjectForm(open) }} > Create New Project
setNewProjectName(e.target.value)} onKeyDown={(e) => { if ( e.key === "Enter" && newProjectName.trim() && !validateProjectName(newProjectName) && !createProjectMutation.isPending ) { handleCreateProject() } }} className="w-full" autoFocus disabled={createProjectMutation.isPending} />
) }