mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-22 06:53:57 +00:00
feat: migrate to api_pro billing and add credits-based usage display (#729)
Summary - Migrate from consumer_pro to api_pro billing product across the app - Enable Nova app for all users (remove feature flag) - Add credits-based usage tracking with tokens abstraction
This commit is contained in:
parent
c15308c503
commit
11a0abd5c0
18 changed files with 309 additions and 103 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { signIn } from "@lib/auth"
|
||||
import { usePostHog } from "@lib/posthog"
|
||||
import { TextSeparator } from "@repo/ui/components/text-separator"
|
||||
import { TextSeparator } from "@ui/components/text-separator"
|
||||
import { ExternalAuthButton } from "@ui/button/external-auth"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Badge } from "@ui/components/badge"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function NavigationLayout({
|
|||
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const flagEnabled = useFeatureFlagEnabled("nova-alpha-access")
|
||||
const flagEnabled = true
|
||||
|
||||
useEffect(() => {
|
||||
if (flagEnabled && !pathname.includes("/new")) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { useFeatureFlagEnabled } from "posthog-js/react"
|
|||
export default function Page() {
|
||||
const { user, session } = useAuth()
|
||||
const router = useRouter()
|
||||
const flagEnabled = useFeatureFlagEnabled("nova-alpha-access")
|
||||
const flagEnabled = true
|
||||
|
||||
// TODO: remove this flow after the feature flag is removed
|
||||
// Old app: localStorage-backed onboarding
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { MobileBanner } from "@/components/new/mobile-banner"
|
|||
|
||||
export default function NewLayout({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const flagEnabled = useFeatureFlagEnabled("nova-alpha-access")
|
||||
const flagEnabled = true
|
||||
|
||||
useEffect(() => {
|
||||
if (!flagEnabled) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import {
|
||||
fetchConsumerProProduct,
|
||||
fetchMemoriesFeature,
|
||||
} from "@repo/lib/queries"
|
||||
import { fetchApiProProduct, fetchMemoriesFeature } from "@repo/lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { ConnectAIModal } from "./connect-ai-modal"
|
||||
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"
|
||||
|
|
@ -74,7 +71,7 @@ function Menu({ id }: { id?: string }) {
|
|||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn)
|
||||
const { data: proCheck } = fetchApiProProduct(autumn)
|
||||
|
||||
useEffect(() => {
|
||||
if (memoriesCheck) {
|
||||
|
|
|
|||
|
|
@ -59,9 +59,8 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
|
|||
useEffect(() => {
|
||||
if (!autumn.isLoading) {
|
||||
setIsProUser(
|
||||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
autumn.customer?.products.some((product) => product.id === "api_pro") ??
|
||||
false,
|
||||
)
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
|
|
@ -70,7 +69,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
|
|||
setIsUpgrading(true)
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: window.location.href,
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { dmSans125ClassName } from "@/lib/fonts"
|
|||
import { cn } from "@lib/utils"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
||||
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
|
||||
import { useTokenUsage } from "@/hooks/use-token-usage"
|
||||
import { formatUsageNumber, tokensToCredits } from "@/lib/billing-utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -107,19 +108,31 @@ export default function Account() {
|
|||
}
|
||||
|
||||
const {
|
||||
memoriesUsed,
|
||||
memoriesLimit,
|
||||
hasProProduct,
|
||||
tokensUsed,
|
||||
tokensLimit,
|
||||
tokensPercent,
|
||||
searchesUsed,
|
||||
searchesLimit,
|
||||
searchesPercent,
|
||||
currentPlan,
|
||||
hasPaidPlan,
|
||||
isLoading: isCheckingStatus,
|
||||
usagePercent,
|
||||
} = useMemoriesUsage(autumn)
|
||||
daysRemaining,
|
||||
} = useTokenUsage(autumn)
|
||||
|
||||
const planDisplayNames: Record<string, string> = {
|
||||
free: "Free",
|
||||
pro: "Pro",
|
||||
scale: "Scale",
|
||||
enterprise: "Enterprise",
|
||||
}
|
||||
|
||||
// Handlers
|
||||
const handleUpgrade = async () => {
|
||||
setIsUpgrading(true)
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: "https://app.supermemory.ai/new/settings#account",
|
||||
})
|
||||
window.location.reload()
|
||||
|
|
@ -222,8 +235,6 @@ export default function Account() {
|
|||
{allOrgs.map((organization) => {
|
||||
const isCurrent = organization.id === org?.id
|
||||
const isSwitching = switchingOrgId === organization.id
|
||||
const isConsumer =
|
||||
organization.metadata?.isConsumer === true
|
||||
return (
|
||||
<button
|
||||
key={organization.id}
|
||||
|
|
@ -256,11 +267,6 @@ export default function Account() {
|
|||
<LoaderIcon className="size-4 text-[#4BA0FA] shrink-0 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
{!isConsumer && (
|
||||
<span className="text-[11px] font-medium tracking-[0.3px] px-1.5 py-0.5 rounded-[4px] shrink-0 bg-[#737373]/15 text-[#737373]">
|
||||
API
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
|
@ -295,7 +301,7 @@ export default function Account() {
|
|||
<SectionTitle>Billing & Subscription</SectionTitle>
|
||||
<SettingsCard>
|
||||
<div className="flex flex-col gap-6">
|
||||
{hasProProduct ? (
|
||||
{hasPaidPlan ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -305,7 +311,7 @@ export default function Account() {
|
|||
"font-semibold text-[20px] tracking-[-0.2px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Pro plan
|
||||
{planDisplayNames[currentPlan]} plan
|
||||
</p>
|
||||
<span className="bg-[#4BA0FA] text-[#00171A] text-[12px] font-bold tracking-[0.36px] px-1 py-[3px] rounded-[3px] h-[18px] flex items-center justify-center">
|
||||
ACTIVE
|
||||
|
|
@ -321,7 +327,8 @@ export default function Account() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div id="progress-bar" className="flex flex-col gap-3">
|
||||
{/* Credits Usage Progress */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className={cn(
|
||||
|
|
@ -329,36 +336,80 @@ export default function Account() {
|
|||
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Unlimited Memories
|
||||
Credits Used
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#4BA0FA]",
|
||||
)}
|
||||
>
|
||||
{memoriesUsed}/
|
||||
</span>
|
||||
<span className="text-[#4BA0FA] text-[20px] leading-none ml-0.5">
|
||||
∞
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#4BA0FA]",
|
||||
)}
|
||||
>
|
||||
{tokensToCredits(tokensUsed)} /{" "}
|
||||
{tokensToCredits(tokensLimit)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
id="progress-bar-fill"
|
||||
className="h-3 w-full rounded-[40px] bg-[#2E353D] blur-[1px] p-px overflow-hidden"
|
||||
>
|
||||
<div className="h-3 w-full rounded-[40px] bg-[#2E353D] p-px overflow-hidden">
|
||||
<div
|
||||
className="h-full w-full rounded-[40px]"
|
||||
className="h-full rounded-[40px]"
|
||||
style={{
|
||||
width: `${tokensPercent}%`,
|
||||
background:
|
||||
"linear-gradient(to right, #4BA0FA 80.517%, #002757 100%)",
|
||||
tokensPercent > 80
|
||||
? "#ef4444"
|
||||
: "linear-gradient(to right, #4BA0FA 80%, #002757 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Queries Progress */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Search Queries
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#4BA0FA]",
|
||||
)}
|
||||
>
|
||||
{formatUsageNumber(searchesUsed)} /{" "}
|
||||
{formatUsageNumber(searchesLimit)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3 w-full rounded-[40px] bg-[#2E353D] p-px overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-[40px]"
|
||||
style={{
|
||||
width: `${searchesPercent}%`,
|
||||
background:
|
||||
searchesPercent > 80
|
||||
? "#ef4444"
|
||||
: "linear-gradient(to right, #4BA0FA 80%, #002757 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Days remaining indicator */}
|
||||
{daysRemaining !== null && (
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-sm text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Resets in {daysRemaining} day
|
||||
{daysRemaining !== 1 ? "s" : ""}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
|
@ -389,14 +440,17 @@ export default function Account() {
|
|||
Free plan
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<PlanFeatureRow icon="x" text="Limited 200 memories" />
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="10 credits / 1M tokens"
|
||||
/>
|
||||
<PlanFeatureRow icon="check" text="10K search queries" />
|
||||
<PlanFeatureRow icon="x" text="No connections" />
|
||||
<PlanFeatureRow icon="check" text="Basic search" />
|
||||
<PlanFeatureRow icon="check" text="Basic support" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pro plan card - highlighted */}
|
||||
{/* Current plan card - highlighted */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-4 p-4 rounded-[10px]",
|
||||
|
|
@ -405,7 +459,6 @@ export default function Account() {
|
|||
"relative overflow-hidden",
|
||||
)}
|
||||
>
|
||||
{/* Header with ACTIVE badge */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className={cn(
|
||||
|
|
@ -413,7 +466,7 @@ export default function Account() {
|
|||
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Pro plan
|
||||
{planDisplayNames[currentPlan]} plan
|
||||
</p>
|
||||
<span className="bg-[#4BA0FA] text-[#00171A] text-[12px] font-bold tracking-[0.36px] px-1 py-[3px] rounded-[3px] h-[18px] flex items-center justify-center">
|
||||
ACTIVE
|
||||
|
|
@ -422,7 +475,12 @@ export default function Account() {
|
|||
<div className="flex flex-col gap-2">
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="Unlimited memories"
|
||||
text="30 credits / 3M tokens"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="100K search queries"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
|
|
@ -430,18 +488,12 @@ export default function Account() {
|
|||
text="10 connections"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="Advanced search"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="Priority support"
|
||||
variant="highlight"
|
||||
/>
|
||||
</div>
|
||||
{/* Inset highlight */}
|
||||
<div className="absolute inset-0 pointer-events-none rounded-[inherit] shadow-[inset_0.711px_0.711px_0.711px_rgba(255,255,255,0.1)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -467,6 +519,7 @@ export default function Account() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* Credits Usage Progress */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
|
|
@ -475,7 +528,7 @@ export default function Account() {
|
|||
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Memories
|
||||
Credits Used
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
|
|
@ -483,18 +536,67 @@ export default function Account() {
|
|||
"font-medium text-[16px] tracking-[-0.16px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
{memoriesUsed}/{memoriesLimit}
|
||||
{tokensToCredits(tokensUsed)} /{" "}
|
||||
{tokensToCredits(tokensLimit)}
|
||||
</p>
|
||||
</div>
|
||||
{/* Progress bar */}
|
||||
<div className="h-3 w-full rounded-[40px] bg-[#2E353D] p-px">
|
||||
<div className="h-3 w-full rounded-[40px] bg-[#2E353D] p-px overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-[40px] bg-[#0054AD] transition-all"
|
||||
style={{ width: `${usagePercent}%` }}
|
||||
className="h-full rounded-[40px] transition-all"
|
||||
style={{
|
||||
width: `${tokensPercent}%`,
|
||||
background: tokensPercent > 80 ? "#ef4444" : "#0054AD",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Queries Progress */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Search Queries
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[16px] tracking-[-0.16px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
{formatUsageNumber(searchesUsed)} /{" "}
|
||||
{formatUsageNumber(searchesLimit)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-3 w-full rounded-[40px] bg-[#2E353D] p-px overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-[40px] transition-all"
|
||||
style={{
|
||||
width: `${searchesPercent}%`,
|
||||
background:
|
||||
searchesPercent > 80 ? "#ef4444" : "#0054AD",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Days remaining indicator */}
|
||||
{daysRemaining !== null && (
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-sm text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Resets in {daysRemaining} day
|
||||
{daysRemaining !== 1 ? "s" : ""}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
|
|
@ -520,7 +622,7 @@ export default function Account() {
|
|||
Upgrading...
|
||||
</>
|
||||
) : (
|
||||
"Upgrade to Pro - $9/month"
|
||||
"Upgrade to Pro - $19/month"
|
||||
)}
|
||||
{/* Inset blue stroke */}
|
||||
<div className="absolute inset-0 pointer-events-none rounded-[inherit] shadow-[inset_1px_1px_2px_1px_#1A88FF]" />
|
||||
|
|
@ -538,9 +640,12 @@ export default function Account() {
|
|||
Free plan
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<PlanFeatureRow icon="x" text="Limited 200 memories" />
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="10 credits / 1M tokens"
|
||||
/>
|
||||
<PlanFeatureRow icon="check" text="10K search queries" />
|
||||
<PlanFeatureRow icon="x" text="No connections" />
|
||||
<PlanFeatureRow icon="check" text="Basic search" />
|
||||
<PlanFeatureRow icon="check" text="Basic support" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -571,7 +676,12 @@ export default function Account() {
|
|||
<div className="flex flex-col gap-2">
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="Unlimited memories"
|
||||
text="30 credits / 3M tokens"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="100K search queries"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
|
|
@ -579,11 +689,6 @@ export default function Account() {
|
|||
text="10 connections"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="Advanced search"
|
||||
variant="highlight"
|
||||
/>
|
||||
<PlanFeatureRow
|
||||
icon="check"
|
||||
text="Priority support"
|
||||
|
|
|
|||
|
|
@ -342,12 +342,12 @@ export default function ConnectionsMCP() {
|
|||
// Billing data
|
||||
const {
|
||||
data: status = {
|
||||
consumer_pro: { allowed: false, status: null },
|
||||
api_pro: { allowed: false, status: null },
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
const hasProProduct = status.consumer_pro?.status !== null
|
||||
const hasProProduct = status.api_pro?.status !== null
|
||||
|
||||
// Get connections data directly from autumn customer
|
||||
const connectionsFeature = autumn.customer?.features?.connections
|
||||
|
|
@ -415,7 +415,7 @@ export default function ConnectionsMCP() {
|
|||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: "https://app.supermemory.ai/new/settings#connections",
|
||||
})
|
||||
window.location.reload()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { useIsMobile } from "@hooks/use-mobile"
|
|||
|
||||
export function NewOnboardingModal() {
|
||||
const router = useRouter()
|
||||
const flagEnabled = useFeatureFlagEnabled("nova-alpha-access")
|
||||
const flagEnabled = true
|
||||
const isMobile = useIsMobile()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ export function BillingView() {
|
|||
|
||||
const {
|
||||
data: status = {
|
||||
consumer_pro: { allowed: false, status: null },
|
||||
api_pro: { allowed: false, status: null },
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
const proStatus = status.consumer_pro
|
||||
const proStatus = status.api_pro
|
||||
const proProductStatus = proStatus?.status
|
||||
const isPastDue = proProductStatus === "past_due"
|
||||
const hasProProduct = proProductStatus !== null
|
||||
|
|
@ -55,7 +55,7 @@ export function BillingView() {
|
|||
setIsLoading(true)
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
analytics.upgradeCompleted()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function ConnectionsTabContent() {
|
|||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
|
|
@ -61,9 +61,8 @@ export function ConnectionsTabContent() {
|
|||
useEffect(() => {
|
||||
if (!autumn.isLoading) {
|
||||
setIsProUser(
|
||||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
autumn.customer?.products.some((product) => product.id === "api_pro") ??
|
||||
false,
|
||||
)
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ export function IntegrationsView() {
|
|||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
|
|
@ -183,9 +183,8 @@ export function IntegrationsView() {
|
|||
useEffect(() => {
|
||||
if (!autumn.isLoading) {
|
||||
setIsProUser(
|
||||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
autumn.customer?.products.some((product) => product.id === "api_pro") ??
|
||||
false,
|
||||
)
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ export function ProfileView() {
|
|||
|
||||
const {
|
||||
data: status = {
|
||||
consumer_pro: { allowed: false, status: null },
|
||||
api_pro: { allowed: false, status: null },
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
const proStatus = status.consumer_pro
|
||||
const proStatus = status.api_pro
|
||||
const isPro = proStatus?.allowed ?? false
|
||||
const proProductStatus = proStatus?.status
|
||||
const isPastDue = proProductStatus === "past_due"
|
||||
|
|
@ -76,7 +76,7 @@ export function ProfileView() {
|
|||
setIsLoading(true)
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
productId: "api_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import type { useCustomer } from "autumn-js/react"
|
|||
export function useMemoriesUsage(autumn: ReturnType<typeof useCustomer>) {
|
||||
const {
|
||||
data: status = {
|
||||
consumer_pro: { allowed: false, status: null },
|
||||
api_pro: { allowed: false, status: null },
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
const proStatus = status.consumer_pro
|
||||
const proStatus = status.api_pro
|
||||
const hasProProduct = proStatus?.status !== null
|
||||
|
||||
const { data: memoriesCheck, isLoading: isLoadingMemories } =
|
||||
|
|
|
|||
57
apps/web/hooks/use-token-usage.ts
Normal file
57
apps/web/hooks/use-token-usage.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { fetchSubscriptionStatus } 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 = {
|
||||
api_pro: { allowed: false, status: null },
|
||||
api_scale: { allowed: false, status: null },
|
||||
api_enterprise: { allowed: false, status: null },
|
||||
},
|
||||
isLoading: isCheckingStatus,
|
||||
} = fetchSubscriptionStatus(autumn, !autumn.isLoading)
|
||||
|
||||
let currentPlan: PlanType = "free"
|
||||
if (status.api_enterprise?.status !== null) {
|
||||
currentPlan = "enterprise"
|
||||
} else if (status.api_scale?.status !== null) {
|
||||
currentPlan = "scale"
|
||||
} else if (status.api_pro?.status !== null) {
|
||||
currentPlan = "pro"
|
||||
}
|
||||
|
||||
const hasPaidPlan = currentPlan !== "free"
|
||||
|
||||
// Get token usage from autumn customer features
|
||||
const tokensFeature = autumn.customer?.features?.api_tokens
|
||||
const tokensUsed = tokensFeature?.usage ?? 0
|
||||
const tokensLimit = tokensFeature?.included_usage ?? 0
|
||||
const tokensResetAt = tokensFeature?.next_reset_at
|
||||
|
||||
// Get search queries usage from autumn customer features
|
||||
const searchesFeature = autumn.customer?.features?.api_search_queries
|
||||
const searchesUsed = searchesFeature?.usage ?? 0
|
||||
const searchesLimit = searchesFeature?.included_usage ?? 0
|
||||
|
||||
const isLoading = autumn.isLoading || isCheckingStatus
|
||||
|
||||
const tokensPercent = calculateUsagePercent(tokensUsed, tokensLimit)
|
||||
const searchesPercent = calculateUsagePercent(searchesUsed, searchesLimit)
|
||||
const daysRemaining = getDaysRemaining(tokensResetAt)
|
||||
|
||||
return {
|
||||
tokensUsed,
|
||||
tokensLimit,
|
||||
tokensPercent,
|
||||
searchesUsed,
|
||||
searchesLimit,
|
||||
searchesPercent,
|
||||
currentPlan,
|
||||
hasPaidPlan,
|
||||
isLoading,
|
||||
daysRemaining,
|
||||
}
|
||||
}
|
||||
49
apps/web/lib/billing-utils.ts
Normal file
49
apps/web/lib/billing-utils.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Convert tokens to credits
|
||||
* 100,000 tokens = 1 credit
|
||||
* Uses floor rounding (2.9 credits shows as 2, only shows 3 when it hits 3.0)
|
||||
*/
|
||||
export function tokensToCredits(tokens: number): number {
|
||||
return Math.floor(tokens / 100_000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number with K/M suffix for display
|
||||
* @example formatUsageNumber(1500000) => "1.5M"
|
||||
* @example formatUsageNumber(50000) => "50K"
|
||||
*/
|
||||
export function formatUsageNumber(value: number): string {
|
||||
if (value >= 1_000_000) {
|
||||
const millions = value / 1_000_000
|
||||
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
const thousands = value / 1_000
|
||||
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`
|
||||
}
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate usage percentage, clamped between 0 and 100
|
||||
*/
|
||||
export function calculateUsagePercent(used: number, limit: number): number {
|
||||
if (limit <= 0) return 0
|
||||
return Math.min(Math.max((used / limit) * 100, 0), 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of days remaining until a reset date
|
||||
* @param resetAt - Unix timestamp in milliseconds (from Autumn billing)
|
||||
* @returns number of days, or null if no date provided
|
||||
*/
|
||||
export function getDaysRemaining(
|
||||
resetAt: number | null | undefined,
|
||||
): number | null {
|
||||
if (!resetAt) return null
|
||||
const end = new Date(resetAt)
|
||||
const now = new Date()
|
||||
const diffMs = end.getTime() - now.getTime()
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24))
|
||||
return Math.max(0, diffDays)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export const fetchSubscriptionStatus = (
|
|||
) =>
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const allPlans = ["consumer_pro"]
|
||||
const allPlans = ["api_pro", "api_scale", "api_enterprise"]
|
||||
const statusMap: Record<
|
||||
string,
|
||||
{ allowed: boolean; status: string | null }
|
||||
|
|
@ -84,15 +84,13 @@ export const fetchConnectionsFeature = (
|
|||
})
|
||||
|
||||
// Product checks
|
||||
export const fetchConsumerProProduct = (
|
||||
autumn: ReturnType<typeof useCustomer>,
|
||||
) =>
|
||||
export const fetchApiProProduct = (autumn: ReturnType<typeof useCustomer>) =>
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const res = autumn.check({ productId: "consumer_pro" })
|
||||
const res = autumn.check({ productId: "api_pro" })
|
||||
return res.data
|
||||
},
|
||||
queryKey: ["autumn-product", "consumer_pro"],
|
||||
queryKey: ["autumn-product", "api_pro"],
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/react": "^8.0.3",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue