Skip to content

Commit a18c021

Browse files
Improve billing clarity and add usage analytics by instance
1 parent 12ff9f5 commit a18c021

12 files changed

Lines changed: 885 additions & 75 deletions

File tree

src/app/(dashboard)/[orgSlug]/billing/page.tsx

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,107 @@
11
'use client'
22

3+
import { useInstances } from '@/hooks/use-instances'
34
import { useUsage } from '@/hooks/use-usage'
45
import { useCredits } from '@/hooks/use-credits'
56
import { CurrentSpend } from '@/components/billing/current-spend'
67
import { PlanCard } from '@/components/billing/plan-card'
7-
import { UsageChart } from '@/components/billing/usage-chart'
88
import { UsageByModelTable } from '@/components/billing/usage-by-model'
99
import { BillingPortalButton } from '@/components/billing/billing-portal-button'
1010
import { AddCreditsButton } from '@/components/billing/add-credits-button'
1111
import { AutoTopupSettings } from '@/components/billing/auto-topup-settings'
12+
import { CreditActivity } from '@/components/billing/credit-activity'
13+
import { UsageAnalytics } from '@/components/billing/usage-analytics'
1214
import { Loading } from '@/components/shared/loading'
15+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
16+
import { ArrowDownToLine, CreditCard, Sparkles } from 'lucide-react'
1317

