From e217dde97ed30b59e99f9e8180e0fbbb1c650064 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 14 Aug 2026 15:51:22 +0530 Subject: [PATCH] Add multi-plugin OAuth connect UI (#1163) Co-authored-by: Ishaan Gupta --- apps/web/app/auth/connect/page.tsx | 173 ++++++++++++++++++++++++----- 1 file changed, 143 insertions(+), 30 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index d123c3cd..febd2760 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -7,7 +7,7 @@ import { dmSans125ClassName } from "@/lib/fonts" import { ArrowRight, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" -import { Suspense, useEffect, useState } from "react" +import { Suspense, useEffect, useMemo, useState } from "react" import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" @@ -87,7 +87,7 @@ const PLUGIN_INFO: Record = { "Auto-capture of project decisions", "Context-aware suggestions", ], - icon: "/images/plugins/cursor.svg", + icon: "/images/plugins/cursor.png", }, codex: { name: "OpenAI Codex", @@ -102,10 +102,76 @@ const PLUGIN_INFO: Record = { }, } +const MULTI_PLUGIN_FEATURES = [ + "Share one persistent memory layer across selected coding agents.", + "Recall project context, coding decisions, and prior sessions.", + "Connect every selected plugin with one approval.", +] + +function isKnownPlugin(value: string): boolean { + return Object.hasOwn(PLUGIN_INFO, value) +} + function getPluginName(client: string): string { return PLUGIN_INFO[client]?.name ?? "External Tool" } +function formatPluginNames(clients: string[]): string { + const names = clients.map((id) => getPluginName(id)) + if (names.length === 0) return "External Tool" + if (names.length === 1) return names[0] ?? "External Tool" + if (names.length === 2) { + return `${names[0] ?? "External Tool"} and ${names[1] ?? "External Tool"}` + } + + return `${names.slice(0, -1).join(", ")}, and ${names.at(-1) ?? "External Tool"}` +} + +function encodeBase64UrlJson(value: Record): string { + return btoa(JSON.stringify(value)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, "") +} + +function PluginLogoStack({ clients }: { clients: string[] }) { + if (clients.length === 0) { + return ( +
+ +
+ ) + } + + return ( +
+ {clients.map((id, index) => { + const plugin = PLUGIN_INFO[id] + return ( +
+ {plugin ? ( + {plugin.name} + ) : ( + + )} +
+ ) + })} +
+ ) +} + type Status = "loading" | "creating" | "success" | "error" const pageWrapperClass = @@ -125,9 +191,29 @@ function AuthConnectContent() { 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 + const clientsParam = params.get("clients") + const hasClientList = params.has("clients") + const rawRequestedClients = useMemo( + () => + (clientsParam !== null ? clientsParam.split(",") : client ? [client] : []) + .map((value) => value.trim()) + .filter(Boolean), + [client, clientsParam], + ) + const requestedClients = useMemo( + () => Array.from(new Set(rawRequestedClients.filter(isKnownPlugin))), + [rawRequestedClients], + ) + const invalidClients = useMemo( + () => rawRequestedClients.filter((value) => !isKnownPlugin(value)), + [rawRequestedClients], + ) + const validClient = requestedClients[0] ?? null + const displayName = formatPluginNames(requestedClients) + const pluginInfo = + requestedClients.length === 1 && 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. @@ -163,6 +249,16 @@ function AuthConnectContent() { setError("Invalid callback URL.") return } + if (invalidClients.length > 0) { + setStatus("error") + setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`) + return + } + if (requestedClients.length === 0) { + setStatus("error") + setError("Invalid or missing client.") + return + } if (!session || !org) { setStatus("error") setError( @@ -174,7 +270,7 @@ function AuthConnectContent() { try { setStatus("creating") const fetchParams = new URLSearchParams({ callback }) - if (validClient) fetchParams.set("client", validClient) + fetchParams.set("client", requestedClients[0] ?? "") const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { credentials: "include", @@ -191,7 +287,21 @@ function AuthConnectContent() { setStatus("success") const redirectUrl = new URL(callback) - redirectUrl.searchParams.set("apikey", data.key) + if (hasClientList) { + redirectUrl.searchParams.set( + "keys", + encodeBase64UrlJson( + Object.fromEntries( + requestedClients.map((requestedClient) => [ + requestedClient, + data.key, + ]), + ), + ), + ) + } else { + redirectUrl.searchParams.set("apikey", data.key) + } redirectUrl.searchParams.set("api_url", API_URL) window.location.href = redirectUrl.toString() } catch (err) { @@ -204,6 +314,20 @@ function AuthConnectContent() { // 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 + + useEffect(() => { + if (status !== "loading") return + if (rawRequestedClients.length === 0) { + setStatus("error") + setError("Invalid or missing client.") + return + } + if (invalidClients.length > 0) { + setStatus("error") + setError(`Unsupported plugin requested: ${invalidClients.join(", ")}.`) + } + }, [invalidClients, rawRequestedClients.length, status]) + if (isAuthLoading || shouldRedirectToOnboarding) { return (
@@ -217,19 +341,7 @@ function AuthConnectContent() {
-
- {pluginInfo ? ( - {pluginInfo.name} - ) : ( - - )} -
+

{pluginInfo?.description ?? - `Allow ${displayName} to access your Supermemory account.`} + (requestedClients.length > 1 + ? "Use one Supermemory account across these plugins." + : `Use your Supermemory account with ${displayName}.`)}

- {pluginInfo && ( -
    - {pluginInfo.features.map((feature) => ( +
      + {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map( + (feature) => (
    • - ))} -
    - )} + ), + )} +