Add expanded billing plans

This commit is contained in:
Ishaan Gupta 2026-05-30 22:27:19 +05:30
parent 960fcf058f
commit d0528fe1ba
2 changed files with 182 additions and 28 deletions

View file

@ -15,7 +15,6 @@ import {
LifeBuoy,
LayoutGrid,
ChevronRight,
Zap,
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
@ -360,7 +359,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
className="gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-white/85 hover:bg-white/[0.06] focus:bg-white/[0.06] focus:text-white cursor-pointer"
>
<a href={billingSettingsUrl}>
<Zap className="size-4 text-[#4BA0FA]" />
<Logo className="h-4 w-5 shrink-0" />
Upgrade
</a>
</DropdownMenuItem>
@ -445,7 +444,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
)}
>
<a href={billingSettingsUrl} aria-label="Upgrade">
<Zap className="size-3.5 shrink-0 lg:size-4" />
<Logo className="h-3.5 w-[17px] shrink-0 lg:h-4 lg:w-5" />
<span className="max-lg:sr-only">Upgrade</span>
</a>
</Button>

View file

@ -15,6 +15,7 @@ import { useCustomer } from "autumn-js/react"
import {
Check,
Coins,
ArrowRight,
ExternalLink,
LoaderIcon,
Plus,
@ -65,13 +66,17 @@ type AutoTopupsResponse =
| { ok: false; reason: string; message?: string }
type PlanCardDefinition = {
id: "free" | "pro"
id: "free" | "pro" | "scale" | "enterprise"
name: string
price: string
period: string
credits: string
productId: "api_free" | "api_pro" | "api_scale" | "api_enterprise"
description: string
includesFrom?: string
features: string[]
highlight?: boolean
isContactSales?: boolean
}
const PLAN_CARDS: PlanCardDefinition[] = [
@ -81,6 +86,7 @@ const PLAN_CARDS: PlanCardDefinition[] = [
price: "$0",
period: "",
credits: "$5",
productId: "api_free",
description: "Try supermemory with no commitment",
features: [
"Pay-as-you-go after $5 runs out",
@ -94,6 +100,7 @@ const PLAN_CARDS: PlanCardDefinition[] = [
price: "$19",
period: "/mo",
credits: "$20",
productId: "api_pro",
description: "For people building with AI memory",
features: [
"Auto top-up when balance runs low",
@ -103,6 +110,48 @@ const PLAN_CARDS: PlanCardDefinition[] = [
},
]
const ADVANCED_PLAN_CARDS: PlanCardDefinition[] = [
{
id: "scale",
name: "Scale",
price: "$399",
period: "/mo",
credits: "$600",
productId: "api_scale",
description: "For teams and production workloads",
includesFrom: "Pro",
features: [
"Auto top-up & spend caps",
"Gmail, S3 & Web Crawler connectors",
"Dedicated support",
],
highlight: true,
},
{
id: "enterprise",
name: "Enterprise",
price: "Custom",
period: "",
credits: "Unlimited",
productId: "api_enterprise",
description: "Custom deployments with dedicated engineering",
includesFrom: "Scale",
features: [
"Custom metering & billing",
"Custom integrations & SSO",
"Forward-deployed engineer",
],
isContactSales: true,
},
]
const PLAN_RANK: Record<PlanCardDefinition["id"], number> = {
free: 0,
pro: 1,
scale: 2,
enterprise: 3,
}
function SectionTitle({
children,
aside,
@ -157,10 +206,24 @@ function PlanCard({
className={cn(
"relative flex min-h-[416px] flex-col overflow-hidden rounded-[14px] border p-5",
"shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
"border-white/[0.08] bg-[#14161A]",
plan.highlight
? "border-[#0B65C9] bg-[#14161A] shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7),0_0_0_1px_rgba(11,101,201,0.72)]"
: "border-white/[0.08] bg-[#14161A]",
)}
>
<p className="font-mono text-[10px] font-medium uppercase tracking-[0.18em] text-[#737373]">
{plan.highlight ? (
<div className="-translate-x-1/2 absolute top-[-1px] left-1/2 whitespace-nowrap">
<span className="rounded-full bg-[#1688FF] px-3 py-1 text-[10px] font-bold uppercase leading-none tracking-[0.32px] text-white">
Most popular
</span>
</div>
) : null}
<p
className={cn(
"font-mono text-[10px] font-medium uppercase tracking-[0.18em]",
plan.highlight ? "text-[#4BA0FA]" : "text-[#737373]",
)}
>
{plan.name}
</p>
@ -187,25 +250,68 @@ function PlanCard({
{plan.description}
</p>
<div className="mt-5 flex items-center gap-2 rounded-[8px] bg-white/[0.04] px-3 py-2.5 text-[#A3A3A3]">
<Coins className="size-3.5 shrink-0 text-[#737373]" />
<div className="min-w-0">
<p className="text-[12px] font-semibold leading-none text-[#C8D0DA] tabular-nums">
{plan.credits}
</p>
<p className="mt-0.5 text-[10px] leading-none text-[#737373]">
of usage included
</p>
{plan.isContactSales ? null : (
<div
className={cn(
"mt-5 flex items-center gap-2 rounded-[8px] px-3 py-2.5 text-[#A3A3A3]",
plan.highlight ? "bg-[#061B38]" : "bg-white/[0.04]",
)}
>
<Coins
className={cn(
"size-3.5 shrink-0",
plan.highlight ? "text-[#4BA0FA]" : "text-[#737373]",
)}
/>
<div className="min-w-0">
<p
className={cn(
"text-[12px] font-semibold leading-none tabular-nums",
plan.highlight ? "text-[#4BA0FA]" : "text-[#C8D0DA]",
)}
>
{plan.credits}
</p>
<p
className={cn(
"mt-0.5 text-[10px] leading-none",
plan.highlight ? "text-[#4BA0FA]/75" : "text-[#737373]",
)}
>
of usage included
</p>
</div>
</div>
</div>
)}
<ul className="mt-5 mb-6 flex flex-1 flex-col gap-3">
{plan.includesFrom ? (
<div className="mt-5 flex items-center gap-3">
<div className="h-px flex-1 bg-white/[0.08]" />
<span className="whitespace-nowrap text-[10px] text-[#737373]">
Everything in {plan.includesFrom}, plus
</span>
<div className="h-px flex-1 bg-white/[0.08]" />
</div>
) : null}
<ul
className={cn(
"mb-6 flex flex-1 flex-col gap-3",
plan.includesFrom ? "mt-5" : "mt-5",
plan.isContactSales && !plan.includesFrom && "mt-7",
)}
>
{plan.features.map((feature) => (
<li
className="flex items-start gap-2 text-[13px] leading-snug text-[#C8D0DA]"
key={feature}
>
<Check className="mt-0.5 size-3.5 shrink-0 text-[#737373]" />
<Check
className={cn(
"mt-0.5 size-3.5 shrink-0",
plan.highlight ? "text-[#4BA0FA]" : "text-[#737373]",
)}
/>
<span>{feature}</span>
</li>
))}
@ -349,6 +455,7 @@ export default function Billing() {
const [isCancelling, setIsCancelling] = useState(false)
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false)
const [isCreditsDialogOpen, setIsCreditsDialogOpen] = useState(false)
const [showOtherPlans, setShowOtherPlans] = useState(false)
const [topUpAmount, setTopUpAmount] = useState<number>(25)
const [customTopUpAmount, setCustomTopUpAmount] = useState("")
const [topUpPendingAmount, setTopUpPendingAmount] = useState<number | null>(
@ -446,11 +553,11 @@ export default function Billing() {
const planDisplayNames = PLAN_DISPLAY_NAMES
const handleUpgrade = async () => {
const handleUpgrade = async (planId: "api_pro" | "api_scale") => {
setIsUpgrading(true)
try {
const result = await autumn.attach({
planId: "api_pro",
planId,
successUrl: `${window.location.origin}/settings#billing`,
})
if ((result as { paymentUrl?: string })?.paymentUrl) {
@ -604,6 +711,10 @@ export default function Billing() {
}
const getPlanCardAction = (plan: PlanCardDefinition) => {
const disabled = isUpgrading || isCheckingStatus || autumn.isLoading
const isCurrentPlan = plan.id === currentPlan
const isIncludedPlan = PLAN_RANK[currentPlan] > PLAN_RANK[plan.id]
if (plan.id === "free") {
return (
<button
@ -620,7 +731,7 @@ export default function Billing() {
)
}
if (currentPlan === "pro") {
if (isCurrentPlan) {
return (
<button
type="button"
@ -636,7 +747,7 @@ export default function Billing() {
)
}
if (currentPlan === "scale" || currentPlan === "enterprise") {
if (isIncludedPlan) {
return (
<button
type="button"
@ -652,21 +763,40 @@ export default function Billing() {
)
}
if (plan.isContactSales) {
return (
<a
href="mailto:support@supermemory.com?subject=Enterprise%20plan"
className={cn(
dmSans125ClassName(),
PLAN_CARD_ACTION_CLASS,
"border border-white/[0.08] bg-transparent text-[#FAFAFA] hover:bg-white/[0.04]",
)}
>
Contact sales
</a>
)
}
const checkoutPlanId =
plan.productId === "api_pro" || plan.productId === "api_scale"
? plan.productId
: null
if (!checkoutPlanId) return null
return (
<button
type="button"
onClick={handleUpgrade}
disabled={isUpgrading || isCheckingStatus || autumn.isLoading}
onClick={() => handleUpgrade(checkoutPlanId)}
disabled={disabled}
className={cn(
dmSans125ClassName(),
PLAN_CARD_ACTION_CLASS,
"bg-[#0054AD] text-[#FAFAFA] hover:bg-[#0B65C9]",
)}
>
{isUpgrading || isCheckingStatus || autumn.isLoading ? (
<LoaderIcon className="size-4 animate-spin" />
) : null}
Upgrade to Pro
{disabled ? <LoaderIcon className="size-4 animate-spin" /> : null}
Upgrade to {plan.name}
</button>
)
}
@ -864,6 +994,31 @@ export default function Billing() {
/>
))}
</div>
{showOtherPlans ? (
<div className="grid gap-4 md:grid-cols-2">
{ADVANCED_PLAN_CARDS.map((plan) => (
<PlanCard
action={getPlanCardAction(plan)}
key={plan.id}
plan={plan}
/>
))}
</div>
) : (
<div className="flex justify-center pt-1">
<button
type="button"
onClick={() => setShowOtherPlans(true)}
className={cn(
dmSans125ClassName(),
"inline-flex h-9 items-center justify-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.02] px-4 text-[13px] font-semibold text-[#A3A3A3] transition-colors hover:border-[#0B65C9]/60 hover:bg-[#061B38] hover:text-[#FAFAFA]",
)}
>
See other plans
<ArrowRight className="size-3.5" />
</button>
</div>
)}
</section>
<section className="flex flex-col gap-4">