1418
export default function BillingPage() {
1519
const { usage, isLoading: usageLoading } = useUsage()
1620
const { credits, isLoading: creditsLoading, mutate } = useCredits()
21+
const { instances, isLoading: instancesLoading } = useInstances()
1722

18-
if (usageLoading || creditsLoading) return <Loading />
23+
if (usageLoading || creditsLoading || instancesLoading) return <Loading />
24+
25+
const topUpNowLabel = credits
26+
? `Top Up €${credits.auto_topup_amount_eur} Now`
27+
: 'Top Up Now'
1928

2029
return (
2130
<div className="space-y-6">
22-
<div className="flex items-center justify-between">
23-
<h1 className="text-2xl font-bold">Billing</h1>
31+
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
32+
<div className="space-y-1">
33+
<h1 className="text-2xl font-bold">Billing</h1>
34+
<p className="max-w-3xl text-sm text-muted-foreground">
35+
Keep an eye on your AI credit balance, monthly usage, and the automatic refill setup that buys more credits when your balance gets low.
36+
</p>
37+
</div>
2438
<div className="flex items-center gap-2">
25-
<AddCreditsButton onSuccess={() => mutate()} />
39+
<AddCreditsButton
40+
amountEur={credits?.auto_topup_amount_eur ?? 20}
41+
label={topUpNowLabel}
42+
onSuccess={() => mutate()}
43+
/>
2644
<BillingPortalButton />
2745
</div>
2846
</div>
29-
<div className="grid gap-4 sm:grid-cols-2">
47+
48+
<Card className="border border-border/70 bg-gradient-to-br from-muted/40 via-background to-background">
49+
<CardHeader>
50+
<CardTitle>How billing works</CardTitle>
51+
</CardHeader>
52+
<CardContent className="grid gap-4 lg:grid-cols-3">
53+
<div className="rounded-xl border border-border/70 bg-background/80 p-4">
54+
<Sparkles className="h-5 w-5 text-muted-foreground" />
55+
<p className="mt-3 font-medium">AI usage spends credits</p>
56+
<p className="mt-1 text-sm text-muted-foreground">
57+
Every model request deducts from your credit balance, so the balance card tells you how much AI usage you can still run.
58+
</p>
59+
</div>
60+
<div className="rounded-xl border border-border/70 bg-background/80 p-4">
61+
<ArrowDownToLine className="h-5 w-5 text-muted-foreground" />
62+
<p className="mt-3 font-medium">Auto top-up refills low balances</p>
63+
<p className="mt-1 text-sm text-muted-foreground">
64+
Choose a refill amount and a threshold. When the balance drops below that threshold, we charge your default card and add more credits automatically.
65+
</p>
66+
</div>
67+
<div className="rounded-xl border border-border/70 bg-background/80 p-4">
68+
<CreditCard className="h-5 w-5 text-muted-foreground" />
69+
<p className="mt-3 font-medium">Cards and invoices live in Stripe</p>
70+
<p className="mt-1 text-sm text-muted-foreground">
71+
Use the billing portal to update payment methods, recover from failed top-ups, and review invoices in one place.
72+
</p>
73+
</div>
74+
</CardContent>
75+
</Card>
76+
77+
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
3078
<CurrentSpend
3179
baseCost={usage?.base_cost ?? 0}
3280
tokenCost={usage?.token_cost ?? 0}
3381
totalCost={usage?.total_cost ?? 0}
3482
creditBalance={credits?.credit_balance_eur}
83+
autoTopupEnabled={credits?.auto_topup_enabled}
84+
autoTopupAmountEur={credits?.auto_topup_amount_eur}
85+
autoTopupThresholdEur={credits?.auto_topup_threshold_eur}
3586
autoTopupFailed={credits?.auto_topup_failed}
3687
/>
3788
<PlanCard currentPlan="starter" />
3889
</div>
39-
{credits && (
40-
<AutoTopupSettings
41-
enabled={credits.auto_topup_enabled}
42-
amountEur={credits.auto_topup_amount_eur}
43-
thresholdEur={credits.auto_topup_threshold_eur}
44-
failed={credits.auto_topup_failed}
45-
onUpdate={() => mutate()}
46-
/>
47-
)}
48-
<UsageChart data={usage?.daily ?? []} />
90+
91+
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
92+
{credits && (
93+
<AutoTopupSettings
94+
enabled={credits.auto_topup_enabled}
95+
amountEur={credits.auto_topup_amount_eur}
96+
thresholdEur={credits.auto_topup_threshold_eur}
97+
failed={credits.auto_topup_failed}
98+
onUpdate={() => mutate()}
99+
/>
100+
)}
101+
<CreditActivity transactions={credits?.recent_transactions ?? []} />
102+
</div>
103+
104+
<UsageAnalytics instances={instances} />
49105
<UsageByModelTable data={usage?.by_model ?? []} />
50106
</div>
51107
)

src/app/api/billing/credits/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ export async function PATCH(req: Request) {
7979
if (parsed.data.auto_topup_threshold_eur !== undefined) {
8080
updates.auto_topup_threshold_eur = String(parsed.data.auto_topup_threshold_eur)
8181
}
82+
if (
83+
parsed.data.auto_topup_enabled !== undefined ||
84+
parsed.data.auto_topup_amount_eur !== undefined ||
85+
parsed.data.auto_topup_threshold_eur !== undefined
86+
) {
87+
// Let the org retry auto top-up after they update settings or fix their card in Stripe.
88+
updates.auto_topup_failed = false
89+
}
8290

8391
if (Object.keys(updates).length > 0) {
8492
await supabaseAdmin

src/app/api/billing/usage/route.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@ import { NextResponse } from 'next/server'
22
import { requireAuth } from '@/lib/auth'
33
import { getOrgUsageSummary } from '@/lib/stripe/usage'
44

5+
function getPresetPeriod(period: string): { start: Date; end: Date } | null {
6+
const now = new Date()
7+
8+
if (period === 'current') {
9+
return {
10+
start: new Date(now.getFullYear(), now.getMonth(), 1),
11+
end: new Date(now),
12+
}
13+
}
14+
15+
const match = period.match(/^(\d+)d$/)
16+
if (!match) return null
17+
18+
const days = Number(match[1])
19+
if (!Number.isFinite(days) || days <= 0) return null
20+
21+
const start = new Date(now)
22+
start.setDate(now.getDate() - (days - 1))
23+
start.setHours(0, 0, 0, 0)
24+
25+
return {
26+
start,
27+
end: now,
28+
}
29+
}
30+
531
export async function GET(req: Request) {
632
const { org } = await requireAuth()
733
const { searchParams } = new URL(req.url)
@@ -11,12 +37,16 @@ export async function GET(req: Request) {
1137
let periodStart: Date
1238
let periodEnd: Date
1339

14-
if (period === 'current') {
15-
const now = new Date()
16-
periodStart = new Date(now.getFullYear(), now.getMonth(), 1)
17-
periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59)
40+
const presetPeriod = getPresetPeriod(period)
41+
42+
if (presetPeriod) {
43+
periodStart = presetPeriod.start
44+
periodEnd = presetPeriod.end
1845
} else {
1946
const [year, month] = period.split('-').map(Number)
47+
if (!year || !month) {
48+
return NextResponse.json({ error: 'Invalid period' }, { status: 400 })
49+
}
2050
periodStart = new Date(year, month - 1, 1)
2151
periodEnd = new Date(year, month, 0, 23, 59, 59)
2252
}
Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useState, type ComponentProps } from 'react'
44
import { Button } from '@/components/ui/button'
55
import { toast } from 'sonner'
66
import { Plus, Loader2 } from 'lucide-react'
77

88
interface AddCreditsButtonProps {
99
onSuccess?: () => void
10+
amountEur?: number
11+
label?: string
12+
variant?: ComponentProps<typeof Button>['variant']
13+
size?: ComponentProps<typeof Button>['size']
14+
className?: string
1015
}
1116

12-
export function AddCreditsButton({ onSuccess }: AddCreditsButtonProps) {
17+
export function AddCreditsButton({
18+
onSuccess,
19+
amountEur = 20,
20+
label,
21+
variant = 'outline',
22+
size = 'sm',
23+
className,
24+
}: AddCreditsButtonProps) {
1325
const [loading, setLoading] = useState(false)
1426

1527
async function handleClick() {
@@ -18,7 +30,7 @@ export function AddCreditsButton({ onSuccess }: AddCreditsButtonProps) {
1830
const res = await fetch('/api/billing/credits', {
1931
method: 'POST',
2032
headers: { 'Content-Type': 'application/json' },
21-
body: JSON.stringify({ amount_eur: 20 }),
33+
body: JSON.stringify({ amount_eur: amountEur }),
2234
})
2335
const data = await res.json()
2436
if (!res.ok) {
@@ -27,7 +39,7 @@ export function AddCreditsButton({ onSuccess }: AddCreditsButtonProps) {
2739
}
2840
// For now, show success toast. In production, use Stripe Elements
2941
// with the clientSecret to confirm the PaymentIntent.
30-
toast.success('Top-up initiated. Credits will be added once payment confirms.')
42+
toast.success(`Top-up started for €${amountEur}. Credits will be added once payment confirms.`)
3143
onSuccess?.()
3244
} catch {
3345
toast.error('Failed to add credits')
@@ -37,9 +49,15 @@ export function AddCreditsButton({ onSuccess }: AddCreditsButtonProps) {
3749
}
3850

3951
return (
40-
<Button variant="outline" size="sm" onClick={handleClick} disabled={loading}>
52+
<Button
53+
variant={variant}
54+
size={size}
55+
className={className}
56+
onClick={handleClick}
57+
disabled={loading}
58+
>
4159
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Plus className="mr-2 h-4 w-4" />}
42-
Add Credits
60+
{label ?? `Top Up €${amountEur}`}
4361
</Button>
4462
)
4563
}

0 commit comments

Comments
 (0)