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}
-
+ {eligibleClients.length > 0 ? (
+
+ {blockedClients.length > 0
+ ? "Approve available plugins"
+ : "Approve Connection"}
+
+
+ ) : (
+
+ {isUpgrading || autumn.isLoading ? (
+ <>
+
+ Upgrading...
+ >
+ ) : (
+ "Upgrade to Pro"
+ )}
+
+
)}
- 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 && (
+
+ {isUpgrading || autumn.isLoading ? (
+ "Opening upgrade..."
+ ) : (
+ <>
+
+ {`Upgrade to include ${blockedDisplayName}`}
+ >
+ )}
+
+ )}
+
@@ -476,19 +702,11 @@ function AuthConnectContent() {
-
- {pluginInfo ? (
-
- ) : (
-
- )}
-
+
0 ? blockedClients : requestedClients
+ }
+ />
- {pluginInfo?.description ??
+ {error ??
+ pluginInfo?.description ??
`A paid plan is required to use ${displayName} with Supermemory.`}