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 - -
- -
- - - View billing - -
@@ -933,7 +430,7 @@ function AuthConnectContent() {