// 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 (
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 && (