From d247ff69a6f49fd7f5b480b9232c720c76f9d476 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 26 Jun 2026 20:04:21 +0530 Subject: [PATCH 1/9] 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 0f96e52c..d103e320 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. + +
  • +
)} + ) : ( + )} - style={{ - background: - "linear-gradient(182.37deg, #0ff0d2 -91.53%, #5bd3fb -67.8%, #1e0ff0 95.17%)", - boxShadow: - "1px 1px 2px 0px #1A88FF inset, 0 2px 10px 0 rgba(5, 1, 0, 0.20)", - }} - > - Approve Connection -
- + + {eligibleClients.length > 0 && blockedClients.length > 0 && ( + + )} +
@@ -476,19 +702,11 @@ function AuthConnectContent() {
-
- {pluginInfo ? ( - {pluginInfo.name} - ) : ( - - )} -
+ 0 ? blockedClients : requestedClients + } + />

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

From a1ae2de356c7d0f6480831bbd8412eceae5a3101 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 3 Jul 2026 16:55:32 +0530 Subject: [PATCH 3/9] Move Pro badge beside blocked plugin name --- apps/web/app/auth/connect/page.tsx | 35 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 7523f695..9c32330d 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -208,18 +208,10 @@ function PluginAccessList({ "flex size-6 shrink-0 items-center justify-center rounded-md border", eligible ? "border-[#24413C] bg-[#0B1717] text-[#8BD8CB]" - : "border-[#2B3240] bg-[#11151C] text-[#8B8B8B]", + : "border-transparent bg-transparent", )} > - {eligible ? ( - - ) : ( - - PRO - - )} + {eligible && }
{plugin && ( )}
-

+

+ {getPluginName(id)} +

+ {!eligible && ( + + PRO + )} - > - {getPluginName(id)} -

+

