'use client'; import { useEffect, useMemo, useState } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; import type { DefaultLimits, Purchase } from '@/lib/types'; import Link from 'next/link'; import { CreditCard, Flame, SlidersHorizontal, Wallet } from 'lucide-react'; import { getPurchasesByUser } from '@/lib/firestore'; import toast from 'react-hot-toast'; export default function CreditsPage() { const { user, isLoading } = useAuth(); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const [balance, setBalance] = useState(0); const [limits, setLimits] = useState(null); const [isSaving, setIsSaving] = useState(false); const [purchaseHistory, setPurchaseHistory] = useState([]); const [checkoutError, setCheckoutError] = useState(null); const [isCreatingCheckout, setIsCreatingCheckout] = useState(null); const getFallbackLimits = (userId: string): DefaultLimits => ({ userId, defaultMaxMinutes: 15, defaultMaxCredits: 15, allowAutoExtension: false, maxExtraMinutes: 0, defaultMaxRecalls: 3, minDelayBetweenRecalls: 30, updatedAt: null as unknown as DefaultLimits['updatedAt'], }); useEffect(() => { if (!isLoading && !user) { router.push('/auth'); } }, [user, isLoading, router]); const fetchCredits = async () => { try { const response = await fetch('/api/credits'); if (!response.ok) { toast.error('Unable to load credits right now.'); setBalance(0); return; } const data = await response.json(); setBalance(data.balance ?? 0); } catch (error) { toast.error('Unable to load credits. Please try again.'); setBalance(0); } }; const fetchLimits = async () => { try { const response = await fetch('/api/credits/limits'); if (!response.ok) { toast.error('Unable to load limits right now.'); setLimits(getFallbackLimits(user?.uid ?? 'test-user-id')); return; } const data = await response.json(); setLimits(data.limits ?? getFallbackLimits(user?.uid ?? 'test-user-id')); } catch (error) { toast.error('Unable to load limits. Please try again.'); setLimits(getFallbackLimits(user?.uid ?? 'test-user-id')); } }; useEffect(() => { if (user) { fetchCredits(); fetchLimits(); getPurchasesByUser(user.uid) .then(setPurchaseHistory) .catch((error) => { console.error('Error loading purchases', error); toast.error('Unable to load purchases right now.'); }); } }, [user]); const estimatedValue = useMemo(() => { return balance.toFixed(2); }, [balance]); const updateLimits = (patch: Partial) => { setLimits((prev) => (prev ? { ...prev, ...patch } : prev)); }; const handleSaveLimits = async () => { if (!limits || isSaving) return; setIsSaving(true); try { const payload = { defaultMaxMinutes: Number(limits.defaultMaxMinutes), defaultMaxCredits: Number(limits.defaultMaxCredits), allowAutoExtension: limits.allowAutoExtension, maxExtraMinutes: Number(limits.maxExtraMinutes), defaultMaxRecalls: Number(limits.defaultMaxRecalls), minDelayBetweenRecalls: Number(limits.minDelayBetweenRecalls), }; const response = await fetch('/api/credits/limits', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!response.ok) throw new Error('Failed'); await fetchLimits(); toast.success('Limits updated.'); } catch (error) { console.error('Error saving limits', error); toast.error('Failed to save limits.'); } finally { setIsSaving(false); } }; const handleAddCredits = async () => { const raw = window.prompt('Enter credits to add', '10'); if (!raw) return; const amount = Number(raw); if (!Number.isFinite(amount) || amount <= 0) return; try { const response = await fetch('/api/credits', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount }), }); if (!response.ok) throw new Error('Failed'); const data = await response.json(); setBalance(data.balance ?? balance); toast.success('Credits added successfully.'); } catch (error) { console.error('Error adding credits', error); toast.error('Unable to add credits right now.'); } }; const creditPackages = [ { id: 'credits-10', label: '10 Credits', price: '€10', value: 10, tag: '€1/credit' }, { id: 'credits-50', label: '50 Credits', price: '€45', value: 50, tag: 'Best Value' }, { id: 'credits-100', label: '100 Credits', price: '€85', value: 100, tag: 'Enterprise' }, ]; const handleCheckout = async (packageId: string) => { setCheckoutError(null); setIsCreatingCheckout(packageId); const selected = creditPackages.find((pack) => pack.id === packageId); if (!selected) { setCheckoutError('Invalid credit package.'); setIsCreatingCheckout(null); return; } try { const response = await fetch('/api/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credits: selected.value, amount: Number(selected.price.replace('€', '')), userId: user?.uid, }), }); if (!response.ok) { throw new Error('Unable to create a checkout session.'); } const data = await response.json(); if (data.url) { toast.success('Redirecting to checkout...'); window.location.href = data.url; } else { throw new Error('Missing Stripe checkout URL.'); } } catch (error) { console.error('Checkout error', error); setCheckoutError('Unable to start checkout. Please try again.'); toast.error('Checkout failed. Please try again.'); } finally { setIsCreatingCheckout(null); } }; if (isLoading || !limits) { return (
Loading...
); } const navLinks = [ { label: 'Dashboard', href: '/dashboard' }, { label: 'Scheduled Calls', href: '/dashboard/scheduled-calls' }, { label: 'Voice Agent', href: '/dashboard/agent-settings' }, { label: 'Notifications', href: '/dashboard/notifications' }, { label: 'Settings', href: '/dashboard/settings' }, ]; return (

Credits & Limits

{navLinks.map((link) => { const isActive = pathname === link.href; return ( {link.label} ); })}

Current Balance

{balance} credits

≈ {balance} minutes of calls

Estimated value: €{estimatedValue}

{(searchParams.get('success') || searchParams.get('canceled')) && (
{searchParams.get('success') ? 'Payment successful! Credits will appear shortly.' : 'Payment canceled. You can try again anytime.'}
)}
{creditPackages.map((pack) => (

Package

{pack.label}

{pack.price}

{pack.tag}

))}
{checkoutError && (
{checkoutError}
)}

Monthly Usage

Last 30 days

+12%
{[30, 40, 20, 55].map((value, index) => (
))}

Default limits per task

updateLimits({ defaultMaxMinutes: Number(event.target.value) }) } className="mt-2 w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" />
updateLimits({ defaultMaxCredits: Number(event.target.value) }) } className="mt-2 w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" />
updateLimits({ defaultMaxRecalls: Number(event.target.value) }) } className="mt-2 w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" />
updateLimits({ minDelayBetweenRecalls: Number(event.target.value) }) } className="mt-2 w-full rounded-xl bg-[#0f0b1a] border border-purple-500/20 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-[#8b5cf6]" />

Credits are consumed per task. One credit is approximately equal to 1 minute of processing time or 1 voice call minute. Rates vary by destination.

Purchase History

{purchaseHistory.length === 0 ? (

No purchases yet.

) : (
{purchaseHistory.map((purchase) => ( ))}
Date Credits Amount Session
{purchase.createdAt?.toDate ? purchase.createdAt.toDate().toLocaleDateString() : '—'} {purchase.credits} €{purchase.amount} {purchase.stripeSessionId.slice(0, 8)}...
)}
); }