import { formatPlanPrice, type Plan, type PlanId, plans } from '@shared/pricing'; import { IconAlertCircle as AlertCircle, IconCheck as Check, IconCreditCard as CreditCard, IconExternalLink as ExternalLink, IconLoader2 as Loader2, IconX as X, } from '@tabler/icons-react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useAuth } from '@/components/auth/AuthProvider'; import { PageHeader } from '@/components/PageHeader'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { apiJson } from '@/lib/api'; import { ContentPanel } from '../../components/ContentPanel'; interface BillingStatus { configured: boolean; } interface PricesResponse { configured: boolean; plans: (Plan & { stripePriceIds: { monthly?: string } | null; })[]; } interface Subscription { id: string; status: string; priceId: string; planId: PlanId; currentPeriodEnd: string; cancelAtPeriodEnd: boolean; product: { id: string; name: string; description?: string; }; price: { id: string; unitAmount: number; currency: string; interval: string; }; } interface SubscriptionResponse { subscription: Subscription | null; status: string; } export async function fetchBillingStatus(): Promise { return apiJson('/api/v1/billing/status', {}, 'Failed to fetch billing status'); } async function fetchPrices(): Promise { return apiJson('/api/v1/billing/prices', {}, 'Failed to fetch prices'); } async function fetchSubscription(): Promise { return apiJson( '/api/v1/billing/subscription?type=user', {}, 'Failed to fetch subscription' ); } async function createCheckout(priceId: string): Promise<{ url: string }> { return apiJson<{ url: string }>( '/api/v1/billing/checkout', { method: 'POST', body: { priceId, type: 'user' } }, 'Failed to create checkout' ); } async function createSetupSession(): Promise<{ url: string }> { return apiJson<{ url: string }>( '/api/v1/billing/setup', { method: 'POST', body: { type: 'user' } }, 'Failed to create setup session' ); } async function createPortalSession(): Promise<{ url: string }> { return apiJson<{ url: string }>( '/api/v1/billing/portal', { method: 'POST', body: { type: 'user' } }, 'Failed to create portal session' ); } function formatCurrency(amount: number, currency: string): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency.toUpperCase(), }).format(amount / 100); } function formatDate(dateString: string): string { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); } export default function Billing() { const { t } = useTranslation(); const { session } = useAuth(); const { data: billingStatus, isLoading: isLoadingStatus } = useQuery({ queryKey: ['billing-status'], queryFn: fetchBillingStatus, enabled: !!session, }); const { data: pricesData } = useQuery({ queryKey: ['billing-prices'], queryFn: fetchPrices, enabled: !!session, }); const { data: subscriptionData, isLoading: isLoadingSubscription } = useQuery({ queryKey: ['subscription'], queryFn: fetchSubscription, enabled: !!session && billingStatus?.configured, }); const checkoutMutation = useMutation({ mutationFn: createCheckout, onSuccess: (data) => { window.location.href = data.url; }, }); const portalMutation = useMutation({ mutationFn: createPortalSession, onSuccess: (data) => { window.location.href = data.url; }, }); const setupMutation = useMutation({ mutationFn: createSetupSession, onSuccess: (data) => { window.location.href = data.url; }, }); const isLoading = isLoadingStatus || isLoadingSubscription; const subscription = subscriptionData?.subscription; const hasActiveSubscription = subscription && (subscription.status === 'active' || subscription.status === 'trialing'); const currentPlanId: PlanId = subscription?.planId ?? 'free'; type DisplayPlan = Plan & { stripePriceIds?: { monthly?: string } | null }; const displayPlans: DisplayPlan[] = pricesData?.plans ?? plans; function getPriceId(plan: DisplayPlan): string | undefined { return plan.stripePriceIds?.monthly; } return ( <> {!billingStatus?.configured && !isLoadingStatus && ( Billing is not configured. Set{' '} STRIPE_SECRET_KEY and{' '} STRIPE_WEBHOOK_SECRET in your environment to enable billing. )} {/* Current subscription status */} {hasActiveSubscription && ( {t('billing.currentPlan')} You are on the{' '} {subscription.product.name}{' '} plan.

{subscription.product.name}

{subscription.status === 'trialing' && ( {t('billing.trial')} )}

{formatCurrency(subscription.price.unitAmount, subscription.price.currency)}/ {subscription.price.interval}

{subscription.cancelAtPeriodEnd && (

{t('billing.cancelsOn', { date: formatDate(subscription.currentPeriodEnd), })}

)}

{subscription.cancelAtPeriodEnd ? 'Your subscription will end on ' : 'Next billing date: '} {formatDate(subscription.currentPeriodEnd)}

)} {/* Plan selection */} {hasActiveSubscription ? t('billing.changePlan') : t('billing.choosePlan')} {hasActiveSubscription ? t('billing.changePlanDescription') : t('billing.choosePlanDescription')} {isLoading ? (
) : ( <> {/* Plan cards */}
{displayPlans.map((plan) => { const isCurrentPlan = plan.id === currentPlanId; const priceId = getPriceId(plan); return (
{plan.popular && (
{t('common.popular')}
)}

{plan.name}

{plan.description}

{formatPlanPrice(plan)}
    {plan.features.map((feature) => (
  • {feature.included ? ( ) : ( )} {feature.text}
  • ))}
{isCurrentPlan ? ( ) : hasActiveSubscription ? ( ) : plan.id === 'free' ? ( ) : ( )}
); })}
{checkoutMutation.error && (

{checkoutMutation.error.message}

)} )}
{/* Payment method */} {t('billing.paymentMethod')} {t('billing.paymentMethodDescription')} {hasActiveSubscription ? (

{t('billing.paymentOnFile')}

{t('billing.manageInPortal')}

) : (

{t('billing.noPaymentMethod')}

{t('billing.addCardDescription')}

)}
{/* Billing history */} {t('billing.billingHistory')} {t('billing.billingHistoryDescription')} {hasActiveSubscription ? (

{t('billing.viewInPortal')}

) : (

{t('billing.noBillingHistory')}

{t('billing.invoicesAppearHere')}

)}
); }