/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useState, useEffect, useMemo, useCallback } from 'react' import { Crown, RefreshCw, Sparkles, Check } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { formatQuota } from '@/lib/format' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Skeleton } from '@/components/ui/skeleton' import { TitledCard } from '@/components/ui/titled-card' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { StatusBadge, dotColorMap, textColorMap, } from '@/components/status-badge' import { getPublicPlans, getSelfSubscriptionFull, updateBillingPreference, } from '@/features/subscriptions/api' import { SubscriptionPurchaseDialog } from '@/features/subscriptions/components/dialogs/subscription-purchase-dialog' import { formatDuration, formatResetPeriod } from '@/features/subscriptions/lib' import type { PlanRecord, UserSubscriptionRecord, } from '@/features/subscriptions/types' import type { PaymentMethod, TopupInfo } from '../types' interface SubscriptionPlansCardProps { topupInfo: TopupInfo | null onAvailabilityChange?: (available: boolean) => void } function getEpayMethods(payMethods: PaymentMethod[] = []): PaymentMethod[] { return payMethods.filter( (m) => m?.type && m.type !== 'stripe' && m.type !== 'creem' ) } function getBillingPreferenceLabel( preference: string, t: (key: string) => string ): string { switch (preference) { case 'subscription_first': return t('Subscription First') case 'wallet_first': return t('Wallet First') case 'subscription_only': return t('Subscription Only') case 'wallet_only': return t('Wallet Only') default: return preference } } export function SubscriptionPlansCard({ topupInfo, onAvailabilityChange, }: SubscriptionPlansCardProps) { const { t } = useTranslation() const [plans, setPlans] = useState([]) const [activeSubscriptions, setActiveSubscriptions] = useState< UserSubscriptionRecord[] >([]) const [allSubscriptions, setAllSubscriptions] = useState< UserSubscriptionRecord[] >([]) const [billingPreference, setBillingPreference] = useState('subscription_first') const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [purchaseOpen, setPurchaseOpen] = useState(false) const [selectedPlan, setSelectedPlan] = useState(null) const enableStripe = !!topupInfo?.enable_stripe_topup const enableCreem = !!topupInfo?.enable_creem_topup const enableOnlineTopUp = !!topupInfo?.enable_online_topup const epayMethods = useMemo( () => getEpayMethods(topupInfo?.pay_methods), [topupInfo?.pay_methods] ) const fetchPlans = useCallback(async () => { try { const res = await getPublicPlans() if (res.success) { setPlans(res.data || []) } } catch { setPlans([]) } }, []) const fetchSelfSubscription = useCallback(async () => { try { const res = await getSelfSubscriptionFull() if (res.success && res.data) { setBillingPreference( res.data.billing_preference || 'subscription_first' ) setActiveSubscriptions(res.data.subscriptions || []) setAllSubscriptions(res.data.all_subscriptions || []) } } catch { // ignore } }, []) useEffect(() => { const init = async () => { setLoading(true) await Promise.all([fetchPlans(), fetchSelfSubscription()]) setLoading(false) } init() }, [fetchPlans, fetchSelfSubscription]) const handleRefresh = async () => { setRefreshing(true) try { await fetchSelfSubscription() } finally { setRefreshing(false) } } const handlePreferenceChange = async (pref: string) => { const previous = billingPreference setBillingPreference(pref) try { const res = await updateBillingPreference(pref) if (res.success) { toast.success(t('Updated successfully')) const normalized = res.data?.billing_preference || pref setBillingPreference(normalized) } else { toast.error(res.message || t('Update failed')) setBillingPreference(previous) } } catch { toast.error(t('Request failed')) setBillingPreference(previous) } } const hasActive = activeSubscriptions.length > 0 const hasAny = allSubscriptions.length > 0 const isAvailable = loading || plans.length > 0 || hasAny const disablePref = !hasActive const isSubPref = billingPreference === 'subscription_first' || billingPreference === 'subscription_only' const displayPref = disablePref && isSubPref ? 'wallet_first' : billingPreference const planPurchaseCountMap = useMemo(() => { const map = new Map() for (const sub of allSubscriptions) { const planId = sub?.subscription?.plan_id if (!planId) continue map.set(planId, (map.get(planId) || 0) + 1) } return map }, [allSubscriptions]) useEffect(() => { onAvailabilityChange?.(isAvailable) }, [isAvailable, onAvailabilityChange]) const planTitleMap = useMemo(() => { const map = new Map() for (const p of plans) { if (p?.plan?.id) { map.set(p.plan.id, p.plan.title || '') } } return map }, [plans]) const getRemainingDays = (sub: UserSubscriptionRecord) => { const endTime = sub?.subscription?.end_time || 0 if (!endTime) return 0 const now = Date.now() / 1000 return Math.max(0, Math.ceil((endTime - now) / 86400)) } const getUsagePercent = (sub: UserSubscriptionRecord) => { const total = Number(sub?.subscription?.amount_total || 0) const used = Number(sub?.subscription?.amount_used || 0) if (total <= 0) return 0 return Math.round((used / total) * 100) } if (loading) { return (
{Array.from({ length: 3 }).map((_, i) => ( ))}
) } if (plans.length === 0 && !hasAny) { return null } return ( <> } contentClassName='space-y-4 sm:space-y-5' > {/* My subscriptions & billing preference */}
{t('My Subscriptions')}
{disablePref && isSubPref && (

{t( 'Preference saved as {{pref}}, but no active subscription. Wallet will be used automatically.', { pref: billingPreference === 'subscription_only' ? t('Subscription Only') : t('Subscription First'), } )}

)} {hasAny && ( <>
{allSubscriptions.map((sub) => { const subscription = sub.subscription const totalAmount = Number(subscription?.amount_total || 0) const usedAmount = Number(subscription?.amount_used || 0) const remainAmount = totalAmount > 0 ? Math.max(0, totalAmount - usedAmount) : 0 const planTitle = planTitleMap.get(subscription?.plan_id) || '' const remainDays = getRemainingDays(sub) const usagePercent = getUsagePercent(sub) const now = Date.now() / 1000 const isExpired = (subscription?.end_time || 0) < now const isCancelled = subscription?.status === 'cancelled' const isActive = subscription?.status === 'active' && !isExpired return (
{planTitle ? `${planTitle} · ${t('Subscription')} #${subscription?.id}` : `${t('Subscription')} #${subscription?.id}`} {isActive ? ( ) : isCancelled ? ( ) : ( )}
{isActive && ( {t('{{count}} days remaining', { count: remainDays, })} )}
{isActive ? t('Until') : isCancelled ? t('Cancelled at') : t('Expired at')}{' '} {new Date( (subscription?.end_time || 0) * 1000 ).toLocaleString()}
{isActive && (subscription?.next_reset_time ?? 0) > 0 && (
{t('Next reset')}:{' '} {new Date( subscription!.next_reset_time! * 1000 ).toLocaleString()}
)}
{t('Total Quota')}:{' '} {totalAmount > 0 ? ( } > {formatQuota(usedAmount)}/ {formatQuota(totalAmount)} · {t('Remaining')}{' '} {formatQuota(remainAmount)} {t('Raw Quota')}: {usedAmount}/{totalAmount} ·{' '} {t('Remaining')} {remainAmount} ) : ( t('Unlimited') )} {totalAmount > 0 && ( {t('Used')} {usagePercent}% )}
{totalAmount > 0 && isActive && ( )}
) })}
)} {!hasAny && (

{t('Subscribe to a plan for model access')}

)}
{/* Available plans grid */} {plans.length > 0 ? (
{plans.map((p, index) => { const plan = p?.plan if (!plan) return null const totalAmount = Number(plan.total_amount || 0) const price = Number(plan.price_amount || 0).toFixed(2) const isPopular = index === 0 && plans.length > 1 const limit = Number(plan.max_purchase_per_user || 0) const count = planPurchaseCountMap.get(plan.id) || 0 const reached = limit > 0 && count >= limit const benefits = [ `${t('Validity Period')}: ${formatDuration(plan, t)}`, formatResetPeriod(plan, t) !== t('No Reset') ? `${t('Quota Reset')}: ${formatResetPeriod(plan, t)}` : null, totalAmount > 0 ? `${t('Total Quota')}: ${formatQuota(totalAmount)}` : `${t('Total Quota')}: ${t('Unlimited')}`, limit > 0 ? `${t('Purchase Limit')}: ${limit}` : null, plan.upgrade_group ? `${t('Upgrade Group')}: ${plan.upgrade_group}` : null, ].filter(Boolean) as string[] return (

{plan.title || t('Subscription Plans')}

{plan.subtitle && (

{plan.subtitle}

)}
{isPopular && ( {t('Recommended')} )}
${price}
{benefits.map((label) => (
{label}
))}
{reached ? ( }> {t('Purchase limit reached')} ({count}/{limit}) ) : ( )}
) })}
) : (

{t('No plans available')}

)}
{ setPurchaseOpen(open) if (!open) { fetchSelfSubscription() } }} plan={selectedPlan} enableStripe={enableStripe} enableCreem={enableCreem} enableOnlineTopUp={enableOnlineTopUp} epayMethods={epayMethods} purchaseLimit={ selectedPlan?.plan?.max_purchase_per_user ? Number(selectedPlan.plan.max_purchase_per_user) : undefined } purchaseCount={ selectedPlan?.plan?.id ? planPurchaseCountMap.get(selectedPlan.plan.id) : undefined } /> ) }