Spaces:
Configuration error
Configuration error
| '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> | |
| ) | |
| } | |