fix: billing gate broke for scale/enterprise and downgraded users (#794)

The subscription check on plugins, connections, and usage pages was hardcoded to only look at `api_pro`. Users on scale or enterprise plans saw "Upgrade to Pro" and couldn't create plugin keys or manage connections.

Also, the check only looked at product `status` but not the `allowed` flag from autumn — so users who downgraded but were still in their billing period got locked out early.
This commit is contained in:
Prasanna721 2026-03-22 16:05:08 +00:00
parent 2ce55c43bc
commit 55ef8ab899
8 changed files with 84 additions and 78 deletions

View file

@ -1,7 +1,12 @@
"use client"
import { $fetch } from "@lib/api"
import { fetchConnectionsFeature } from "@lib/queries"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchConnectionsFeature,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
@ -51,7 +56,9 @@ interface ConnectContentProps {
export function ConnectContent({ selectedProject }: ConnectContentProps) {
const queryClient = useQueryClient()
const autumn = useCustomer()
const [isProUser, setIsProUser] = useState(false)
const { data: subscriptionStatus = DEFAULT_SUBSCRIPTION_STATUS } =
fetchSubscriptionStatus(autumn, !autumn.isLoading)
const isProUser = isAllowedFrom(subscriptionStatus, "api_pro")
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(null)
const [isUpgrading, setIsUpgrading] = useState(false)
@ -60,17 +67,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
connection: Connection | null
}>({ open: false, connection: null })
// Check Pro status
useEffect(() => {
if (!autumn.isLoading) {
setIsProUser(
autumn.customer?.products?.some(
(product) => product.id === "api_pro",
) ?? false,
)
}
}, [autumn.isLoading, autumn.customer])
const handleUpgrade = async () => {
setIsUpgrading(true)
try {

View file

@ -3,7 +3,11 @@
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { $fetch } from "@lib/api"
import { fetchSubscriptionStatus } from "@lib/queries"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
@ -181,11 +185,11 @@ export function ConnectionsDetail() {
[]) as Project[]
const {
data: status = { api_pro: { allowed: false, status: null } },
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = status.api_pro?.status !== null
const hasProProduct = isAllowedFrom(status, "api_pro")
const connectionsFeature = autumn.customer?.features?.connections
const connectionsUsed = connectionsFeature?.usage ?? 0

View file

@ -4,7 +4,11 @@ import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { fetchSubscriptionStatus } from "@lib/queries"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
@ -17,7 +21,6 @@ import {
ExternalLink,
Key,
Loader,
Plug,
Trash2,
Zap,
} from "lucide-react"
@ -31,7 +34,6 @@ import {
DialogTitle,
DialogPortal,
} from "@ui/components/dialog"
import { analytics } from "@/lib/analytics"
interface PluginInfo {
id: string
@ -108,11 +110,11 @@ export function PluginsDetail() {
const [keyCopied, setKeyCopied] = useState(false)
const {
data: status = { api_pro: { allowed: false, status: null } },
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = status.api_pro?.status !== null
const hasProProduct = isAllowedFrom(status, "api_pro")
const { data: pluginsData } = useQuery({
queryFn: async () => {
@ -181,6 +183,9 @@ export function PluginsDetail() {
credentials: "include",
})
if (!res.ok) {
if (res.status === 403) {
throw new Error("A Pro plan is required to connect plugins.")
}
const errorData = (await res.json().catch(() => ({}))) as {
message?: string
}

View file

@ -3,7 +3,11 @@
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { $fetch } from "@lib/api"
import { fetchSubscriptionStatus } from "@lib/queries"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
@ -348,13 +352,11 @@ export default function ConnectionsMCP() {
// Billing data
const {
data: status = {
api_pro: { allowed: false, status: null },
},
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = status.api_pro?.status !== null
const hasProProduct = isAllowedFrom(status, "api_pro")
// Get connections data directly from autumn customer
const connectionsFeature = autumn.customer?.features?.connections

View file

@ -1,16 +1,18 @@
import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchMemoriesFeature,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import type { useCustomer } from "autumn-js/react"
export function useMemoriesUsage(autumn: ReturnType<typeof useCustomer>) {
const {
data: status = {
api_pro: { allowed: false, status: null },
},
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const proStatus = status.api_pro
const hasProProduct = proStatus?.status !== null
const hasProProduct = isAllowedFrom(status, "api_pro")
const { data: memoriesCheck, isLoading: isLoadingMemories } =
fetchMemoriesFeature(autumn, !isCheckingStatus && !autumn.isLoading)

View file

@ -1,4 +1,8 @@
import { fetchSubscriptionStatus } from "@lib/queries"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import type { useCustomer } from "autumn-js/react"
import { calculateUsagePercent, getDaysRemaining } from "@/lib/billing-utils"
@ -6,20 +10,16 @@ export type PlanType = "free" | "pro" | "scale" | "enterprise"
export function useTokenUsage(autumn: ReturnType<typeof useCustomer>) {
const {
data: status = {
api_pro: { allowed: false, status: null },
api_scale: { allowed: false, status: null },
api_enterprise: { allowed: false, status: null },
},
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
let currentPlan: PlanType = "free"
if (status.api_enterprise?.status !== null) {
if (isAllowedFrom(status, "api_enterprise")) {
currentPlan = "enterprise"
} else if (status.api_scale?.status !== null) {
} else if (isAllowedFrom(status, "api_scale")) {
currentPlan = "scale"
} else if (status.api_pro?.status !== null) {
} else if (isAllowedFrom(status, "api_pro")) {
currentPlan = "pro"
}

View file

@ -322,11 +322,12 @@
},
"packages/tools": {
"name": "@supermemory/tools",
"version": "1.4.0",
"version": "1.4.1",
"dependencies": {
"@ai-sdk/anthropic": "^2.0.25",
"@ai-sdk/openai": "^2.0.23",
"ai": "^5.0.29",
"lru-cache": "^11.2.6",
"openai": "^4.104.0",
"supermemory": "^3.0.0-alpha.26",
"zod": "^4.1.5",
@ -3476,7 +3477,7 @@
"lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="],
@ -4934,8 +4935,6 @@
"@mastra/core/hono-openapi": ["hono-openapi@1.3.0", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig=="],
"@mastra/core/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"@mastra/core/p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="],
"@mdx-js/mdx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
@ -5198,6 +5197,8 @@
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@vanilla-extract/css/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@vanilla-extract/integration/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
@ -5470,6 +5471,8 @@
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
@ -6554,8 +6557,6 @@
"@opennextjs/cloudflare/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"@opennextjs/cloudflare/glob/path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@ -6572,8 +6573,6 @@
"@sentry/bundler-plugin-core/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"agents/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],

View file

@ -8,20 +8,41 @@ import { $fetch } from "./api"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
export const PLAN_TIERS = ["api_pro", "api_scale", "api_enterprise"] as const
export type PlanTier = (typeof PLAN_TIERS)[number]
export type SubscriptionStatusMap = Record<
string,
{ allowed: boolean; status: string | null }
>
export const DEFAULT_SUBSCRIPTION_STATUS: SubscriptionStatusMap = {
api_pro: { allowed: false, status: null },
api_scale: { allowed: false, status: null },
api_enterprise: { allowed: false, status: null },
}
export function isAllowedFrom(
status: SubscriptionStatusMap,
minimumTier: PlanTier,
): boolean {
const minIndex = PLAN_TIERS.indexOf(minimumTier)
return PLAN_TIERS.slice(minIndex).some((tier) => {
const s = status[tier]
return s?.status === "active"
})
}
export const fetchSubscriptionStatus = (
autumn: ReturnType<typeof useCustomer>,
isEnabled: boolean,
) =>
useQuery({
queryFn: async () => {
const allPlans = ["api_pro", "api_scale", "api_enterprise"]
const statusMap: Record<
string,
{ allowed: boolean; status: string | null }
> = {}
const statusMap: SubscriptionStatusMap = {}
await Promise.all(
allPlans.map(async (plan) => {
PLAN_TIERS.map(async (plan) => {
try {
const res = autumn.check({
productId: plan,
@ -83,29 +104,6 @@ export const fetchConnectionsFeature = (
enabled: isEnabled,
})
// Product checks
export const fetchApiProProduct = (autumn: ReturnType<typeof useCustomer>) =>
useQuery({
queryFn: async () => {
const res = autumn.check({ productId: "api_pro" })
return res.data
},
queryKey: ["autumn-product", "api_pro"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
})
export const fetchProProduct = (autumn: ReturnType<typeof useCustomer>) =>
useQuery({
queryFn: async () => {
const res = autumn.check({ productId: "pro" })
return res.data
},
queryKey: ["autumn-product", "pro"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
})
export const useDeleteDocument = (selectedProject: string) => {
const queryClient = useQueryClient()