"use client" import { useAuth } from "@lib/auth-context" import { useSession } from "@lib/auth" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { useCustomer } from "autumn-js/react" import { ArrowRight, Loader, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" import { Suspense, useEffect, useState } from "react" import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" function isValidLocalhostCallback(callback: string): boolean { try { const url = new URL(callback) const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" const isHttp = url.protocol === "http:" const isCallbackPath = url.pathname === "/callback" return isLocalhost && isHttp && isCallbackPath } catch { return false } } interface PluginInfo { name: string description: string features: string[] icon: string } const PLUGIN_INFO: Record = { claude_code: { name: "Claude Code", description: "Persistent memory for Claude Code. Remembers your coding context, patterns, and decisions across sessions.", features: [ "Auto-recalls relevant context at session start", "Captures important observations from tool usage", "Builds persistent user profile from interactions", ], icon: "/images/plugins/claude-code.svg", }, opencode: { name: "OpenCode", description: "Memory layer for OpenCode. Enhances your coding assistant with long-term memory capabilities.", features: [ "Semantic search across previous sessions", "Auto-capture of coding decisions", "Context injection before each prompt", ], icon: "/images/plugins/opencode.svg", }, openclaw: { name: "OpenClaw", description: "Multi-platform memory for OpenClaw. Works across Telegram, WhatsApp, Discord, Slack and more.", features: [ "Cross-channel memory persistence", "Automatic conversation capture", "User profile building across platforms", ], icon: "/images/plugins/openclaw.svg", }, hermes: { name: "Hermes", description: "Memory layer for Hermes agent", features: [ "Semantic search across previous sessions", "Auto-capture of conversation context", "Builds persistent user profile from interactions", ], icon: "/images/plugins/hermes.svg", }, cursor: { name: "Cursor", description: "Memory layer for Cursor. Enhances your AI coding assistant with persistent context.", features: [ "Remembers coding patterns across sessions", "Auto-capture of project decisions", "Context-aware suggestions", ], icon: "/images/plugins/cursor.svg", }, } function getPluginName(client: string): string { return PLUGIN_INFO[client]?.name ?? "External Tool" } type Status = "loading" | "creating" | "success" | "error" | "upgrade" const pageWrapperClass = "flex items-center justify-center min-h-screen bg-background p-4" const cardClass = cn( "bg-[#14161A] rounded-[14px] p-6 w-full max-w-[400px]", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", ) function AuthConnectContent() { const params = useSearchParams() const router = useRouter() const { data: session, isPending } = useSession() const { org, organizations, isRestoring } = useAuth() const autumn = useCustomer() const [status, setStatus] = useState("loading") const [error, setError] = useState(null) const [isUpgrading, setIsUpgrading] = useState(false) const callback = params.get("callback") const client = params.get("client") const validClient = client && client in PLUGIN_INFO ? client : null const displayName = validClient ? getPluginName(validClient) : "External Tool" const pluginInfo = validClient ? PLUGIN_INFO[validClient] : null // Redirect new users (logged in but no organization) to onboarding. // Store the current connect URL so onboarding can redirect back here. const shouldRedirectToOnboarding = !isPending && !isRestoring && !!session && Array.isArray(organizations) && organizations.length === 0 useEffect(() => { if (isPending || isRestoring) return if (!session) return if (organizations === null) return // orgs query still pending if (organizations.length > 0) return // has orgs, nothing to do try { sessionStorage.setItem(PENDING_CONNECT_URL_KEY, window.location.href) } catch (e) { console.warn("Failed to access sessionStorage for pending connect URL", e) } router.replace("/onboarding") }, [isPending, isRestoring, session, organizations, router]) async function handleConnect() { if (!callback) { setStatus("error") setError("Missing callback parameter.") return } if (!isValidLocalhostCallback(callback)) { setStatus("error") setError("Invalid callback URL.") return } if (!session || !org) { setStatus("error") setError( "Your account is not fully set up yet. Please complete onboarding first.", ) return } try { setStatus("creating") const fetchParams = new URLSearchParams({ callback }) if (validClient) fetchParams.set("client", validClient) const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { credentials: "include", }) if (!res.ok) { if (res.status === 403) { setStatus("upgrade") return } const errorData = (await res.json().catch(() => ({}))) as { message?: string } throw new Error(errorData.message || "Failed to get API key") } const data = (await res.json()) as { key: string } setStatus("success") const redirectUrl = new URL(callback) redirectUrl.searchParams.set("apikey", data.key) window.location.href = redirectUrl.toString() } catch (err) { console.error("Failed to get API key:", err) setStatus("error") setError(err instanceof Error ? err.message : "Failed to get API key") } } async function handleUpgrade() { try { setIsUpgrading(true) const safeSuccessUrl = `${window.location.origin}${window.location.pathname}?callback=${encodeURIComponent(callback ?? "")}&client=${encodeURIComponent(validClient ?? "")}` await autumn.attach({ planId: "api_pro", successUrl: safeSuccessUrl, }) } catch (err) { console.error("Upgrade failed:", err) setIsUpgrading(false) } } // Show a spinner while session/org data is loading or while we're about // to redirect to onboarding (prevents a brief flash of the connect card). const isAuthLoading = isPending || isRestoring || organizations === null if (isAuthLoading || shouldRedirectToOnboarding) { return (
) } if (status === "loading") { return (
{pluginInfo ? ( {pluginInfo.name} ) : ( )}

Connect {displayName}

{pluginInfo?.description ?? `Allow ${displayName} to access your Supermemory account.`}

{pluginInfo && (
    {pluginInfo.features.map((feature) => (
  • {feature}
  • ))}
)}
) } if (status === "upgrade") { return (
{pluginInfo ? ( {pluginInfo.name} ) : ( )}

{pluginInfo?.name ?? displayName}

{pluginInfo?.description ?? `A paid plan is required to use ${displayName} with Supermemory.`}

{pluginInfo && (
    {pluginInfo.features.map((feature) => (
  • {feature}
  • ))}
)} View all plans
) } if (status === "error") { return (

Connection failed

{error}

Go to app
) } return (

{status === "creating" && `Connecting ${displayName}…`} {status === "success" && `Success! Redirecting back to ${displayName}…`}

) } export default function AuthConnectPage() { return (
} >
) }