import { useState, useEffect } from 'react' import { CreditCard, Loader2, CircleHelp, X } from 'lucide-react' import api from '../services/api' import { useToast } from '../components/ToastProvider' import { getAuthUser } from '../utils/authSession' import { useNavigate } from 'react-router-dom' import { readPageCache, writePageCache } from '../utils/pageCache' export default function Billing() { const cached = readPageCache('billing_page', 90 * 1000) const navigate = useNavigate() const toast = useToast() const [stats, setStats] = useState(cached?.stats || null) const [plan, setPlan] = useState(cached?.plan || null) const [limits, setLimits] = useState(cached?.limits || null) const [billingConfig, setBillingConfig] = useState(cached?.billingConfig || null) const [payments, setPayments] = useState(cached?.payments || []) const [loading, setLoading] = useState(!cached) const [actionLoading, setActionLoading] = useState(false) const [payingCode, setPayingCode] = useState('') const [selectedPackageCode, setSelectedPackageCode] = useState('growth') const [showTrialApplyModal, setShowTrialApplyModal] = useState(false) const [showQuotaHelp, setShowQuotaHelp] = useState(false) const [showPurchaseHelp, setShowPurchaseHelp] = useState(false) const [showHistoryHelp, setShowHistoryHelp] = useState(false) useEffect(() => { fetchBillingInfo() }, []) const fetchBillingInfo = async () => { const silent = Boolean(stats && plan && limits) if (!silent) setLoading(true) try { const [profileRes, paymentsRes, billingConfigRes] = await Promise.all([ api.get('/api/user/profile'), api.get('/api/user/billing/payments'), api.get('/api/user/billing/config'), ]) const nextStats = profileRes.data.stats const nextPlan = profileRes.data.plan const nextLimits = profileRes.data.limits const nextBillingConfig = billingConfigRes.data.config || null const nextPayments = paymentsRes.data.payments || [] setStats(nextStats) setPlan(nextPlan) setLimits(nextLimits) setBillingConfig(nextBillingConfig) setPayments(nextPayments) writePageCache('billing_page', { stats: nextStats, plan: nextPlan, limits: nextLimits, billingConfig: nextBillingConfig, payments: nextPayments, }) } catch (err) { console.error("Failed to fetch billing info", err) } finally { if (!silent) setLoading(false) } } const handleApply = async () => { setActionLoading(true) try { await api.post('/api/user/trial/apply') const res = await api.post('/api/user/trial/activate') toast.success(res.data.message || 'Free trial activated successfully') setShowTrialApplyModal(false) await fetchBillingInfo() } catch (err) { toast.error(err.response?.data?.error || 'Failed to activate free trial') } finally { setActionLoading(false) } } const handleActivate = async () => { setActionLoading(true) try { const res = await api.post('/api/user/trial/activate') toast.success(res.data.message || 'Free trial activated') await fetchBillingInfo() } catch (err) { toast.error(err.response?.data?.error || 'Failed to activate free trial') } finally { setActionLoading(false) } } const packages = [ { code: 'starter', label: 'Starter Kit', credits: 100, amount: 20, save: '', popular: false }, { code: 'growth', label: 'Growth Bundle', credits: 500, amount: 90, save: '10% off', popular: true }, { code: 'volume', label: 'Volume Pack', credits: 1000, amount: 170, save: '15% off', popular: false }, { code: 'enterprise', label: 'Enterprise', credits: 5000, amount: 800, save: '20% off', popular: false }, ] const selectedPackage = packages.find((pkg) => pkg.code === selectedPackageCode) || null const handlePayViaUpi = async (pkg) => { if (!billingConfig?.payments_enabled) { toast.error('Payments are disabled by admin right now') return } if (!billingConfig?.upi_available) { toast.error('UPI is not configured by admin yet') return } setPayingCode(pkg.code) try { const res = await api.post('/api/user/billing/payment-request', { package_name: pkg.label, credits: pkg.credits, amount_inr: pkg.amount, }) const payment = res.data.payment if (payment?.upi_link) { window.open(payment.upi_link, '_blank', 'noopener,noreferrer') } toast.info('UPI intent opened. Submit UTR after payment.') const utr = window.prompt(`Enter UTR for ${payment.request_ref} after payment (optional now):`, '') if (utr !== null) { await api.post(`/api/user/billing/payment-request/${payment.request_ref}/submit`, { utr: utr || '' }) toast.success('Payment submitted for admin approval') } await fetchBillingInfo() } catch (err) { toast.error(err.response?.data?.error || 'Failed to start UPI payment') } finally { setPayingCode('') } } if (loading) return
const isLiveMode = plan?.mode === 'live' const activeTotal = isLiveMode ? (stats?.live_total_credits || 0) : (stats?.trial_total_credits || 0) const activeRemaining = isLiveMode ? (stats?.live_remaining_credits || 0) : (stats?.trial_remaining_credits || 0) const activeUsed = isLiveMode ? (stats?.live_used_this_month || 0) : (stats?.trial_used_credits || 0) const activeLabel = isLiveMode ? 'Live Credits Remaining' : 'Sandbox Credits Remaining' const sandboxBase = limits?.sandbox_monthly_message_limit_base || 500 const liveBase = limits?.live_monthly_message_limit_base || 100 const liveBonus = stats?.live_bonus_credits || 0 const activeProgress = activeTotal ? (activeUsed / activeTotal) * 100 : 0 const currentUser = getAuthUser() const isAdminUser = currentUser?.role === 'admin' const formatDate = (value) => { if (!value) return '-' const parsed = new Date(value) if (Number.isNaN(parsed.getTime())) return '-' return parsed.toLocaleString() } return (

Plans & Billing

Manage quotas, purchase live credits, and track approvals in one place.

Current Plan
{plan?.name || 'Free Trial'}
{plan?.label || 'Sandbox / Free Trial'} {isLiveMode ? 'Live mode' : 'Sandbox mode'}
{activeLabel}
{activeRemaining} / {activeTotal}
Used: {activeUsed}
Sandbox Bucket
{stats?.trial_remaining_credits || 0} / {stats?.trial_total_credits || 0}
Base: {sandboxBase} • Used: {stats?.trial_used_credits || 0}
Live Bucket
{stats?.live_remaining_credits || 0} / {stats?.live_total_credits || 0}
Base: {liveBase} + Purchased: {liveBonus}
Sandbox and Live are separate counters. Sandbox is self-number only. Purchased packs add credits only to Live.
{plan?.trial_status === 'not_applied' && ( )} {plan?.trial_status === 'applied' && ( )} {plan?.trial_status === 'active' && (
{isAdminUser ? ( ) : null}
)}

Buy Credits

Select bundle and pay via UPI.

{billingConfig?.payments_enabled ? 'Payments Enabled' : 'Payments Disabled'}
{billingConfig?.payment_note || 'Pay via UPI and submit UTR for admin approval.'}
Package
OTPs
Price
{packages.map((pkg) => { const isSelected = selectedPackageCode === pkg.code return ( ) })}
Selected: {selectedPackage?.label || '-'} {selectedPackage ? ` • ${selectedPackage.credits} OTP credits • ₹${selectedPackage.amount}` : ''}

Payment Requests & History

{payments.length === 0 ? (
No payment requests yet.
) : payments.map((payment) => (
{payment.request_ref}
{payment.status}
{payment.package_name} • {payment.credits} credits • ₹{Number(payment.amount_inr).toFixed(2)}
Created: {formatDate(payment.created_at)}
{payment.utr ?
UTR: {payment.utr}
: null} {payment.admin_note ?
Note: {payment.admin_note}
: null}
))}
{showQuotaHelp ? (
Sandbox bucket: self number only, separate from live.
Live bucket: any valid number, separate from sandbox.
Purchased packs add credits only to live bucket.
) : null} {showTrialApplyModal ? (
1. Sandbox trial quota: {stats?.trial_total_credits || 500} OTPs/month (self WhatsApp number only).
2. Live starter quota: {stats?.live_total_credits || 100} OTPs/month (can send to valid external numbers).
3. Sandbox and Live use separate API key sets and separate usage counters.
4. Misuse, spam, or policy violations may suspend trial access.
) : null} {showPurchaseHelp ? (
1. Select package.
2. Click Purchase Selected.
3. Complete UPI payment and submit UTR.
4. Admin approves payment and credits move to live bucket.
) : null} {showHistoryHelp ? (
pending: created but proof not submitted.
submitted: UTR submitted, waiting admin review.
approved: credits added to live bucket.
rejected: not approved by admin.
) : null} ) }