455 lines
24 KiB
JavaScript
455 lines
24 KiB
JavaScript
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 <div className="flex justify-center py-20"><Loader2 className="animate-spin text-accent" /></div>
|
|
|
|
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 (
|
|
<div className="flex flex-col gap-6">
|
|
<div>
|
|
<h2 className="text-2xl font-black text-text-primary tracking-tight m-0 mb-1">Plans & Billing</h2>
|
|
<p className="text-text-secondary text-sm font-semibold m-0">
|
|
Manage quotas, purchase live credits, and track approvals in one place.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6 items-start">
|
|
<div className="xl:col-span-8 flex flex-col gap-6">
|
|
<div className="neu-raised rounded-2xl p-6">
|
|
<div className="flex flex-wrap items-center justify-between gap-3 mb-5">
|
|
<div>
|
|
<div className="text-xs font-black uppercase tracking-widest text-text-muted">Current Plan</div>
|
|
<div className="text-2xl font-black text-text-primary mt-1">{plan?.name || 'Free Trial'}</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button type="button" onClick={() => setShowQuotaHelp(true)} className="neu-btn w-8 h-8 rounded-full" title="Quota help">
|
|
<CircleHelp size={13} />
|
|
</button>
|
|
<span className="badge-neutral">{plan?.label || 'Sandbox / Free Trial'}</span>
|
|
<span className="badge-neutral">{isLiveMode ? 'Live mode' : 'Sandbox mode'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="neu-inset rounded-xl p-4">
|
|
<div className="text-[11px] font-black uppercase tracking-wider text-text-muted mb-2">{activeLabel}</div>
|
|
<div className="text-2xl font-black text-accent">{activeRemaining} / {activeTotal}</div>
|
|
<div className="text-[11px] font-semibold text-text-secondary mt-1">Used: {activeUsed}</div>
|
|
<div className="mt-3 h-2 rounded-full overflow-hidden" style={{ background: 'rgba(255,255,255,0.06)' }}>
|
|
<div className="h-full" style={{ width: `${activeProgress}%`, background: 'linear-gradient(90deg, var(--color-accent) 0%, var(--color-accent-dark) 100%)' }} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="neu-inset rounded-xl p-4">
|
|
<div className="text-[11px] font-black uppercase tracking-wider text-text-muted mb-2">Sandbox Bucket</div>
|
|
<div className="text-xl font-black text-text-primary">{stats?.trial_remaining_credits || 0} / {stats?.trial_total_credits || 0}</div>
|
|
<div className="text-[11px] font-semibold text-text-secondary mt-1">Base: {sandboxBase} • Used: {stats?.trial_used_credits || 0}</div>
|
|
</div>
|
|
|
|
<div className="neu-inset rounded-xl p-4">
|
|
<div className="text-[11px] font-black uppercase tracking-wider text-text-muted mb-2">Live Bucket</div>
|
|
<div className="text-xl font-black text-text-primary">{stats?.live_remaining_credits || 0} / {stats?.live_total_credits || 0}</div>
|
|
<div className="text-[11px] font-semibold text-text-secondary mt-1">Base: {liveBase} + Purchased: {liveBonus}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs font-semibold text-text-secondary mt-4">
|
|
Sandbox and Live are separate counters. Sandbox is self-number only. Purchased packs add credits only to Live.
|
|
</div>
|
|
|
|
<div className="mt-5">
|
|
{plan?.trial_status === 'not_applied' && (
|
|
<button onClick={() => setShowTrialApplyModal(true)} disabled={actionLoading} className="neu-btn-accent px-6 py-3 w-full md:w-auto">
|
|
Apply Free Trial
|
|
</button>
|
|
)}
|
|
{plan?.trial_status === 'applied' && (
|
|
<button onClick={handleActivate} disabled={actionLoading} className="neu-btn-accent px-6 py-3 w-full md:w-auto">
|
|
{actionLoading ? <Loader2 size={16} className="animate-spin" /> : 'Activate Free Trial'}
|
|
</button>
|
|
)}
|
|
{plan?.trial_status === 'active' && (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button className="neu-btn px-6 py-3 w-full md:w-auto">Free Trial Active</button>
|
|
{isAdminUser ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate('/admin/payment-config')}
|
|
className="neu-btn px-6 py-3 w-full md:w-auto"
|
|
>
|
|
Manage Payment Methods
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="xl:col-span-4">
|
|
<div className="neu-raised rounded-2xl p-6 xl:sticky xl:top-6">
|
|
<div className="flex items-center justify-between gap-2 mb-2">
|
|
<h3 className="text-base font-black text-text-primary m-0">Buy Credits</h3>
|
|
<button type="button" onClick={() => setShowPurchaseHelp(true)} className="neu-btn w-8 h-8 rounded-full" title="Purchase help">
|
|
<CircleHelp size={13} />
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-text-secondary font-semibold mb-5">Select bundle and pay via UPI.</p>
|
|
<div className={`rounded-xl p-3 mb-4 ${billingConfig?.payments_enabled ? 'bg-accent/5' : 'bg-red-500/10'}`}>
|
|
<div className={`text-xs font-black uppercase tracking-wider ${billingConfig?.payments_enabled ? 'text-accent' : 'text-red-400'}`}>
|
|
{billingConfig?.payments_enabled ? 'Payments Enabled' : 'Payments Disabled'}
|
|
</div>
|
|
<div className="text-[11px] font-semibold text-text-secondary mt-1">
|
|
{billingConfig?.payment_note || 'Pay via UPI and submit UTR for admin approval.'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="neu-inset rounded-xl overflow-hidden">
|
|
<div className="grid grid-cols-[20px_minmax(0,1fr)_70px_70px] gap-2 px-3 py-2 border-b border-white/5 text-[10px] font-black uppercase tracking-wider text-text-muted">
|
|
<div />
|
|
<div>Package</div>
|
|
<div className="text-right">OTPs</div>
|
|
<div className="text-right">Price</div>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
{packages.map((pkg) => {
|
|
const isSelected = selectedPackageCode === pkg.code
|
|
return (
|
|
<button
|
|
type="button"
|
|
key={pkg.code}
|
|
onClick={() => setSelectedPackageCode(pkg.code)}
|
|
className={`grid grid-cols-[20px_minmax(0,1fr)_70px_70px] gap-2 px-3 py-2.5 text-left border-none cursor-pointer border-b last:border-b-0 border-white/5 ${
|
|
isSelected ? 'bg-accent/10' : 'bg-transparent hover:bg-white/5'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-center">
|
|
<span className={`w-3 h-3 rounded-full border ${isSelected ? 'border-accent bg-accent' : 'border-white/40'}`} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-black text-text-primary truncate">{pkg.label}</div>
|
|
<div className="text-[10px] font-bold text-text-muted">
|
|
{pkg.popular ? 'Most Popular' : pkg.save || 'Standard'}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm font-black text-text-primary text-right">{pkg.credits}</div>
|
|
<div className="text-sm font-black text-accent text-right">₹{pkg.amount}</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-xs font-semibold text-text-secondary mt-3">
|
|
Selected: <span className="font-black text-text-primary">{selectedPackage?.label || '-'}</span>
|
|
{selectedPackage ? ` • ${selectedPackage.credits} OTP credits • ₹${selectedPackage.amount}` : ''}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
disabled={!selectedPackage || payingCode === selectedPackage?.code}
|
|
onClick={() => selectedPackage && handlePayViaUpi(selectedPackage)}
|
|
className="neu-btn-accent py-3 px-4 w-full mt-4"
|
|
>
|
|
{selectedPackage && payingCode === selectedPackage.code ? (
|
|
<span className="inline-flex items-center gap-2"><Loader2 size={14} className="animate-spin" /> Processing...</span>
|
|
) : (
|
|
`Purchase Selected${selectedPackage ? ` • ₹${selectedPackage.amount}` : ''}`
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="neu-raised rounded-2xl p-6">
|
|
<div className="flex items-center justify-between gap-2 mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<CreditCard size={16} className="text-accent" />
|
|
<h3 className="text-base font-black text-text-primary m-0">Payment Requests & History</h3>
|
|
</div>
|
|
<button type="button" onClick={() => setShowHistoryHelp(true)} className="neu-btn w-8 h-8 rounded-full" title="History help">
|
|
<CircleHelp size={13} />
|
|
</button>
|
|
</div>
|
|
<div className="max-h-[320px] overflow-auto flex flex-col gap-2">
|
|
{payments.length === 0 ? (
|
|
<div className="text-sm font-semibold text-text-muted py-6 text-center">No payment requests yet.</div>
|
|
) : payments.map((payment) => (
|
|
<div key={payment.request_ref} className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="text-xs font-black text-text-primary">{payment.request_ref}</div>
|
|
<span className={payment.status === 'approved' ? 'badge-success' : payment.status === 'rejected' ? 'badge-error' : 'badge-warning'}>{payment.status}</span>
|
|
</div>
|
|
<div className="text-xs font-semibold text-text-secondary mt-1">
|
|
{payment.package_name} • {payment.credits} credits • ₹{Number(payment.amount_inr).toFixed(2)}
|
|
</div>
|
|
<div className="text-[11px] font-semibold text-text-muted mt-1">Created: {formatDate(payment.created_at)}</div>
|
|
{payment.utr ? <div className="text-[11px] font-semibold text-text-muted mt-1">UTR: {payment.utr}</div> : null}
|
|
{payment.admin_note ? <div className="text-[11px] font-semibold text-text-secondary mt-1">Note: {payment.admin_note}</div> : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{showQuotaHelp ? (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<button type="button" onClick={() => setShowQuotaHelp(false)} className="absolute inset-0 bg-black/70 border-none cursor-pointer" />
|
|
<div className="relative w-full max-w-[720px] rounded-2xl p-6 neu-raised" style={{ background: 'var(--color-base)' }}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-black text-text-primary m-0">Quota Help</h2>
|
|
<button type="button" onClick={() => setShowQuotaHelp(false)} className="neu-btn p-2"><X size={14} /></button>
|
|
</div>
|
|
<div className="grid gap-3 text-sm font-semibold text-text-secondary">
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Sandbox bucket: self number only, separate from live.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Live bucket: any valid number, separate from sandbox.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Purchased packs add credits only to live bucket.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{showTrialApplyModal ? (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => !actionLoading && setShowTrialApplyModal(false)}
|
|
className="absolute inset-0 bg-black/70 border-none cursor-pointer"
|
|
/>
|
|
<div className="relative w-full max-w-[760px] rounded-2xl p-6 neu-raised" style={{ background: 'var(--color-base)' }}>
|
|
<div className="flex items-center justify-between gap-3 mb-4">
|
|
<h2 className="text-lg font-black text-text-primary m-0">Activate Free Trial</h2>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowTrialApplyModal(false)}
|
|
disabled={actionLoading}
|
|
className="neu-btn p-2"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
<div className="grid gap-3 text-sm font-semibold text-text-secondary">
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
|
|
1. Sandbox trial quota: <b>{stats?.trial_total_credits || 500}</b> OTPs/month (self WhatsApp number only).
|
|
</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
|
|
2. Live starter quota: <b>{stats?.live_total_credits || 100}</b> OTPs/month (can send to valid external numbers).
|
|
</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
|
|
3. Sandbox and Live use separate API key sets and separate usage counters.
|
|
</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
|
|
4. Misuse, spam, or policy violations may suspend trial access.
|
|
</div>
|
|
</div>
|
|
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowTrialApplyModal(false)}
|
|
disabled={actionLoading}
|
|
className="neu-btn px-5 py-2.5"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleApply}
|
|
disabled={actionLoading}
|
|
className="neu-btn-accent px-5 py-2.5"
|
|
>
|
|
{actionLoading ? <span className="inline-flex items-center gap-2"><Loader2 size={14} className="animate-spin" /> Activating...</span> : 'Agree & Activate Trial'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{showPurchaseHelp ? (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<button type="button" onClick={() => setShowPurchaseHelp(false)} className="absolute inset-0 bg-black/70 border-none cursor-pointer" />
|
|
<div className="relative w-full max-w-[720px] rounded-2xl p-6 neu-raised" style={{ background: 'var(--color-base)' }}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-black text-text-primary m-0">Purchase Flow</h2>
|
|
<button type="button" onClick={() => setShowPurchaseHelp(false)} className="neu-btn p-2"><X size={14} /></button>
|
|
</div>
|
|
<div className="grid gap-3 text-sm font-semibold text-text-secondary">
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>1. Select package.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>2. Click Purchase Selected.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>3. Complete UPI payment and submit UTR.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>4. Admin approves payment and credits move to live bucket.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{showHistoryHelp ? (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<button type="button" onClick={() => setShowHistoryHelp(false)} className="absolute inset-0 bg-black/70 border-none cursor-pointer" />
|
|
<div className="relative w-full max-w-[720px] rounded-2xl p-6 neu-raised" style={{ background: 'var(--color-base)' }}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-black text-text-primary m-0">Request Status</h2>
|
|
<button type="button" onClick={() => setShowHistoryHelp(false)} className="neu-btn p-2"><X size={14} /></button>
|
|
</div>
|
|
<div className="grid gap-3 text-sm font-semibold text-text-secondary">
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}><b>pending:</b> created but proof not submitted.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}><b>submitted:</b> UTR submitted, waiting admin review.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}><b>approved:</b> credits added to live bucket.</div>
|
|
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}><b>rejected:</b> not approved by admin.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|