open-prompt / src /components /settings /billing-settings.tsx
GitHub Action
Automated sync to Hugging Face
bcce530
'use client'
import { useState, useTransition } from 'react'
import { useRouter } from 'next/navigation'
import {
Zap, Check, CreditCard, AlertCircle, Loader2, ExternalLink, Calendar, Shield
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Separator } from '@/components/ui/separator'
interface BillingSettingsProps {
currentPlan: 'free' | 'pro'
subscriptionStatus?: string | null
subscriptionEnd?: Date | null
cancelAtPeriodEnd?: boolean
}
export function BillingSettings({
currentPlan,
subscriptionStatus,
subscriptionEnd,
cancelAtPeriodEnd,
}: BillingSettingsProps) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
const isPro = currentPlan === 'pro' && subscriptionStatus === 'active'
const isTrial = subscriptionStatus === 'trialing'
const isPastDue = subscriptionStatus === 'past_due'
const handleUpgrade = () => {
setError(null)
startTransition(async () => {
try {
const res = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan: 'pro_monthly' }),
})
const data = await res.json()
if (data.url) {
window.location.href = data.url
} else {
setError(data.error || 'Failed to start checkout')
}
} catch {
setError('Something went wrong. Please try again.')
}
})
}
const handleManageBilling = () => {
setError(null)
startTransition(async () => {
try {
const res = await fetch('/api/stripe/portal', { method: 'POST' })
const data = await res.json()
if (data.url) {
window.location.href = data.url
} else {
setError(data.error || 'Failed to open billing portal')
}
} catch {
setError('Something went wrong. Please try again.')
}
})
}
return (
<div className="space-y-6">
{/* Current plan card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard className="h-5 w-5 text-primary" />
Current Plan
</CardTitle>
<CardDescription>
Manage your subscription and billing information
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-xl border bg-muted/30">
<div className="flex items-center gap-3">
<div className={`h-10 w-10 rounded-full flex items-center justify-center ${
isPro || isTrial ? 'bg-primary/15' : 'bg-muted'
}`}>
<Zap className={`h-5 w-5 ${
isPro || isTrial ? 'text-primary' : 'text-muted-foreground'
}`} />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold capitalize">
{currentPlan} Plan
</span>
{isTrial && (
<Badge variant="secondary" className="text-xs">Free Trial</Badge>
)}
{isPastDue && (
<Badge variant="destructive" className="text-xs">Payment Failed</Badge>
)}
{cancelAtPeriodEnd && (
<Badge variant="outline" className="text-xs text-muted-foreground">
Cancels soon
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{isPro || isTrial
? 'Unlimited executions · API access · Private prompts'
: '10 executions/hr · 10 prompts · Community access'}
</p>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold">
{isPro || isTrial ? '$9' : '$0'}
</p>
<p className="text-xs text-muted-foreground">
{isPro || isTrial ? '/month' : 'forever'}
</p>
</div>
</div>
{/* Subscription details */}
{subscriptionEnd && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
{cancelAtPeriodEnd ? (
<span>
Access ends on{' '}
<span className="font-medium text-foreground">
{new Date(subscriptionEnd).toLocaleDateString('en-US', {
month: 'long', day: 'numeric', year: 'numeric',
})}
</span>
</span>
) : (
<span>
Next billing date:{' '}
<span className="font-medium text-foreground">
{new Date(subscriptionEnd).toLocaleDateString('en-US', {
month: 'long', day: 'numeric', year: 'numeric',
})}
</span>
</span>
)}
</div>
)}
{/* Payment failed warning */}
{isPastDue && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Your last payment failed. Please update your payment method to continue Pro access.
</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* CTA buttons */}
<div className="flex gap-3 flex-wrap">
{isPro || isTrial ? (
<Button
id="manage-billing-btn"
variant="outline"
onClick={handleManageBilling}
disabled={isPending}
className="gap-2"
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ExternalLink className="h-4 w-4" />
)}
Manage Billing
</Button>
) : (
<Button
id="upgrade-to-pro-btn"
onClick={handleUpgrade}
disabled={isPending}
className="gap-2 btn-glow"
>
{isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Zap className="h-4 w-4" />
)}
Upgrade to Pro — $9/mo
</Button>
)}
</div>
</CardContent>
</Card>
{/* Feature comparison */}
<Card>
<CardHeader>
<CardTitle className="text-base">Plan Features</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[
{ feature: 'Public prompt access', free: true, pro: true },
{ feature: 'AI tool executions', free: '10/hr', pro: 'Unlimited' },
{ feature: 'Create prompts', free: '10 max', pro: 'Unlimited' },
{ feature: 'Private prompts', free: false, pro: true },
{ feature: 'REST API access', free: false, pro: true },
{ feature: 'Execution history', free: false, pro: true },
{ feature: 'Advanced analytics', free: false, pro: true },
{ feature: 'Remove embed branding', free: false, pro: true },
{ feature: 'Priority support', free: false, pro: true },
].map((row) => (
<div key={row.feature} className="flex items-center text-sm">
<span className="text-muted-foreground flex-1">{row.feature}</span>
<div className="flex gap-6">
<span className="w-20 text-center">
{row.free === true ? (
<Check className="h-4 w-4 text-green-500 mx-auto" />
) : row.free === false ? (
<span className="text-muted-foreground/40"></span>
) : (
<span className="text-muted-foreground text-xs">{row.free}</span>
)}
</span>
<span className="w-20 text-center">
{row.pro === true ? (
<Check className="h-4 w-4 text-primary mx-auto" />
) : (
<span className="text-primary text-xs font-medium">{row.pro}</span>
)}
</span>
</div>
</div>
))}
</div>
<Separator className="my-4" />
<div className="flex justify-end gap-6 text-xs font-semibold text-muted-foreground">
<span className="w-20 text-center">FREE</span>
<span className="w-20 text-center text-primary">PRO</span>
</div>
</CardContent>
</Card>
{/* Security note */}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Shield className="h-3.5 w-3.5" />
<span>
Payments are securely processed by{' '}
<a
href="https://stripe.com"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Stripe
</a>
. We never store your card details.
</span>
</div>
</div>
)
}