diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx new file mode 100644 index 00000000..e21b294c --- /dev/null +++ b/apps/web/app/auth/connect/page.tsx @@ -0,0 +1,432 @@ +"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 { useSearchParams } from "next/navigation" +import { Suspense, useState } from "react" + +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", + }, + clawdbot: { + name: "ClawdBot", + 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/clawdbot.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 { data: session, isPending } = useSession() + const { org } = 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 + + 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) 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({ + productId: "api_pro", + successUrl: safeSuccessUrl, + }) + } catch (err) { + console.error("Upgrade failed:", err) + setIsUpgrading(false) + } + } + + if (isPending) { + 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 ( + +
+
+ } + > + +
+ ) +}