{eligible ? "Available on your current plan" From 2c84eb099f9c8ffb9cea4b55211a1b8942f98eae Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 3 Jul 2026 20:22:17 +0530 Subject: [PATCH 4/9] Move eligible check badge beside plugin name --- apps/web/app/auth/connect/page.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 9c32330d..5e50a44c 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -203,16 +203,6 @@ function PluginAccessList({ const eligible = state === "eligible" return (

-
- {eligible && } -
{plugin && ( {getPluginName(id)}

+ {eligible && ( + + + + )} {!eligible && ( Date: Sat, 11 Jul 2026 17:44:01 +0530 Subject: [PATCH 5/9] Handle delayed plugin OAuth plan sync --- apps/web/app/auth/connect/page.tsx | 123 ++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 9 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 5e50a44c..79c6c0fe 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -7,7 +7,7 @@ import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { isFreeTierPlugin } from "@/lib/plugin-catalog" import { useCustomer } from "autumn-js/react" -import { ArrowRight, Check, Loader, XCircle } from "lucide-react" +import { ArrowRight, Check, Clock3, Loader, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" import { @@ -23,6 +23,8 @@ import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" +const UPGRADE_PLAN_SYNC_TIMEOUT_MS = 10 * 60 * 1000 +const UPGRADE_PLAN_SYNC_RETRY_MS = 3000 function isValidLocalhostCallback(callback: string): boolean { try { @@ -249,7 +251,13 @@ function PluginAccessList({
) } -type Status = "loading" | "creating" | "success" | "error" | "upgrade" +type Status = + | "loading" + | "creating" + | "success" + | "error" + | "upgrade" + | "upgrade_timeout" const pageWrapperClass = "flex items-center justify-center min-h-screen bg-background p-4" @@ -268,6 +276,8 @@ function AuthConnectContent() { const [error, setError] = useState(null) const [isUpgrading, setIsUpgrading] = useState(false) const hasAutoConnectedAfterUpgrade = useRef(false) + const upgradeSyncStartedAt = useRef(null) + const upgradeSyncRetryTimer = useRef(null) const callback = params.get("callback") const client = params.get("client") @@ -374,18 +384,37 @@ function AuthConnectContent() { try { setStatus("creating") + if ( + shouldAutoConnectAfterUpgrade && + eligibleClients.length < requestedClients.length + ) { + const startedAt = upgradeSyncStartedAt.current ?? Date.now() + upgradeSyncStartedAt.current = startedAt + + if (Date.now() - startedAt < UPGRADE_PLAN_SYNC_TIMEOUT_MS) { + hasAutoConnectedAfterUpgrade.current = false + setStatus("loading") + if (upgradeSyncRetryTimer.current !== null) { + window.clearTimeout(upgradeSyncRetryTimer.current) + } + upgradeSyncRetryTimer.current = window.setTimeout(() => { + upgradeSyncRetryTimer.current = null + void autumn.refetch?.() + }, UPGRADE_PLAN_SYNC_RETRY_MS) + return + } + + setStatus("upgrade_timeout") + setError( + "Your upgrade completed, but Pro access is still syncing. Re-authenticate from the CLI to finish connecting.", + ) + return + } if (eligibleClients.length === 0) { setStatus("upgrade") setError(`Upgrade to Pro to connect ${blockedDisplayName}.`) return } - if (shouldAutoConnectAfterUpgrade && blockedClients.length > 0) { - setStatus("upgrade") - setError( - "Your plan update is still processing. Please try again in a moment.", - ) - return - } const fetchParams = new URLSearchParams({ callback }) fetchParams.set("client", eligibleClients[0] ?? "") @@ -449,6 +478,7 @@ function AuthConnectContent() { setError(err instanceof Error ? err.message : "Failed to get API key") } }, [ + autumn, blockedClients, blockedDisplayName, callback, @@ -478,6 +508,21 @@ function AuthConnectContent() { } } + const retryUpgradeSync = useCallback(() => { + upgradeSyncStartedAt.current = Date.now() + hasAutoConnectedAfterUpgrade.current = false + setError(null) + setStatus("loading") + void autumn.refetch?.() + }, [autumn]) + + useEffect(() => { + return () => { + if (upgradeSyncRetryTimer.current !== null) { + window.clearTimeout(upgradeSyncRetryTimer.current) + } + } + }, []) // 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 = @@ -785,6 +830,66 @@ function AuthConnectContent() { ) } + if (status === "upgrade_timeout") { + return ( +
+
+
+ +
+

+ Request timed out +

+

+ {error ?? + "Your upgrade completed, but Pro access is still syncing."} +

+
+ +
+

+ Re-authenticate from the CLI to finish connecting. +

+ + npx supermemory plugin login + +
+ +
+ + + View billing + +
+
+
+
+ ) + } if (status === "error") { return (
From 45df7ba090de4a93a62cac082fc55516b46873a6 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 13 Jul 2026 14:09:07 +0530 Subject: [PATCH 6/9] Refine OAuth timeout UI --- apps/web/app/auth/connect/page.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 79c6c0fe..0dc9968f 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -7,7 +7,7 @@ import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { isFreeTierPlugin } from "@/lib/plugin-catalog" import { useCustomer } from "autumn-js/react" -import { ArrowRight, Check, Clock3, Loader, XCircle } from "lucide-react" +import { ArrowRight, Check, Loader, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" import { @@ -835,7 +835,7 @@ function AuthConnectContent() {
- +

-
-

- Re-authenticate from the CLI to finish connecting. +

+

+ Re-authenticate from the CLI to finish connecting:

- + npx supermemory plugin login
@@ -869,8 +869,10 @@ function AuthConnectContent() { onClick={retryUpgradeSync} className={cn( "w-full flex items-center justify-center rounded-[10px] h-11", - "bg-[#0D121A] border border-[#1E293B] text-[#FAFAFA]", - "text-[14px] font-medium cursor-pointer transition-colors hover:bg-[#1E293B]", + "text-[#FAFAFA] text-[14px] font-medium cursor-pointer", + "bg-[linear-gradient(182.37deg,#0ff0d2_-91.53%,#5bd3fb_-67.8%,#1e0ff0_95.17%)]", + "shadow-[1px_1px_2px_0px_#1A88FF_inset,0_2px_10px_0_rgba(5,1,0,0.20)]", + "transition-opacity hover:opacity-90", dmSans125ClassName(), )} > From 29e6ab590cf5335b9b9bbd107d55e1004cdd06d0 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 13 Jul 2026 14:49:54 +0530 Subject: [PATCH 7/9] Fix OAuth upgrade sync retry loop --- apps/web/app/auth/connect/page.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 0dc9968f..2989ee5e 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -278,6 +278,7 @@ function AuthConnectContent() { const hasAutoConnectedAfterUpgrade = useRef(false) const upgradeSyncStartedAt = useRef(null) const upgradeSyncRetryTimer = useRef(null) + const handleConnectRef = useRef<(() => Promise) | null>(null) const callback = params.get("callback") const client = params.get("client") @@ -392,14 +393,19 @@ function AuthConnectContent() { upgradeSyncStartedAt.current = startedAt if (Date.now() - startedAt < UPGRADE_PLAN_SYNC_TIMEOUT_MS) { - hasAutoConnectedAfterUpgrade.current = false setStatus("loading") if (upgradeSyncRetryTimer.current !== null) { window.clearTimeout(upgradeSyncRetryTimer.current) } upgradeSyncRetryTimer.current = window.setTimeout(() => { upgradeSyncRetryTimer.current = null - void autumn.refetch?.() + void (async () => { + try { + await autumn.refetch?.() + } finally { + void handleConnectRef.current?.() + } + })() }, UPGRADE_PLAN_SYNC_RETRY_MS) return } @@ -492,6 +498,10 @@ function AuthConnectContent() { shouldAutoConnectAfterUpgrade, ]) + useEffect(() => { + handleConnectRef.current = handleConnect + }, [handleConnect]) + async function handleUpgrade() { try { setIsUpgrading(true) @@ -510,7 +520,6 @@ function AuthConnectContent() { const retryUpgradeSync = useCallback(() => { upgradeSyncStartedAt.current = Date.now() - hasAutoConnectedAfterUpgrade.current = false setError(null) setStatus("loading") void autumn.refetch?.() From 3d49e9e0d6713429a6a36289210b33a42c6d10a4 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Wed, 29 Jul 2026 17:49:54 +0530 Subject: [PATCH 8/9] Fix OAuth upgrade sync manual retry --- apps/web/app/auth/connect/page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 2989ee5e..2e9e4bfc 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -522,7 +522,13 @@ function AuthConnectContent() { upgradeSyncStartedAt.current = Date.now() setError(null) setStatus("loading") - void autumn.refetch?.() + void (async () => { + try { + await autumn.refetch?.() + } finally { + void handleConnectRef.current?.() + } + })() }, [autumn]) useEffect(() => { From 49bf6494bfef10ef6a1bfd005ad807513e597e7c Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Mon, 3 Aug 2026 18:33:31 +0530 Subject: [PATCH 9/9] Simplify multi-plugin OAuth for free plugins --- apps/web/app/auth/connect/page.tsx | 563 ++--------------------------- 1 file changed, 30 insertions(+), 533 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index 2e9e4bfc..febd2760 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -2,29 +2,17 @@ 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, Check, Loader, XCircle } from "lucide-react" +import { ArrowRight, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" -import { - Suspense, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react" +import { Suspense, useEffect, useMemo, useState } from "react" import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" -const UPGRADE_PLAN_SYNC_TIMEOUT_MS = 10 * 60 * 1000 -const UPGRADE_PLAN_SYNC_RETRY_MS = 3000 function isValidLocalhostCallback(callback: string): boolean { try { @@ -99,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", @@ -114,6 +102,16 @@ 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" } @@ -136,10 +134,6 @@ function encodeBase64UrlJson(value: Record): string { .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 ( @@ -178,86 +172,7 @@ function PluginLogoStack({ clients }: { clients: string[] }) { ) } -function PluginAccessList({ - blockedClients, - eligibleClients, -}: { - blockedClients: string[] - eligibleClients: string[] -}) { - const rows = [ - ...eligibleClients.map((id) => ({ id, state: "eligible" as const })), - ...blockedClients.map((id) => ({ id, state: "blocked" as const })), - ] - - if (rows.length === 0) return null - - return ( -
-

- Connection summary -

-
- {rows.map(({ id, state }) => { - const plugin = PLUGIN_INFO[id] - const eligible = state === "eligible" - return ( -
- {plugin && ( - - )} -
-
-

- {getPluginName(id)} -

- {eligible && ( - - - - )} - {!eligible && ( - - PRO - - )} -
-

- {eligible - ? "Available on your current plan" - : "Upgrade required"} -

-
-
- ) - })} -
-
- ) -} -type Status = - | "loading" - | "creating" - | "success" - | "error" - | "upgrade" - | "upgrade_timeout" +type Status = "loading" | "creating" | "success" | "error" const pageWrapperClass = "flex items-center justify-center min-h-screen bg-background p-4" @@ -271,14 +186,8 @@ function AuthConnectContent() { 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 hasAutoConnectedAfterUpgrade = useRef(false) - const upgradeSyncStartedAt = useRef(null) - const upgradeSyncRetryTimer = useRef(null) - const handleConnectRef = useRef<(() => Promise) | null>(null) const callback = params.get("callback") const client = params.get("client") @@ -292,14 +201,11 @@ function AuthConnectContent() { [client, clientsParam], ) const requestedClients = useMemo( - () => - Array.from( - new Set(rawRequestedClients.filter((value) => value in PLUGIN_INFO)), - ), + () => Array.from(new Set(rawRequestedClients.filter(isKnownPlugin))), [rawRequestedClients], ) const invalidClients = useMemo( - () => rawRequestedClients.filter((value) => !(value in PLUGIN_INFO)), + () => rawRequestedClients.filter((value) => !isKnownPlugin(value)), [rawRequestedClients], ) const validClient = requestedClients[0] ?? null @@ -308,28 +214,6 @@ function AuthConnectContent() { requestedClients.length === 1 && validClient ? PLUGIN_INFO[validClient] : null - const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") - const eligibleClients = useMemo( - () => - requestedClients.filter( - (requestedClient) => hasProProduct || isFreeTierPlugin(requestedClient), - ), - [hasProProduct, requestedClients], - ) - const blockedClients = useMemo( - () => - requestedClients.filter( - (requestedClient) => !eligibleClients.includes(requestedClient), - ), - [eligibleClients, requestedClients], - ) - const needsPlanStatus = requestedClients.some( - (requestedClient) => !isFreeTierPlugin(requestedClient), - ) - const shouldAutoConnectAfterUpgrade = - params.get("upgrade_complete") === "true" - 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. @@ -354,7 +238,7 @@ function AuthConnectContent() { router.replace("/onboarding") }, [isPending, isRestoring, session, organizations, router]) - const handleConnect = useCallback(async () => { + async function handleConnect() { if (!callback) { setStatus("error") setError("Missing callback parameter.") @@ -385,45 +269,8 @@ function AuthConnectContent() { try { setStatus("creating") - if ( - shouldAutoConnectAfterUpgrade && - eligibleClients.length < requestedClients.length - ) { - const startedAt = upgradeSyncStartedAt.current ?? Date.now() - upgradeSyncStartedAt.current = startedAt - - if (Date.now() - startedAt < UPGRADE_PLAN_SYNC_TIMEOUT_MS) { - setStatus("loading") - if (upgradeSyncRetryTimer.current !== null) { - window.clearTimeout(upgradeSyncRetryTimer.current) - } - upgradeSyncRetryTimer.current = window.setTimeout(() => { - upgradeSyncRetryTimer.current = null - void (async () => { - try { - await autumn.refetch?.() - } finally { - void handleConnectRef.current?.() - } - })() - }, UPGRADE_PLAN_SYNC_RETRY_MS) - return - } - - setStatus("upgrade_timeout") - setError( - "Your upgrade completed, but Pro access is still syncing. Re-authenticate from the CLI to finish connecting.", - ) - return - } - if (eligibleClients.length === 0) { - setStatus("upgrade") - setError(`Upgrade to Pro to connect ${blockedDisplayName}.`) - return - } - const fetchParams = new URLSearchParams({ callback }) - fetchParams.set("client", eligibleClients[0] ?? "") + fetchParams.set("client", requestedClients[0] ?? "") const res = await fetch(`${API_URL}/v3/auth/key?${fetchParams}`, { credentials: "include", @@ -433,14 +280,6 @@ function AuthConnectContent() { const errorData = (await res.json().catch(() => ({}))) as { message?: string } - if (res.status === 403) { - setStatus("upgrade") - setError( - errorData.message || - `Upgrade to Pro to connect ${eligibleDisplayName}.`, - ) - return - } throw new Error(errorData.message || "Failed to get API key") } @@ -453,26 +292,13 @@ function AuthConnectContent() { "keys", encodeBase64UrlJson( Object.fromEntries( - eligibleClients.map((eligibleClient) => [ - eligibleClient, + requestedClients.map((requestedClient) => [ + requestedClient, 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) } @@ -483,85 +309,11 @@ function AuthConnectContent() { setStatus("error") setError(err instanceof Error ? err.message : "Failed to get API key") } - }, [ - autumn, - blockedClients, - blockedDisplayName, - callback, - eligibleClients, - eligibleDisplayName, - hasClientList, - invalidClients, - org, - requestedClients.length, - session, - shouldAutoConnectAfterUpgrade, - ]) - - useEffect(() => { - handleConnectRef.current = handleConnect - }, [handleConnect]) - - async function handleUpgrade() { - try { - setIsUpgrading(true) - const successParams = new URLSearchParams(params.toString()) - successParams.set("upgrade_complete", "true") - const safeSuccessUrl = `${window.location.origin}${window.location.pathname}?${successParams.toString()}` - await autumn.attach({ - planId: "api_pro", - successUrl: safeSuccessUrl, - }) - } catch (err) { - console.error("Upgrade failed:", err) - setIsUpgrading(false) - } } - const retryUpgradeSync = useCallback(() => { - upgradeSyncStartedAt.current = Date.now() - setError(null) - setStatus("loading") - void (async () => { - try { - await autumn.refetch?.() - } finally { - void handleConnectRef.current?.() - } - })() - }, [autumn]) - - useEffect(() => { - return () => { - if (upgradeSyncRetryTimer.current !== null) { - window.clearTimeout(upgradeSyncRetryTimer.current) - } - } - }, []) // 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 || - (needsPlanStatus && autumn.isLoading) - useEffect(() => { - if (!shouldAutoConnectAfterUpgrade) return - if (hasAutoConnectedAfterUpgrade.current) return - if (status !== "loading") return - if (isAuthLoading || shouldRedirectToOnboarding || !session || !org) return - - hasAutoConnectedAfterUpgrade.current = true - void handleConnect() - }, [ - shouldAutoConnectAfterUpgrade, - status, - isAuthLoading, - shouldRedirectToOnboarding, - session, - org, - handleConnect, - ]) + const isAuthLoading = isPending || isRestoring || organizations === null useEffect(() => { if (status !== "loading") return @@ -610,18 +362,9 @@ function AuthConnectContent() {

- {(requestedClients.length > 1 || blockedClients.length > 0) && ( - - )} - - {requestedClients.length <= 1 && - blockedClients.length === 0 && - pluginInfo ? ( -
    - {pluginInfo.features.map((feature) => ( +
      + {(pluginInfo?.features ?? MULTI_PLUGIN_FEATURES).map( + (feature) => (
    • - ))} -
    - ) : requestedClients.length <= 1 && blockedClients.length === 0 ? ( -
      -
    • - - - 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. - -
    • -
    - ) : null} - -
    - {eligibleClients.length > 0 ? ( - - ) : ( - + ), )} - - {eligibleClients.length > 0 && blockedClients.length > 0 && ( - - )} -
    -
-
-
- ) - } - - if (status === "upgrade") { - return ( -
-
-
- 0 ? blockedClients : requestedClients - } - /> -
-

- {pluginInfo?.name ?? displayName} -

-

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

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

- Request timed out -

-

- {error ?? - "Your upgrade completed, but Pro access is still syncing."} -

-
- -
-

- Re-authenticate from the CLI to finish connecting: -

- - npx supermemory plugin login - -
- -
- - - View billing - -
@@ -933,7 +430,7 @@ function AuthConnectContent() {