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 (