From 49bf6494bfef10ef6a1bfd005ad807513e597e7c Mon Sep 17 00:00:00 2001
From: Ishaan Gupta
Date: Mon, 3 Aug 2026 18:33:31 +0530
Subject: [PATCH] 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
-
-
-
-
@@ -933,7 +430,7 @@ function AuthConnectContent() {