/** * BoostMedia AI Content Generator Admin - Usage & Costs Page * * Adapted from BoostBot's UsageDashboard for the BoostContent plugin. * * @package BoostMedia_AI * @license GPL-2.0-or-later */ import { useState, useEffect, useMemo, useRef, useCallback } from 'react' import { Sparkles, ShoppingCart, RefreshCw, Gift, CheckCircle, PartyPopper, Calendar, Zap, } from 'lucide-react' import { Header } from '../components/layout/Header' import { Card, CardContent, Button, Badge, HelpTooltip } from '../components/common' import { useCreditsStatus, usePricingCatalog, useCreateSubscription, useCancelSubscription } from '../hooks/useCredits' import type { PricingPackage, PricingSubscription } from '../hooks/useCredits' import { endpoints, getErrorMessage } from '../api/client' import type { AffiliatePurchasePayload } from '../api/client' import { SetupBanner } from '../components/onboarding/SetupBanner' import { t, tf, getDateLocale } from '../lib/i18n' const AFFILIATE_PENDING_SUBSCRIPTION_KEY = 'bmai_pending_affiliate_subscription' interface PayPalApproveData { orderID?: string } interface PayPalButtonsInstance { render: (host: HTMLElement) => Promise } interface PayPalNamespace { Buttons: (config: { onInit?: () => void style?: Record createOrder?: () => Promise onApprove?: (data: PayPalApproveData) => Promise onCancel?: () => void onError?: (error: unknown) => void }) => PayPalButtonsInstance } function getPaypalWindow(): Window & { paypal?: PayPalNamespace } { return window as Window & { paypal?: PayPalNamespace } } function errorMessage(error: unknown, fallback: string): string { return getErrorMessage(error, fallback) } function currencySymbol(c?: string) { if (!c) return '' const u = c.toUpperCase() return u === 'ILS' ? '₪' : u === 'USD' ? '$' : `${u} ` } function fmtPrice(amount: number, currency?: string) { return (currency || '').toUpperCase() === 'ILS' ? String(Math.round(amount)) : amount.toFixed(2) } function discountPct(ppu: number, basePpu: number) { if (basePpu <= 0 || ppu >= basePpu) return 0 return Math.round((1 - ppu / basePpu) * 100) } function loadPayPalSdk(clientId: string, currency: string): Promise { return new Promise((resolve, reject) => { if (getPaypalWindow().paypal) { resolve(); return } const existing = document.getElementById('bc-paypal-sdk') as HTMLScriptElement | null if (existing) { existing.addEventListener('load', () => resolve(), { once: true }) existing.addEventListener('error', () => reject(new Error('Failed to load PayPal SDK.')), { once: true }) return } const paypalLocale = getDateLocale() === 'he-IL' ? 'he_IL' : 'en_US' const script = document.createElement('script') script.id = 'bc-paypal-sdk' script.src = `https://www.paypal.com/sdk/js?client-id=${encodeURIComponent(clientId)}¤cy=${encodeURIComponent(currency)}&locale=${paypalLocale}&intent=capture` script.async = true script.onload = () => resolve() script.onerror = () => reject(new Error('Failed to load PayPal SDK.')) document.head.appendChild(script) }) } /* ── Confetti Celebration ── */ const CONFETTI_COLORS = ['#10b981', '#06b6d4', '#6366f1', '#f59e0b', '#ec4899', '#8b5cf6', '#14b8a6'] function ConfettiCelebration({ active }: { active: boolean }) { const particles = useMemo(() => Array.from({ length: 50 }, (_, i) => ({ id: i, color: CONFETTI_COLORS[i % CONFETTI_COLORS.length], left: Math.random() * 100, delay: Math.random() * 1.2, duration: 2.5 + Math.random() * 2, size: 6 + Math.random() * 8, heightRatio: 0.4 + Math.random() * 0.6, rotate: Math.random() * 360, drift: (Math.random() - 0.5) * 120, })), []) if (!active) return null return (
{particles.map((p) => ( ))}
) } function PurchaseSuccessOverlay({ show, credits, onClose }: { show: boolean; credits: number; onClose: () => void }) { const onCloseRef = useRef(onClose) onCloseRef.current = onClose useEffect(() => { if (!show) return const timer = setTimeout(() => onCloseRef.current(), 6000) return () => clearTimeout(timer) }, [show]) if (!show) return null return (
e.stopPropagation()}>

{t('Purchase completed successfully!')}

{credits.toLocaleString()} {t('BoostCoin added to your account')}

{t('Invoice will be sent to your email from PayPal.')}

) } /* ── Credit Slider ── */ function CreditSlider({ items, selectedIndex, onSelect }: { items: { id: string; label: string }[] selectedIndex: number onSelect: (i: number) => void }) { if (items.length <= 1) return null return (
{t('Choose BoostCoin amount')}
{items.map((item, i) => ( ))}
) } /* ── Pricing Table ── */ function PricingTable({ rows, selectedIndex, onSelect }: { rows: { name: string; qty: number; price: string; perUnit: string; discount: number; popular?: boolean }[] selectedIndex: number onSelect: (i: number) => void }) { return (
{rows.map((r, i) => ( onSelect(i)} className={`border-t border-bc-gray-50 cursor-pointer transition-colors duration-150 ${ i === selectedIndex ? 'bg-bc-primary-light' : 'hover:bg-bc-gray-50' }`} > ))}
{t('Package')} {t('Quantity')} {t('Price')} {t('Per Coin')} {t('Discount')}
{r.name} {r.popular && {t('Popular')}} {r.qty.toLocaleString()} BoostCoin {r.price} {r.perUnit} {r.discount > 0 ? ( {r.discount}%− ) : ( )}
) } /* ═══════════════════════════════════════════════════════ UsagePage – Main Component ═══════════════════════════════════════════════════════ */ export default function UsagePage() { const credits = useCreditsStatus() const catalog = usePricingCatalog() const createSub = useCreateSubscription() const cancelSub = useCancelSubscription() const refetchCredits = credits.refetch const [cancelSuccess, setCancelSuccess] = useState(false) const [pkgIdx, setPkgIdx] = useState(0) const [subIdx, setSubIdx] = useState(0) const [paypalOpen, setPaypalOpen] = useState(false) const [paypalConfigLoading, setPaypalConfigLoading] = useState(false) const [paypalButtonsLoading, setPaypalButtonsLoading] = useState(false) const [paypalReady, setPaypalReady] = useState(false) const [paypalBusy, setPaypalBusy] = useState(false) const [paypalError, setPaypalError] = useState(null) const [paypalConfig, setPaypalConfig] = useState<{ paypal_client_id: string; paypal_env: string } | null>(null) const [purchaseSuccess, setPurchaseSuccess] = useState<{ credits: number } | null>(null) const paypalHostRef = useRef(null) const pkgInit = useRef(false) const subInit = useRef(false) const packages = useMemo( () => [...(catalog.data?.packages || [])].sort((a, b) => a.credits - b.credits), [catalog.data?.packages] ) const subs = useMemo( () => [...(catalog.data?.subscriptions || [])].sort((a, b) => a.price - b.price), [catalog.data?.subscriptions] ) useEffect(() => { if (packages.length > 0 && !pkgInit.current) { pkgInit.current = true const i = packages.findIndex((p) => p.popular) setPkgIdx(i >= 0 ? i : Math.floor(packages.length / 2)) } }, [packages]) useEffect(() => { if (subs.length > 0 && !subInit.current) { subInit.current = true const i = subs.findIndex((s) => s.popular) setSubIdx(i >= 0 ? i : 0) } }, [subs]) const sPkgI = Math.max(0, Math.min(pkgIdx, packages.length - 1)) const selPkg: PricingPackage | null = packages[sPkgI] || null const sSubI = Math.max(0, Math.min(subIdx, subs.length - 1)) const selSub: PricingSubscription | null = subs[sSubI] || null const cd = credits.data const freeTotal = cd?.credits?.free?.total ?? 0 const freeUsed = cd?.credits?.free?.used ?? 0 const freeRemaining = Math.max(0, freeTotal - freeUsed) const purchasedTotal = cd?.credits?.purchased?.total ?? cd?.boost_credits?.total ?? 0 const purchasedUsed = cd?.credits?.purchased?.used ?? cd?.boost_credits?.used ?? 0 const purchasedRemaining = Math.max(0, purchasedTotal - purchasedUsed) const subTotal = cd?.credits?.subscription?.total ?? 0 const subUsed = cd?.credits?.subscription?.used ?? 0 const subRemaining = Math.max(0, subTotal - subUsed) const subStatus = cd?.credits?.subscription?.status ?? 'none' const subRenewAt = cd?.credits?.subscription?.renew_at ?? null const dailyGenerated = cd?.daily_usage?.generated_today ?? cd?.daily_usage?.articles_generated ?? 0 const dailyLimit = cd?.daily_usage?.limit ?? cd?.daily_usage?.article_limit ?? 1000 const dailyRemaining = cd?.daily_usage?.remaining ?? cd?.daily_usage?.articles_remaining ?? Math.max(0, dailyLimit - dailyGenerated) const blocked = cd ? !cd.allowed : false const currency = catalog.data?.currency const sym = currencySymbol(currency) const coinBase = 1.0 const selPkgDisc = selPkg ? discountPct(selPkg.price / selPkg.credits, coinBase) : 0 const freePct = freeTotal > 0 ? Math.min(100, (freeRemaining / freeTotal) * 100) : 0 const purchasedPct = purchasedTotal > 0 ? Math.min(100, (purchasedRemaining / purchasedTotal) * 100) : 0 const subPct = subTotal > 0 ? Math.min(100, (subRemaining / subTotal) * 100) : 0 const dailyPct = dailyLimit > 0 ? Math.min(100, (dailyGenerated / dailyLimit) * 100) : 0 const renewLabel = subRenewAt ? new Date(subRenewAt).toLocaleDateString(getDateLocale(), { day: 'numeric', month: 'short' }) : null const reportAffiliatePurchaseSafely = useCallback(async (payload: AffiliatePurchasePayload) => { try { await endpoints.reportAffiliatePurchase(payload) } catch (error) { // Affiliate reporting should not block a purchase that already succeeded. console.warn('BoostAffiliate purchase reporting failed', error) } }, []) /* ── PayPal inline purchase ── */ const openPaypal = async () => { if (paypalOpen && !paypalBusy) { setPaypalOpen(false); return } setPaypalOpen(true) setPaypalError(null) if (paypalConfig || paypalConfigLoading) return setPaypalConfigLoading(true) try { const res = await endpoints.getPaypalConfig() setPaypalConfig(res.data as { paypal_client_id: string; paypal_env: string }) } catch (e) { setPaypalError(errorMessage(e, t('Error loading PayPal'))) } finally { setPaypalConfigLoading(false) } } useEffect(() => { if (!paypalOpen || !paypalConfig || !selPkg || !paypalHostRef.current) return let cancelled = false const host = paypalHostRef.current ;(async () => { try { setPaypalReady(false) setPaypalButtonsLoading(true) setPaypalError(null) await loadPayPalSdk(paypalConfig.paypal_client_id, (currency || 'USD').toUpperCase()) if (cancelled) return const paypal = getPaypalWindow().paypal if (!paypal?.Buttons) throw new Error('PayPal SDK not available') if (!host) return host.innerHTML = '' const buttons = paypal.Buttons({ onInit: () => { if (!cancelled) setPaypalReady(true) }, style: { layout: 'vertical', color: 'gold', shape: 'rect', label: 'pay', height: 45 }, createOrder: async () => { setPaypalBusy(true) const r = await endpoints.createPurchaseOrder(selPkg.id) const d = r.data as { order_id?: string } if (!d?.order_id) throw new Error('Failed to create order') return d.order_id }, onApprove: async (data: { orderID?: string }) => { try { const oid = data?.orderID || '' if (!oid) throw new Error('Missing order ID') await endpoints.capturePurchaseOrder(oid) await reportAffiliatePurchaseSafely({ amount: selPkg.price, currency: (currency || 'ILS').toUpperCase(), package_id: selPkg.id, }) await new Promise((r) => setTimeout(r, 1200)) await refetchCredits() if (!cancelled) { setPaypalOpen(false); setPurchaseSuccess({ credits: selPkg.credits }) } } catch (e) { if (!cancelled) setPaypalError(errorMessage(e, t('Purchase failed'))) } finally { if (!cancelled) setPaypalBusy(false) } }, onCancel: () => { if (!cancelled) setPaypalBusy(false) }, onError: (err) => { if (!cancelled) { setPaypalBusy(false); setPaypalError(errorMessage(err, t('Payment error'))) } }, }) await buttons.render(host) if (!cancelled) setPaypalReady(true) } catch (e) { if (!cancelled) setPaypalError(errorMessage(e, t('Error loading PayPal'))) } finally { if (!cancelled) setPaypalButtonsLoading(false) } })() return () => { cancelled = true; if (host) host.innerHTML = '' } }, [currency, paypalConfig, paypalOpen, refetchCredits, reportAffiliatePurchaseSafely, selPkg]) const selSubDisc = selSub ? discountPct(selSub.price / selSub.allowance, coinBase) : 0 /* ── Pricing table rows ── */ const pkgRows = packages.map((p) => ({ name: p.name, qty: p.credits, price: `${sym}${fmtPrice(p.price, currency)}`, perUnit: `${sym}${(p.price / p.credits).toFixed(3)}`, discount: discountPct(p.price / p.credits, coinBase), popular: p.popular, })) const subRows = subs.map((s) => ({ name: s.name, qty: s.allowance, price: `${sym}${fmtPrice(s.price, currency)}`, perUnit: `${sym}${(s.price / s.allowance).toFixed(3)}`, discount: discountPct(s.price / s.allowance, coinBase), popular: s.popular, })) // Handle subscription return from PayPal useEffect(() => { let active = true void (async () => { const params = new URLSearchParams(window.location.search) if (params.get('bmai_subscription') !== 'success') return const raw = sessionStorage.getItem(AFFILIATE_PENDING_SUBSCRIPTION_KEY) if (raw) { try { const payload = JSON.parse(raw) as AffiliatePurchasePayload await reportAffiliatePurchaseSafely(payload) } catch (error) { console.warn('Failed to restore pending affiliate subscription payload', error) } finally { sessionStorage.removeItem(AFFILIATE_PENDING_SUBSCRIPTION_KEY) } } if (!active) return await refetchCredits() if (!active) return const cleanUrl = window.location.href.replace(/[?&]bmai_subscription=[^&#]+/, '') window.history.replaceState({}, '', cleanUrl) })() return () => { active = false } }, [refetchCredits, reportAffiliatePurchaseSafely]) if (credits.loading && !cd) { return (
{t('Loading usage data...')}
) } if (!credits.loading && credits.error && !cd) { return (
{t('Failed to load credit information')}
) } if (!credits.loading && !cd) { return (
{t('Credit information unavailable')}
) } return (
{credits.refreshing && (
{t('Refreshing...')}
)} {/* Free Credits Card */} {freeTotal > 0 && (
{t('Free BoostCoin')}
{t('Installation gift')}
{freeRemaining.toLocaleString()}
{t('Used:')} {freeUsed.toLocaleString()} {t('Total:')} {freeTotal.toLocaleString()}
20 ? 'bg-emerald-500' : freePct > 0 ? 'bg-amber-500' : 'bg-red-500'}`} style={{ width: `${freePct}%` }} />

{freeRemaining > 0 ? t('Used first before any other type. Identical to regular BoostCoin — same AI model and quality.') : t('All free BoostCoin used. Consumption continues from subscription or purchased.')}

)} {/* Credit Balance Cards */}
{/* Purchased Credits */}
{t('Purchased BoostCoin')}
{t('Accumulating')}
{purchasedRemaining.toLocaleString()}
{t('Used:')} {purchasedUsed.toLocaleString()} {t('Total:')} {purchasedTotal.toLocaleString()}
20 ? 'bg-bc-primary' : 'bg-red-500'}`} style={{ width: `${purchasedPct}%` }} />

{t('Never expire, accumulate over time')}

{/* Subscription Credits */}
{t('Subscription BoostCoin')}
{subStatus === 'active' ? {t('Active')} : subStatus === 'paused' ? {t('Paused')} : subStatus === 'cancelled' ? {t('Cancelled')} : {t('No subscription')} }
{subStatus !== 'none' ? subRemaining.toLocaleString() : '—'}
{subStatus !== 'none' ? ( <>
{t('Used:')} {subUsed.toLocaleString()} {t('Quota:')} {subTotal.toLocaleString()}
20 ? 'bg-blue-500' : 'bg-red-500'}`} style={{ width: `${subPct}%` }} />
) :
}
{renewLabel && ( {t('Renewal:')} {renewLabel} )}
{t('Daily article usage')}
0 ? 'accent' : 'warning'}> {dailyRemaining > 0 ? t('Available') : t('Limit reached')}
{dailyRemaining.toLocaleString()}
{t('Generated today:')} {dailyGenerated.toLocaleString()} {t('Daily cap:')} {dailyLimit.toLocaleString()}
0 ? 'bg-violet-500' : 'bg-red-500'}`} style={{ width: `${dailyPct}%` }} />

{t('Resets at midnight UTC. Large batch plans may stop early when the daily cap is reached.')}

{t('Consumption order: Subscription → Purchased (subscription coins are consumed first, unused balances roll over to purchased)')}

{t('Estimated rate: ~4-5 BoostCoins per article (~700 words, includes research, image, and plagiarism check).')}

{t('~0.6 BoostCoins per 100 words (actual cost calculated after each generation)')}

{/* Blocked warning */} {blocked && (
{t('No BoostCoin available — ')} {t('All BoostCoin used. Purchase a package to continue.')}
)} {/* One-Time Purchase */}

{t('One-time purchase')}

{t('Purchase BoostCoin that never expire')}

{catalog.loading ? (
{t('Loading packages...')}
) : packages.length === 0 ? (
{t('No packages available right now')}
) : ( <> ({ id: p.id, label: p.credits.toLocaleString() }))} selectedIndex={sPkgI} onSelect={setPkgIdx} /> {selPkg && (
{sym}{fmtPrice(selPkg.price, currency)}
{selPkg.credits.toLocaleString()} BoostCoin {selPkgDisc > 0 && ( <> {selPkgDisc}{t('% discount')} )}
{paypalOpen && (
{(paypalConfigLoading || paypalButtonsLoading || !paypalReady) && !paypalError && (
{t('Loading payment options...')}
)} {paypalError && (
{paypalError}
)}

{t('Pay with PayPal account or credit card via PayPal.')}

)}
)}

{t('Purchased BoostCoin never expire and always accumulate. Coins are used for API calls to various AI models (content creation, images and more).')}

)} {/* Monthly Subscriptions */} {subs.length > 0 && (

{t('Monthly Subscription')}

{t('Monthly BoostCoin allowance that resets each billing cycle')}

{(subStatus === 'active' || cancelSuccess) && (
{cancelSuccess ? (
{t('Subscription cancelled successfully')} {subRenewAt && ( — {tf('Active until %s', new Date(subRenewAt).toLocaleDateString(getDateLocale(), { day: 'numeric', month: 'long', year: 'numeric' }))} )}
) : (
{t('You currently have an active subscription. You can cancel or change plans via PayPal.')}
)} {cancelSub.error && (
{cancelSub.error}
)}
)} ({ id: s.id, label: s.allowance.toLocaleString() }))} selectedIndex={sSubI} onSelect={setSubIdx} /> {selSub && (
{sym}{fmtPrice(selSub.price, currency)} /{t('month')}
{tf('%s BoostCoin per month', selSub.allowance.toLocaleString())} {selSubDisc > 0 && ( <> {selSubDisc}{t('% discount')} )}
{createSub.error && (
{createSub.error}
)}
)}

{t('Subscription coins refresh each billing cycle. Unused subscription coins roll over into your purchased balance.')}

)}
setPurchaseSuccess(null)} />
) }