mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
add web upgrade plans UI
This commit is contained in:
parent
3b24bf2fb2
commit
db06e79da3
2 changed files with 244 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ import {
|
|||
LifeBuoy,
|
||||
LayoutGrid,
|
||||
ChevronRight,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils"
|
||||
|
|
@ -352,6 +353,15 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
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="https://app.supermemory.ai/settings#billing">
|
||||
<Zap className="size-4 text-[#4BA0FA]" />
|
||||
Upgrade
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onAddMemory}
|
||||
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"
|
||||
|
|
@ -421,6 +431,30 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
className={cn(
|
||||
"rounded-full! h-9! min-h-9 shrink-0 border-transparent bg-[#4BA0FA] text-[#00171A] hover:bg-[#4BA0FA]/90",
|
||||
"max-lg:w-9 max-lg:min-w-9 max-lg:justify-center max-lg:gap-0 max-lg:px-0",
|
||||
"lg:min-w-0 lg:gap-1.5 lg:px-3 lg:font-semibold",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<a
|
||||
href="https://app.supermemory.ai/settings#billing"
|
||||
aria-label="Upgrade"
|
||||
>
|
||||
<Zap className="size-3.5 shrink-0 lg:size-4" />
|
||||
<span className="max-lg:sr-only">Upgrade</span>
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={dmSansClassName()}>
|
||||
Upgrade
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import {
|
||||
Check,
|
||||
Coins,
|
||||
ExternalLink,
|
||||
LoaderIcon,
|
||||
|
|
@ -30,6 +31,8 @@ const API_BASE =
|
|||
const CREDIT_FEATURE_ID = "usd_credits"
|
||||
const TOP_UP_PLAN_ID = "credits_topup"
|
||||
const TOP_UP_AMOUNTS = [10, 25, 50, 100] as const
|
||||
const PLAN_CARD_ACTION_CLASS =
|
||||
"inline-flex h-10 w-full items-center justify-center gap-2 rounded-[10px] text-[14px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
||||
|
||||
type BillingInvoice = {
|
||||
planIds?: string[]
|
||||
|
|
@ -61,6 +64,45 @@ type AutoTopupsResponse =
|
|||
}
|
||||
| { ok: false; reason: string; message?: string }
|
||||
|
||||
type PlanCardDefinition = {
|
||||
id: "free" | "pro"
|
||||
name: string
|
||||
price: string
|
||||
period: string
|
||||
credits: string
|
||||
description: string
|
||||
features: string[]
|
||||
}
|
||||
|
||||
const PLAN_CARDS: PlanCardDefinition[] = [
|
||||
{
|
||||
id: "free",
|
||||
name: "Free",
|
||||
price: "$0",
|
||||
period: "",
|
||||
credits: "$5",
|
||||
description: "Try supermemory with no commitment",
|
||||
features: [
|
||||
"Pay-as-you-go after $5 runs out",
|
||||
"Full search and memory access",
|
||||
"Email support",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
price: "$19",
|
||||
period: "/mo",
|
||||
credits: "$20",
|
||||
description: "For people building with AI memory",
|
||||
features: [
|
||||
"Auto top-up when balance runs low",
|
||||
"All plugins (Claude Code, Cursor, Hermes...)",
|
||||
"Priority support",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function SectionTitle({
|
||||
children,
|
||||
aside,
|
||||
|
|
@ -103,6 +145,81 @@ function SettingsCard({
|
|||
)
|
||||
}
|
||||
|
||||
function PlanCard({
|
||||
action,
|
||||
isCurrent,
|
||||
plan,
|
||||
}: {
|
||||
action: React.ReactNode
|
||||
isCurrent: boolean
|
||||
plan: PlanCardDefinition
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
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)]",
|
||||
isCurrent
|
||||
? "border-[#2261CA33] bg-[#17202B]"
|
||||
: "border-white/[0.08] bg-[#14161A]",
|
||||
)}
|
||||
>
|
||||
<p className="font-mono text-[10px] font-medium uppercase tracking-[0.18em] text-[#737373]">
|
||||
{plan.name}
|
||||
</p>
|
||||
|
||||
<div className="mt-3 flex items-baseline gap-1">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[34px] font-bold leading-none tracking-[-0.34px] text-[#FAFAFA] tabular-nums",
|
||||
)}
|
||||
>
|
||||
{plan.price}
|
||||
</span>
|
||||
{plan.period ? (
|
||||
<span className="text-[13px] text-[#737373]">{plan.period}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"mt-2 text-[13px] leading-snug text-[#A3A3A3]",
|
||||
)}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="mt-5 mb-6 flex flex-1 flex-col gap-3">
|
||||
{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]" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{action}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Pill({
|
||||
children,
|
||||
tone = "muted",
|
||||
|
|
@ -490,6 +607,74 @@ export default function Billing() {
|
|||
})
|
||||
}
|
||||
|
||||
const getPlanCardAction = (plan: PlanCardDefinition) => {
|
||||
if (plan.id === "free") {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
PLAN_CARD_ACTION_CLASS,
|
||||
"border border-white/[0.04] bg-white/[0.02] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
{hasPaidPlan ? "Included with current plan" : "Your current plan"}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentPlan === "pro") {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
PLAN_CARD_ACTION_CLASS,
|
||||
"border border-white/[0.04] bg-white/[0.02] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Your current plan
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentPlan === "scale" || currentPlan === "enterprise") {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
PLAN_CARD_ACTION_CLASS,
|
||||
"border border-white/[0.04] bg-white/[0.02] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Included with {planDisplayNames[currentPlan]}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
disabled={isUpgrading || isCheckingStatus || autumn.isLoading}
|
||||
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
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-7">
|
||||
<section id="billing-subscription" className="flex flex-col gap-4">
|
||||
|
|
@ -694,6 +879,31 @@ export default function Billing() {
|
|||
</SettingsCard>
|
||||
</section>
|
||||
|
||||
<section id="billing-plans" className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<SectionTitle>Plans</SectionTitle>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] leading-relaxed text-[#A3A3A3]",
|
||||
)}
|
||||
>
|
||||
All plans include usage-metered access. Top-ups roll over with paid
|
||||
plans.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{PLAN_CARDS.map((plan) => (
|
||||
<PlanCard
|
||||
action={getPlanCardAction(plan)}
|
||||
isCurrent={currentPlan === plan.id}
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<SectionTitle>Credits</SectionTitle>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue