From 694ad8123aa04ee2c517e6baba219bd66ec2e012 Mon Sep 17 00:00:00 2001 From: Prasanna721 <106952318+Prasanna721@users.noreply.github.com> Date: Thu, 26 Mar 2026 05:29:53 +0000 Subject: [PATCH] fix: remove useQuery wrapper around synchronous billing checks (#805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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` --- .../components/add-document/connections.tsx | 21 +---- .../integrations/connections-detail.tsx | 15 +-- .../integrations/plugins-detail.tsx | 15 +-- .../components/settings/connections-mcp.tsx | 16 +--- apps/web/hooks/use-memories-usage.ts | 37 -------- apps/web/hooks/use-token-usage.ts | 13 +-- packages/lib/queries.ts | 94 +++++-------------- 7 files changed, 39 insertions(+), 172 deletions(-) delete mode 100644 apps/web/hooks/use-memories-usage.ts diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index 29fcbf14..84a18adb 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -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(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 diff --git a/apps/web/components/integrations/connections-detail.tsx b/apps/web/components/integrations/connections-detail.tsx index 0bbb573b..d3613400 100644 --- a/apps/web/components/integrations/connections-detail.tsx +++ b/apps/web/components/integrations/connections-detail.tsx @@ -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(["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 (
{ @@ -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 ( diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx index fb89130e..7ffafa2d 100644 --- a/apps/web/components/settings/connections-mcp.tsx +++ b/apps/web/components/settings/connections-mcp.tsx @@ -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(["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 (
diff --git a/apps/web/hooks/use-memories-usage.ts b/apps/web/hooks/use-memories-usage.ts deleted file mode 100644 index be9a5b21..00000000 --- a/apps/web/hooks/use-memories-usage.ts +++ /dev/null @@ -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) { - 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, - } -} diff --git a/apps/web/hooks/use-token-usage.ts b/apps/web/hooks/use-token-usage.ts index 21962567..af3a98c7 100644 --- a/apps/web/hooks/use-token-usage.ts +++ b/apps/web/hooks/use-token-usage.ts @@ -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) { - 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) { 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) diff --git a/packages/lib/queries.ts b/packages/lib/queries.ts index 5f6d0493..f3b4d745 100644 --- a/packages/lib/queries.ts +++ b/packages/lib/queries.ts @@ -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, - 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, - 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, - 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()