/* Tab Billing — Plan, créditos y gestión de suscripción */ const PLAN_COLORS = { starter: { accent: "var(--accent)", bg: "var(--accent-softer)", border: "var(--accent-soft)" }, creator: { accent: "var(--accent)", bg: "var(--accent-softer)", border: "var(--accent-soft)" }, pro: { accent: "var(--accent)", bg: "var(--accent-softer)", border: "var(--accent-soft)" }, }; const PLAN_ICONS_BILLING = { starter: "△", creator: "◆", pro: "★" }; const PLAN_LABELS = { starter: "Starter", creator: "Creator", pro: "Pro", admin: "Admin", none: "Sin plan" }; function CreditBar({ used, grant, color }) { const pct = grant > 0 ? Math.min(100, Math.round((used / grant) * 100)) : 0; const isUnlimited = grant >= 9999999; return (
Uso mensual {isUnlimited ? "Ilimitado" : (used || 0).toLocaleString() + " / " + (grant || 0).toLocaleString()}
{!isUnlimited && (
= 90 ? "#f87171" : (color || "var(--accent)"), transition: "width 0.4s ease", }}/>
)}
); } function PlanCard({ plan, isCurrent, onUpgrade }) { const colors = PLAN_COLORS[plan.id] || PLAN_COLORS.starter; const icon = PLAN_ICONS_BILLING[plan.id] || ""; return (
{isCurrent && (
ACTUAL
)}
{icon && {icon}} {plan.name} ${plan.price} /mes
{(plan.features || []).map(function(f, i) { return (
✓ {f}
); })}
{isCurrent ? (
Plan actual
) : ( )}
); } function CreditPackageCard({ pkg, onBuy }) { return (
{(pkg.credits || 0).toLocaleString()}
créditos
${pkg.price_usd}
); } function MyCostsSection() { const now = new Date(); const [year, setYear] = React.useState(now.getFullYear()); const [selMonth, setSelMonth] = React.useState( now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0") ); const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); React.useEffect(() => { let alive = true; setLoading(true); fetch(`/api/me/costs/breakdown?year=${year}&month=${selMonth}`, { credentials: "same-origin" }) .then((r) => r.json()) .then((d) => { if (alive) setData(d); }) .catch(() => {}) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; }; }, [year, selMonth]); const fmt = window.costFmtUSD || ((n) => "$" + (Number(n) || 0).toFixed(2)); const mLabel = window.costMonthLabel || ((ym) => ym); const AnnualChart = window.AnnualCostChart; const MonthPick = window.MonthPicker; const Calendar = window.SpendCalendar; const Legend = window.ProviderLegend; const PC = window.COST_PROVIDER_COLORS || {}; const PL = window.COST_PROVIDER_LABELS || {}; const hasAny = data && (data.by_month || []).some((m) => m.total > 0); return (

Consumo de APIs

Estimación en USD · solo tu cuenta
{loading && !data ? (
Cargando…
) : !hasAny ? (
Aún no hay gasto registrado en tu cuenta.
) : (
{/* Total del mes */}
{fmt(data.month_total)} en {mLabel(selMonth)}
{/* Gráfica anual */}
Resumen anual {year}
{AnnualChart && ( { setYear(Number(ym.split("-")[0])); setSelMonth(ym); }}/> )} {Legend &&
}
{/* Selector de mes + calendario */}
Seleccionar mes
{MonthPick && }
Calendario · {mLabel(selMonth)}
{Calendar && }
{/* Por API */}
Por API · {mLabel(selMonth)}
{(data.month_by_provider || []).length === 0 ? (
Sin gasto este mes.
) : data.month_by_provider.map((p) => (
{PL[p.provider] || p.provider} {fmt(p.total)}
))}
)}
); } function BillingTab({ currentUser, onUserRefresh }) { var [plans, setPlans] = React.useState([]); var [packages, setPackages] = React.useState([]); var [loading, setLoading] = React.useState(false); var [portalLoading, setPortalLoading] = React.useState(false); var [msg, setMsg] = React.useState(null); React.useEffect(function() { fetch("/api/billing/plans", { credentials: "same-origin" }) .then(function(r) { return r.json(); }) .then(function(d) { setPlans(d.plans || []); }) .catch(function() {}); fetch("/api/billing/credit-packages", { credentials: "same-origin" }) .then(function(r) { return r.json(); }) .then(function(d) { setPackages(d.packages || []); }) .catch(function() {}); }, []); // Check for Stripe redirect result React.useEffect(function() { var params = new URLSearchParams(window.location.search); var sess = params.get("session"); if (sess === "success") { setMsg({ type: "success", text: "¡Suscripción activada correctamente! Puede tardar unos segundos en actualizarse." }); if (onUserRefresh) onUserRefresh(); window.history.replaceState({}, "", window.location.pathname); } else if (sess === "cancel") { setMsg({ type: "info", text: "Checkout cancelado." }); window.history.replaceState({}, "", window.location.pathname); } }, []); var user = currentUser || {}; var plan = user.plan || null; var status = user.subscription_status || "inactive"; var isAdmin = user.is_admin; var creditsAvailable = user.credits_available != null ? user.credits_available : 0; var creditsMonthlyGrant = user.credits_monthly_grant != null ? user.credits_monthly_grant : 0; var creditsUsedMonth = user.credits_used_month != null ? user.credits_used_month : 0; var creditsPurchased = user.credits_purchased != null ? user.credits_purchased : 0; var channelCount = user.channel_count != null ? user.channel_count : 0; var channelLimit = user.channel_limit != null ? user.channel_limit : 0; var voiceCloneSlots = user.voice_clone_slots != null ? user.voice_clone_slots : 0; var accentColor = "var(--accent)"; async function handleUpgrade(planId) { setLoading(true); setMsg(null); try { var r = await fetch("/api/billing/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "same-origin", body: JSON.stringify({ plan_id: planId }), }); var d = await r.json(); if (d.url) { window.location.href = d.url; } else { setMsg({ type: "error", text: d.detail || "Error al crear sesión de pago" }); } } catch (e) { setMsg({ type: "error", text: "Error de conexión" }); } finally { setLoading(false); } } async function handlePortal() { setPortalLoading(true); setMsg(null); try { var r = await fetch("/api/billing/portal", { method: "POST", credentials: "same-origin", }); var d = await r.json(); if (d.url) { window.location.href = d.url; } else { setMsg({ type: "error", text: d.detail || "Error al abrir portal" }); } } catch (e) { setMsg({ type: "error", text: "Error de conexión" }); } finally { setPortalLoading(false); } } function handleBuyPackage(pkg) { setMsg({ type: "info", text: "Próximamente — configura Stripe para habilitar la compra de créditos." }); } return (
{msg && (
{msg.text}
)} {/* ─── Current plan & credit balance ─── */}
{PLAN_LABELS[plan] || "Sin plan"} {isAdmin ? "Admin" : status === "active" ? "Activo" : status === "trialing" ? "Trial" : "Inactivo"}

{isAdmin ? "Acceso completo sin límites" : plan ? "Suscripción " + PLAN_LABELS[plan] + " · $" + (plan === "starter" ? "99" : plan === "creator" ? "199" : "299") + "/mes" : "No tienes una suscripción activa"}

{!isAdmin && plan && status === "active" && ( )}
{/* Credit balance */} {!plan && !isAdmin ? (
Sin suscripción activa
Elige un plan para empezar a generar vídeos.
) : (
{/* Big credit number */}
{isAdmin ? "∞" : creditsAvailable.toLocaleString()} créditos disponibles
{!isAdmin && ( )} {/* Sub-stats */}
{!isAdmin && (
Concedidos/mes
{creditsMonthlyGrant.toLocaleString()}
)} {!isAdmin && creditsPurchased > 0 && (
Comprados
+{creditsPurchased.toLocaleString()}
)}
Canales
{isAdmin ? "∞" : channelCount + " / " + channelLimit}
Voice Slots
{isAdmin ? "∞" : voiceCloneSlots}
)}
{/* ─── Consumo de APIs (solo el gasto propio del usuario) ─── */} {/* ─── Plan cards — hide for admin ─── */} {!isAdmin && (

{plan ? "Cambiar de plan" : "Elige tu plan"}

{plans.map(function(p) { return ( ); })}
{loading && (
Redirigiendo a Stripe…
)}
)} {/* ─── Buy extra credits — hide for admin ─── */} {!isAdmin && (

Comprar créditos adicionales

Los créditos comprados no caducan y se suman a tu saldo mensual.

{packages.map(function(pkg) { return ( ); })}
)}
); } Object.assign(window, { BillingTab });