/* 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 });