From d247ff69a6f49fd7f5b480b9232c720c76f9d476 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 26 Jun 2026 20:04:21 +0530 Subject: [PATCH] Add multi-plugin OAuth connect UI --- apps/web/app/auth/connect/page.tsx | 201 ++++++++++++++++++++++++++--- 1 file changed, 180 insertions(+), 21 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 0f96e52c0..d103e3200 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -2,8 +2,10 @@ import { useAuth } from "@lib/auth-context" import { useSession } from "@lib/auth" +import { hasActivePlan } from "@lib/queries" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" +import { isFreeTierPlugin } from "@/lib/plugin-catalog" import { useCustomer } from "autumn-js/react" import { ArrowRight, Loader, XCircle } from "lucide-react" import Image from "next/image" @@ -107,6 +109,66 @@ 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 pluginAccessError(client: string): string { + return `${getPluginName(client)} requires a Pro plan or higher.` +} + +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" | "upgrade" const pageWrapperClass = @@ -128,9 +190,30 @@ function AuthConnectContent() { const callback = params.get("callback") const client = params.get("client") + const clients = (params.get("clients") ?? "") + .split(",") + .map((value) => value.trim()) + .filter((value) => value in PLUGIN_INFO) + const requestedClients = clients.length > 0 ? clients : client ? [client] : [] + const hasClientList = params.has("clients") const validClient = client && client in PLUGIN_INFO ? client : null - const displayName = validClient ? getPluginName(validClient) : "External Tool" - const pluginInfo = validClient ? PLUGIN_INFO[validClient] : null + const displayName = formatPluginNames(requestedClients) + const pluginInfo = + requestedClients.length === 1 + ? PLUGIN_INFO[requestedClients[0] ?? ""] + : null + const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") + const eligibleClients = requestedClients.filter( + (requestedClient) => hasProProduct || isFreeTierPlugin(requestedClient), + ) + const blockedClients = requestedClients.filter( + (requestedClient) => !eligibleClients.includes(requestedClient), + ) + const needsPlanStatus = requestedClients.some( + (requestedClient) => !isFreeTierPlugin(requestedClient), + ) + const eligibleDisplayName = formatPluginNames(eligibleClients) + const blockedDisplayName = formatPluginNames(blockedClients) // Redirect new users (logged in but no organization) to onboarding. // Store the current connect URL so onboarding can redirect back here. @@ -176,8 +259,25 @@ function AuthConnectContent() { try { setStatus("creating") + if (eligibleClients.length === 0) { + const redirectUrl = new URL(callback) + redirectUrl.searchParams.set( + "errors", + encodeBase64UrlJson( + Object.fromEntries( + blockedClients.map((blockedClient) => [ + blockedClient, + pluginAccessError(blockedClient), + ]), + ), + ), + ) + window.location.href = redirectUrl.toString() + return + } + const fetchParams = new URLSearchParams({ callback }) - if (validClient) fetchParams.set("client", validClient) + fetchParams.set("client", eligibleClients[0] ?? validClient ?? "") const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { credentials: "include", @@ -198,7 +298,34 @@ function AuthConnectContent() { setStatus("success") const redirectUrl = new URL(callback) - redirectUrl.searchParams.set("apikey", data.key) + if (hasClientList) { + redirectUrl.searchParams.set( + "keys", + encodeBase64UrlJson( + Object.fromEntries( + eligibleClients.map((eligibleClient) => [ + eligibleClient, + data.key, + ]), + ), + ), + ) + if (blockedClients.length > 0) { + redirectUrl.searchParams.set( + "errors", + encodeBase64UrlJson( + Object.fromEntries( + blockedClients.map((blockedClient) => [ + blockedClient, + pluginAccessError(blockedClient), + ]), + ), + ), + ) + } + } else { + redirectUrl.searchParams.set("apikey", data.key) + } redirectUrl.searchParams.set("api_url", API_URL) window.location.href = redirectUrl.toString() } catch (err) { @@ -211,7 +338,7 @@ function AuthConnectContent() { async function handleUpgrade() { try { setIsUpgrading(true) - const safeSuccessUrl = `${window.location.origin}${window.location.pathname}?callback=${encodeURIComponent(callback ?? "")}&client=${encodeURIComponent(validClient ?? "")}` + const safeSuccessUrl = `${window.location.origin}${window.location.pathname}${window.location.search}` await autumn.attach({ planId: "api_pro", successUrl: safeSuccessUrl, @@ -224,7 +351,11 @@ 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 + const isAuthLoading = + isPending || + isRestoring || + organizations === null || + (needsPlanStatus && autumn.isLoading) if (isAuthLoading || shouldRedirectToOnboarding) { return (
@@ -238,19 +369,7 @@ function AuthConnectContent() {
-
- {pluginInfo ? ( - {pluginInfo.name} - ) : ( - - )} -
+

{pluginInfo?.description ?? - `Allow ${displayName} to access your Supermemory account.`} + `Approve one Supermemory OAuth flow for ${displayName}.`}

- {pluginInfo && ( + {blockedClients.length > 0 && ( +
+

+ {eligibleClients.length > 0 + ? `OAuth will connect ${eligibleDisplayName}. Upgrade to Pro to connect ${blockedDisplayName}.` + : `Upgrade to Pro to connect ${blockedDisplayName}.`} +

+
+ )} + + {pluginInfo ? (
    {pluginInfo.features.map((feature) => (
  • @@ -284,6 +413,36 @@ function AuthConnectContent() {
  • ))}
+ ) : ( +
    +
  • + + + Share one persistent memory layer across selected coding + agents. + +
  • +
  • + + + Recall project context, coding decisions, and prior + sessions. + +
  • +
  • + + + Keep each connected plugin ready without separate auth + steps. + +
  • +
)}