// Limites.jsx — Plan gratuito vs. Enfermería Premium. // // DÓNDE ESTÁ LA VERDAD // El plan y el consumo NO se guardan aquí: se leen de Supabase (mi_plan y // mi_uso, en sql/suscripciones.sql). Antes el plan vivía en localStorage y // bastaba una línea en la consola del navegador para volverse premium gratis. // Este archivo solo DIBUJA lo que el servidor responde. // // Eso implica que el conteo exige sesión iniciada: sin cuenta no hay a quién // contarle el uso, y el límite se saltaría abriendo una ventana de incógnito. const { useState: useLiS, useEffect: useLiE, useCallback: useLiC, useRef: useLiR } = React; // Límites del plan GRATUITO. Premium no tiene ninguno. const LIMITES = { planes: 3, // diagnósticos de enfermería que se pueden armar pdfs: 3, // descargas totales… pdfPorPlan: 1, // …y cada plan solo una vez bot: 3, // consultas a Cynamoncita IA revisionesLibro: 3, // consultas de detalle en CADA libro (dx · obj · int) // Las calculadoras son ILIMITADAS también en el plan gratuito. }; const PRECIO = { importe: 89, moneda: 'MXN', periodo: 'mes' }; // ── Estado compartido ──────────────────────────────────────── const LimitesStore = { state: { cargando: true, premium: false, venceEn: null, uso: null, sesion: false }, listeners: new Set(), set(patch) { this.state = { ...this.state, ...patch }; this.listeners.forEach(fn => fn(this.state)); }, async refrescar() { const sb = window.cynaAuth && window.cynaAuth.client; const hayUsuario = !!window.__cynaUser; if (!sb || !hayUsuario) { this.set({ cargando: false, premium: false, uso: null, sesion: false }); return; } try { const [plan, uso] = await Promise.all([sb.rpc('mi_plan'), sb.rpc('mi_uso')]); const p = plan && plan.data && plan.data[0]; const u = uso && uso.data && uso.data[0]; const estado = (p && p.estado) || 'free'; this.set({ cargando: false, sesion: true, estado, // 'free' | 'prueba' | 'premium' // Durante la prueba gratuita NO se aplica ningún límite: es justo el // sentido de la prueba, que se conozca la herramienta completa. premium: estado === 'premium' || estado === 'prueba', enPrueba: estado === 'prueba', diasRestantes: p ? p.dias_restantes : null, venceEn: p ? p.vence_en : null, uso: u || null, }); } catch (e) { // Ante la duda NO regalamos premium, pero tampoco bloqueamos la app: // se trata como plan gratuito con el consumo desconocido. console.error('[Cynamonurs] No se pudo leer el plan:', e); this.set({ cargando: false, sesion: true, premium: false, uso: null }); } }, }; function useLimites() { const [s, setS] = useLiS(LimitesStore.state); useLiE(() => { const fn = (st) => setS({ ...st }); LimitesStore.listeners.add(fn); if (LimitesStore.state.cargando) LimitesStore.refrescar(); return () => LimitesStore.listeners.delete(fn); }, []); return s; } // Se recalcula al iniciar/cerrar sesión y cuando se registra una acción. if (typeof window !== 'undefined') { window.addEventListener('cyna-session', () => LimitesStore.refrescar()); window.addEventListener('cyna-uso', () => LimitesStore.refrescar()); // El pago con Stripe ahora se abre en OTRA pestaña (src/stripe.js), así que // esta pestaña se queda con la sesión abierta mientras la persona paga. Al // volver aquí —cambiar de pestaña, o de app en el móvil y regresar— se // vuelve a consultar el plan solo, sin que nadie tenga que recargar a // ciegas para saber si ya se activó. document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') LimitesStore.refrescar(); }); window.addEventListener('focus', () => LimitesStore.refrescar()); } // ── Consulta de disponibilidad ─────────────────────────────── // Devuelve { ok, restantes, motivo }. `ok:false` → hay que ofrecer la suscripción. function revisarLimite(clave) { const s = LimitesStore.state; if (s.premium) return { ok: true, restantes: Infinity }; if (!s.sesion) return { ok: false, restantes: 0, motivo: 'sesion' }; if (!s.uso) return { ok: true, restantes: null }; // aún sin datos: no bloqueamos const u = s.uso; const mapa = { planes: [Number(u.planes_creados || 0), LIMITES.planes], pdfs: [Number(u.pdfs_descargados || 0), LIMITES.pdfs], bot: [Number(u.consultas_bot || 0), LIMITES.bot], rev_dx: [Number(u.revisiones_dx || 0), LIMITES.revisionesLibro], rev_obj: [Number(u.revisiones_obj || 0), LIMITES.revisionesLibro], rev_int: [Number(u.revisiones_int || 0), LIMITES.revisionesLibro], }; const par = mapa[clave]; if (!par) return { ok: true, restantes: null }; const [usado, tope] = par; return { ok: usado < tope, restantes: Math.max(0, tope - usado), motivo: 'limite' }; } // ¿Ya se descargó el PDF de este plan concreto? async function yaDescargado(firma) { if (LimitesStore.state.premium) return false; const sb = window.cynaAuth && window.cynaAuth.client; if (!sb || !window.__cynaUser) return false; try { const { data } = await sb.rpc('ya_descargado', { p_plan: firma }); return data === true; } catch (e) { return false; } } // ── Aviso de límite (la «leyenda de información») ───────────── function LimiteAviso({ clave, etiqueta, compacto }) { const s = useLimites(); if (s.cargando) return null; // Prueba gratuita: sin límites, pero conviene decir que termina — y cuándo. // Presentarlo como «Premium» a secas haría creer que ya se pagó, y el corte // llegaría por sorpresa. if (s.enPrueba) { const d = s.diasRestantes; return (
Prueba gratuita · acceso completo sin límites {d != null && <> · te {d === 1 ? 'queda' : 'quedan'} {d} día{d === 1 ? '' : 's'}}
); } if (s.premium) { return (
Premium · {etiqueta || 'uso ilimitado'}
); } const r = revisarLimite(clave); if (!s.sesion) { return (
Inicia sesión para usar esta función. El plan gratuito incluye {LIMITES[clave === 'bot' ? 'bot' : 'planes']} usos.
); } const agotado = !r.ok; return (
{agotado ? <>Has usado tus {etiqueta || 'usos'} gratuitos. : <>Te {r.restantes === 1 ? 'queda' : 'quedan'} {r.restantes} de {etiqueta || 'uso gratuito'}.} {' '}Con Enfermería Premium es ilimitado.
); } // ── Ventana de suscripción ─────────────────────────────────── function PremiumModal({ onClose }) { const s = useLimites(); const ir = () => { if (window.cynaPay && window.cynaPay.checkout) window.cynaPay.checkout('premium'); }; return (
e.stopPropagation()}>

Enfermería Premium

${PRECIO.importe} {PRECIO.moneda} / {PRECIO.periodo}
  • Diagnósticos ilimitados — sin tope de {LIMITES.planes}
  • Descargas en PDF ilimitadas — y las veces que quieras por plan
  • Cynamoncita IA sin límite — no solo {LIMITES.bot} consultas
  • Los tres libros completos — sin tope de {LIMITES.revisionesLibro} consultas por libro
  • Sin publicidad
  • Calculadoras y escalas — ya son ilimitadas en el plan gratuito

Se renueva automáticamente cada mes hasta que decidas cancelarla. Puedes cancelar cuando quieras para evitar cobros futuros; la cancelación evita la siguiente renovación pero no implica el reembolso del periodo ya pagado. Consulta los antes de contratar.

{s.enPrueba && (
Estás en tu prueba gratuita {s.diasRestantes != null && <> · quedan {s.diasRestantes} día{s.diasRestantes === 1 ? '' : 's'}}. Si te suscribes ahora no pierdes nada: el acceso continúa sin cortes.
)} {s.estado === 'premium' && (
Ya tienes la suscripción activa. ¡Gracias!
)}
{/* Se compara con el ESTADO, no con `premium`: durante la prueba `premium` es true (no hay límites), pero ahí es justo cuando más sentido tiene ofrecer la suscripción. */} {s.estado !== 'premium' && }
); } // Detecta el momento EXACTO en que la suscripción pasa de no-premium a // premium (p. ej. al volver de pagar en la otra pestaña) para mostrar una // confirmación clara. Solo se dispara ante una transición real observada en // esta sesión — no al cargar la página ya siendo premium — para no mostrar el // aviso de "recién activada" a alguien que lo es desde hace tiempo. function useSuscripcionActivada() { const s = useLimites(); const anterior = useLiR(null); const [activada, setActivada] = useLiS(false); useLiE(() => { if (s.cargando) return; if (anterior.current === false && s.premium === true) setActivada(true); anterior.current = s.premium; }, [s.cargando, s.premium]); return [activada, () => setActivada(false)]; } function PremiumHost() { const [abierto, setAbierto] = useLiS(false); const [activada, cerrarActivada] = useSuscripcionActivada(); useLiE(() => { window.__cynaAbrirPremium = () => setAbierto(true); return () => { delete window.__cynaAbrirPremium; }; }, []); return ( {activada && (
¡Tu suscripción Enfermería Premium ya está activa! Si alguna pantalla no lo refleja todavía, fuerza la recarga de la página.
)} {abierto && setAbierto(false)}/>}
); } Object.assign(window, { LIMITES, PRECIO, LimitesStore, useLimites, revisarLimite, yaDescargado, LimiteAviso, PremiumHost, });