mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
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:
parent
fe5d16509a
commit
694ad8123a
7 changed files with 39 additions and 172 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue