supermemory/apps/web/hooks/use-token-usage.ts
MaheshtheDev 53c971ac85 feat(billing): add Max plan to nova with cancel/resume flow (#1024)
Integrate the new api_max plan ($100/mo, $130 credits) from mono into the
nova web app, mirroring mono's tier semantics.

- Tiers: insert api_max between pro and scale; resolves to Pro-tier for feature gates while displaying as "Max" (queries.ts, use-token-usage)
- Billing page: Max plan card ("Most Popular"), checkout/cancel/invoice wiring; advanced carousel page now 3-up (Max/Scale/Enterprise); Scale builds on Max
- Carousel auto-opens to the page holding the current plan (Max/Scale/ Enterprise users land on their own card instead of Free+Pro)
- Cancel: retention dialog listing what you lose + type-to-confirm "CANCEL"
- Cancel/resume state mirroring console: detect canceledAt-scheduled cancellation, show "Cancelling" pill + "Cancels on {date} · N days left", and a "Resume plan" (uncancel) action
2026-05-31 19:10:17 +00:00

94 lines
2.4 KiB
TypeScript

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" | "max" | "scale" | "enterprise"
export const PLAN_DISPLAY_NAMES: Record<PlanType, string> = {
free: "Free",
pro: "Pro",
max: "Max",
scale: "Scale",
enterprise: "Enterprise",
}
/** Higher rank sorts first in org lists (enterprise at top). */
export const PLAN_RANK: Record<PlanType, number> = {
free: 0,
pro: 1,
max: 2,
scale: 3,
enterprise: 4,
}
export function normalizePlanType(raw: unknown): PlanType {
if (typeof raw !== "string" || !raw.trim()) return "free"
const normalized = raw.toLowerCase().replace(/^api_/, "")
if (normalized === "enterprise") return "enterprise"
if (normalized === "scale") return "scale"
if (normalized === "max") return "max"
if (normalized === "pro") return "pro"
return "free"
}
const TOKEN_METER_IDS = [
"sm_tokens_text",
"sm_tokens_rich",
"sm_superrag_text",
"sm_superrag_rich",
] as const
export function useTokenUsage(autumn: ReturnType<typeof useCustomer>) {
const status = getSubscriptionStatus(autumn.data?.subscriptions)
let currentPlan: PlanType = "free"
if (isAllowedFrom(status, "api_enterprise")) {
currentPlan = "enterprise"
} else if (isAllowedFrom(status, "api_scale")) {
currentPlan = "scale"
} else if (isAllowedFrom(status, "api_max")) {
currentPlan = "max"
} else if (isAllowedFrom(status, "api_pro")) {
currentPlan = "pro"
}
const hasPaidPlan = currentPlan !== "free"
const balances = autumn.data?.balances ?? {}
const tokensUsed = TOKEN_METER_IDS.reduce((sum, id) => {
const balance = balances[id]
return sum + (balance?.usage ?? 0)
}, 0)
const searchesBalance = balances.sm_search_queries
const searchesUsed = searchesBalance?.usage ?? 0
const usdBalance = balances.usd_credits
const usdIncluded = usdBalance?.granted ?? 0
const usdSpent = usdBalance?.usage ?? 0
const planUsagePct =
usdIncluded > 0 ? calculateUsagePercent(usdSpent, usdIncluded) : 0
const resetAt =
usdBalance?.nextResetAt ??
balances.sm_tokens_text?.nextResetAt ??
searchesBalance?.nextResetAt ??
undefined
const daysRemaining = getDaysRemaining(resetAt)
const isLoading = autumn.isLoading
return {
tokensUsed,
searchesUsed,
usdIncluded,
usdSpent,
planUsagePct,
currentPlan,
hasPaidPlan,
isLoading,
daysRemaining,
}
}