fix: remove useQuery wrapper around synchronous billing checks (#805)

#### fix: billing subscription status race condition

 - **Bug fix**: Replaced `fetchSubscriptionStatus` (useQuery wrapper) with `getSubscriptionStatus` (pure function) — fixes intermittent "Upgrade to Pro"
  failures caused by stale cached billing state
  - **Anti-pattern**: Removed `fetchConnectionsFeature` useQuery wrapper, replaced with direct `autumn.customer?.features?.connections` read
  - **Dead code**: Removed unused `useMemoriesUsage` hook and `fetchMemoriesFeature`
This commit is contained in:
Prasanna721 2026-03-26 05:29:53 +00:00
parent fe5d16509a
commit 694ad8123a
7 changed files with 39 additions and 172 deletions

View file

@ -1,12 +1,7 @@
"use client"
import { $fetch } from "@lib/api"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchConnectionsFeature,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { hasActivePlan } 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"
@ -56,9 +51,7 @@ interface ConnectContentProps {
export function ConnectContent({ selectedProject }: ConnectContentProps) {
const queryClient = useQueryClient()
const autumn = useCustomer()
const { data: subscriptionStatus = DEFAULT_SUBSCRIPTION_STATUS } =
fetchSubscriptionStatus(autumn, !autumn.isLoading)
const isProUser = isAllowedFrom(subscriptionStatus, "api_pro")
const isProUser = hasActivePlan(autumn.customer?.products, "api_pro")
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(null)
const [isUpgrading, setIsUpgrading] = useState(false)
@ -81,13 +74,9 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
}
}
// Check connections feature limits
const { data: connectionsCheck } = fetchConnectionsFeature(
autumn,
!autumn.isLoading,
)
const connectionsUsed = connectionsCheck?.balance ?? 0
const connectionsLimit = connectionsCheck?.included_usage ?? 0
const connectionsFeature = autumn.customer?.features?.connections
const connectionsUsed = connectionsFeature?.usage ?? 0
const connectionsLimit = connectionsFeature?.included_usage ?? 10
const canAddConnection = connectionsUsed < connectionsLimit
// Fetch connections

View file

@ -3,11 +3,7 @@
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { $fetch } from "@lib/api"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { hasActivePlan } 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"
@ -184,12 +180,7 @@ export function ConnectionsDetail() {
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
[]) as Project[]
const {
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = isAllowedFrom(status, "api_pro")
const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
const connectionsFeature = autumn.customer?.features?.connections
const connectionsUsed = connectionsFeature?.usage ?? 0
@ -268,7 +259,7 @@ export function ConnectionsDetail() {
}
}
const isLoading = autumn.isLoading || isCheckingStatus
const isLoading = autumn.isLoading
return (
<div

View file

@ -4,11 +4,7 @@ import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { hasActivePlan } from "@lib/queries"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
@ -109,12 +105,7 @@ export function PluginsDetail() {
})
const [keyCopied, setKeyCopied] = useState(false)
const {
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = isAllowedFrom(status, "api_pro")
const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
const { data: pluginsData } = useQuery({
queryFn: async () => {
@ -242,7 +233,7 @@ export function PluginsDetail() {
}
}
const isLoading = autumn.isLoading || isCheckingStatus
const isLoading = autumn.isLoading
const availablePlugins = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)
return (

View file

@ -3,11 +3,7 @@
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { $fetch } from "@lib/api"
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { hasActivePlan } 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"
@ -350,13 +346,7 @@ export default function ConnectionsMCP() {
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
[]) as Project[]
// Billing data
const {
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = isAllowedFrom(status, "api_pro")
const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro")
// Get connections data directly from autumn customer
const connectionsFeature = autumn.customer?.features?.connections
@ -444,7 +434,7 @@ export default function ConnectionsMCP() {
}
}
const isLoading = autumn.isLoading || isCheckingStatus
const isLoading = autumn.isLoading
return (
<div className="flex flex-col gap-8 pt-4 w-full">

View file

@ -1,37 +0,0 @@
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 = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const hasProProduct = isAllowedFrom(status, "api_pro")
const { data: memoriesCheck, isLoading: isLoadingMemories } =
fetchMemoriesFeature(autumn, !isCheckingStatus && !autumn.isLoading)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
const isLoading = autumn.isLoading || isCheckingStatus || isLoadingMemories
const usagePercent =
memoriesLimit <= 0
? 0
: Math.min(Math.max((memoriesUsed / memoriesLimit) * 100, 0), 100)
return {
memoriesUsed,
memoriesLimit,
hasProProduct,
isLoading,
usagePercent,
}
}

View file

@ -1,18 +1,11 @@
import {
DEFAULT_SUBSCRIPTION_STATUS,
fetchSubscriptionStatus,
isAllowedFrom,
} from "@lib/queries"
import { getSubscriptionStatus, isAllowedFrom } from "@lib/queries"
import type { useCustomer } from "autumn-js/react"
import { calculateUsagePercent, getDaysRemaining } from "@/lib/billing-utils"
export type PlanType = "free" | "pro" | "scale" | "enterprise"
export function useTokenUsage(autumn: ReturnType<typeof useCustomer>) {
const {
data: status = DEFAULT_SUBSCRIPTION_STATUS,
isLoading: isCheckingStatus,
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
const status = getSubscriptionStatus(autumn.customer?.products)
let currentPlan: PlanType = "free"
if (isAllowedFrom(status, "api_enterprise")) {
@ -36,7 +29,7 @@ export function useTokenUsage(autumn: ReturnType<typeof useCustomer>) {
const searchesUsed = searchesFeature?.usage ?? 0
const searchesLimit = searchesFeature?.included_usage ?? 0
const isLoading = autumn.isLoading || isCheckingStatus
const isLoading = autumn.isLoading
const tokensPercent = calculateUsagePercent(tokensUsed, tokensLimit)
const searchesPercent = calculateUsagePercent(searchesUsed, searchesLimit)

View file

@ -1,5 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import type { useCustomer } from "autumn-js/react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import type { z } from "zod"
import type { DocumentsWithMemoriesResponseSchema } from "../validation/api"
@ -16,7 +15,7 @@ export type SubscriptionStatusMap = Record<
{ allowed: boolean; status: string | null }
>
export const DEFAULT_SUBSCRIPTION_STATUS: SubscriptionStatusMap = {
const DEFAULT_SUBSCRIPTION_STATUS: SubscriptionStatusMap = {
api_pro: { allowed: false, status: null },
api_scale: { allowed: false, status: null },
api_enterprise: { allowed: false, status: null },
@ -33,76 +32,27 @@ export function isAllowedFrom(
})
}
export const fetchSubscriptionStatus = (
autumn: ReturnType<typeof useCustomer>,
isEnabled: boolean,
) =>
useQuery({
queryFn: async () => {
const statusMap: SubscriptionStatusMap = {}
export function getSubscriptionStatus(
products: Array<{ id: string; status: string }> | undefined,
): SubscriptionStatusMap {
const statusMap: SubscriptionStatusMap = { ...DEFAULT_SUBSCRIPTION_STATUS }
if (!products) return statusMap
for (const tier of PLAN_TIERS) {
const product = products.find((p) => p.id === tier)
statusMap[tier] = {
allowed: product?.status === "active",
status: product?.status ?? null,
}
}
return statusMap
}
await Promise.all(
PLAN_TIERS.map(async (plan) => {
try {
const res = autumn.check({
productId: plan,
})
const allowed = res.data?.allowed ?? false
const product = autumn.customer?.products?.find(
(p) => p.id === plan,
)
const productStatus = product?.status ?? null
statusMap[plan] = {
allowed,
status: productStatus,
}
} catch (error) {
console.error(`Error checking status for ${plan}:`, error)
statusMap[plan] = { allowed: false, status: null }
}
}),
)
return statusMap
},
queryKey: ["subscription-status"],
refetchInterval: 60 * 1000, // Refetch every 1 minute
staleTime: 55 * 1000, // Consider data stale after 55 seconds
enabled: isEnabled,
})
// Feature checks
export const fetchMemoriesFeature = (
autumn: ReturnType<typeof useCustomer>,
isEnabled: boolean,
) =>
useQuery({
queryFn: async () => {
const res = autumn.check({ featureId: "memories" })
return res.data
},
queryKey: ["autumn-feature", "memories"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
enabled: isEnabled,
})
export const fetchConnectionsFeature = (
autumn: ReturnType<typeof useCustomer>,
isEnabled: boolean,
) =>
useQuery({
queryFn: async () => {
const res = autumn.check({ featureId: "connections" })
return res.data
},
queryKey: ["autumn-feature", "connections"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
enabled: isEnabled,
})
export function hasActivePlan(
products: Array<{ id: string; status: string }> | undefined,
minimumTier: PlanTier,
): boolean {
return isAllowedFrom(getSubscriptionStatus(products), minimumTier)
}
export const useDeleteDocument = (selectedProject: string) => {
const queryClient = useQueryClient()