From 7ba9cef17c27e9e371e4475e177d08add3a0e1c1 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 | 321 +++++++++++++++++++++++++++-- 1 file changed, 299 insertions(+), 22 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index d123c3cd..d103e320 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -2,9 +2,12 @@ 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 { ArrowRight, XCircle } from "lucide-react" +import { isFreeTierPlugin } from "@/lib/plugin-catalog" +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" @@ -106,7 +109,67 @@ function getPluginName(client: string): string { return PLUGIN_INFO[client]?.name ?? "External Tool" } -type Status = "loading" | "creating" | "success" | "error" +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 = "flex items-center justify-center min-h-screen bg-background p-4" @@ -120,14 +183,37 @@ 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 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. @@ -173,14 +259,35 @@ 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", }) if (!res.ok) { + if (res.status === 403) { + setStatus("upgrade") + return + } const errorData = (await res.json().catch(() => ({}))) as { message?: string } @@ -191,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) { @@ -201,9 +335,27 @@ function AuthConnectContent() { } } + async function handleUpgrade() { + try { + setIsUpgrading(true) + const safeSuccessUrl = `${window.location.origin}${window.location.pathname}${window.location.search}` + 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 + const isAuthLoading = + isPending || + isRestoring || + organizations === null || + (needsPlanStatus && autumn.isLoading) if (isAuthLoading || shouldRedirectToOnboarding) { return (
@@ -217,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) => (
  • @@ -263,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. + +
  • +
)} + + + View all plans + +
+
+
+ ) + } + if (status === "error") { return (
From d7abd334556a090532635b92687a7767ec278386 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 3 Jul 2026 16:24:07 +0530 Subject: [PATCH 2/9] Refine multi-plugin OAuth upgrade UI --- apps/web/app/auth/connect/page.tsx | 377 +++++++++++++++++++++++------ 1 file changed, 298 insertions(+), 79 deletions(-) diff --git a/apps/web/app/auth/connect/page.tsx b/apps/web/app/auth/connect/page.tsx index d103e320..7523f695 100644 --- a/apps/web/app/auth/connect/page.tsx +++ b/apps/web/app/auth/connect/page.tsx @@ -7,10 +7,17 @@ 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 { ArrowRight, Check, Loader, XCircle } from "lucide-react" import Image from "next/image" import { useRouter, useSearchParams } from "next/navigation" -import { Suspense, useEffect, useState } from "react" +import { + Suspense, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react" import { PENDING_CONNECT_URL_KEY } from "@/lib/constants" @@ -169,6 +176,81 @@ 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 ( +
+
+ {eligible ? ( + + ) : ( + + PRO + + )} +
+ {plugin && ( + + )} +
+

+ {getPluginName(id)} +

+

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

+
+
+ ) + })} +
+
+ ) +} type Status = "loading" | "creating" | "success" | "error" | "upgrade" const pageWrapperClass = @@ -187,31 +269,56 @@ function AuthConnectContent() { const [status, setStatus] = useState("loading") const [error, setError] = useState(null) const [isUpgrading, setIsUpgrading] = useState(false) + const hasAutoConnectedAfterUpgrade = useRef(false) 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 clientsParam = params.get("clients") const hasClientList = params.has("clients") - const validClient = client && client in PLUGIN_INFO ? client : null + 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((value) => value in PLUGIN_INFO)), + ), + [rawRequestedClients], + ) + const invalidClients = useMemo( + () => rawRequestedClients.filter((value) => !(value in PLUGIN_INFO)), + [rawRequestedClients], + ) + const validClient = requestedClients[0] ?? null const displayName = formatPluginNames(requestedClients) const pluginInfo = - requestedClients.length === 1 - ? PLUGIN_INFO[requestedClients[0] ?? ""] + requestedClients.length === 1 && validClient + ? PLUGIN_INFO[validClient] : null const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") - const eligibleClients = requestedClients.filter( - (requestedClient) => hasProProduct || isFreeTierPlugin(requestedClient), + const eligibleClients = useMemo( + () => + requestedClients.filter( + (requestedClient) => hasProProduct || isFreeTierPlugin(requestedClient), + ), + [hasProProduct, requestedClients], ) - const blockedClients = requestedClients.filter( - (requestedClient) => !eligibleClients.includes(requestedClient), + 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) @@ -238,7 +345,7 @@ function AuthConnectContent() { router.replace("/onboarding") }, [isPending, isRestoring, session, organizations, router]) - async function handleConnect() { + const handleConnect = useCallback(async () => { if (!callback) { setStatus("error") setError("Missing callback parameter.") @@ -249,6 +356,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( @@ -260,37 +377,37 @@ 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), - ]), - ), - ), + 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.", ) - window.location.href = redirectUrl.toString() return } const fetchParams = new URLSearchParams({ callback }) - fetchParams.set("client", eligibleClients[0] ?? validClient ?? "") + fetchParams.set("client", eligibleClients[0] ?? "") 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 } + 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") } @@ -333,12 +450,26 @@ function AuthConnectContent() { setStatus("error") setError(err instanceof Error ? err.message : "Failed to get API key") } - } + }, [ + blockedClients, + blockedDisplayName, + callback, + eligibleClients, + eligibleDisplayName, + hasClientList, + invalidClients, + org, + requestedClients.length, + session, + shouldAutoConnectAfterUpgrade, + ]) async function handleUpgrade() { try { setIsUpgrading(true) - const safeSuccessUrl = `${window.location.origin}${window.location.pathname}${window.location.search}` + 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, @@ -356,6 +487,37 @@ function AuthConnectContent() { 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, + ]) + + 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 (
@@ -384,21 +546,22 @@ function AuthConnectContent() { )} > {pluginInfo?.description ?? - `Approve one Supermemory OAuth flow for ${displayName}.`} + (requestedClients.length > 1 + ? "Use one Supermemory account across these plugins." + : `Use your Supermemory account with ${displayName}.`)}

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

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

-
+ {(requestedClients.length > 1 || blockedClients.length > 0) && ( + )} - {pluginInfo ? ( + {requestedClients.length <= 1 && + blockedClients.length === 0 && + pluginInfo ? (
    {pluginInfo.features.map((feature) => (
  • @@ -413,7 +576,7 @@ function AuthConnectContent() {
  • ))}
- ) : ( + ) : requestedClients.length <= 1 && blockedClients.length === 0 ? (
  • @@ -443,28 +606,91 @@ function AuthConnectContent() {
- )} + ) : null} - + ) : ( + )} - 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 fee715bcb6c5373efe022be2ee410f02128cc06b 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 ee96b38bfdb623f2e00980f37b8c793bd10c743e 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 eb480d90f287fc1b3e56c599ecf86dc31a1049b5 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 fbc8cf28d6914f3e89bf11e2692f7a5252f3e183 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 1263c29b83b1e2f437d3202e2e3f8965be1378a0 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 d261ff191c60f03025edea4c6563961e28ac8f29 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() {