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.'}
{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 ? (
) : null}
{showTrialApplyModal ? (
!actionLoading && setShowTrialApplyModal(false)}
className="absolute inset-0 bg-black/70 border-none cursor-pointer"
/>
Activate Free Trial
setShowTrialApplyModal(false)}
disabled={actionLoading}
className="neu-btn p-2"
>
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.
setShowTrialApplyModal(false)}
disabled={actionLoading}
className="neu-btn px-5 py-2.5"
>
Cancel
{actionLoading ? Activating... : 'Agree & Activate Trial'}
) : null}
{showPurchaseHelp ? (
setShowPurchaseHelp(false)} className="absolute inset-0 bg-black/70 border-none cursor-pointer" />
Purchase Flow
setShowPurchaseHelp(false)} className="neu-btn p-2">
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 ? (
setShowHistoryHelp(false)} className="absolute inset-0 bg-black/70 border-none cursor-pointer" />
Request Status
setShowHistoryHelp(false)} className="neu-btn p-2">
pending: created but proof not submitted.
submitted: UTR submitted, waiting admin review.
approved: credits added to live bucket.
rejected: not approved by admin.
) : null}
)
}