// FLUJO DE PAGO — Checkout, Mercado Pago (real), Oxxo Pay (real), Pendiente, Confirmado window.PaymentFlow = {}; // ── Utilidad compartida ─────────────────────────────────────── function authHeaders() { const tok = localStorage.getItem('sd_token'); return tok ? { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' }; } // ══════════════════════════════════════════════════════════════ // CHECKOUT — elige método y llama la API // ══════════════════════════════════════════════════════════════ window.PaymentFlow.Checkout = function Checkout({ navigate, store }) { if (!store.session) { navigate('/login'); return null; } const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS; const items = store.cart.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean); const subtotal = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0); const descuento = items.length >= 3 ? Math.round(subtotal * 0.15) : 0; const total = subtotal - descuento; const [metodo, setMetodo] = React.useState('mercadopago'); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); if (items.length === 0) { return (
🛒

Tu carrito está vacío

Agrega materias desde tu panel.

); } const procesar = async () => { setLoading(true); setError(''); const ids = store.cart; try { if (metodo === 'mercadopago') { const r = await fetch(`${window.API_BASE}/checkout/mercadopago`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ prueba_ids: ids }), }); const d = await r.json(); if (!r.ok) { setError(d.error || 'Error al procesar'); setLoading(false); return; } if (d.sandbox) { // API no configurada con credenciales reales → flujo demo navigate('/pago/mercadopago'); } else { // Redirigir al checkout real de Mercado Pago // En sandbox usa sandbox_init_point, en producción usa init_point const url = d.sandbox_init_point || d.init_point; window.location.href = url; } } else { // OXXO: crear pago y mostrar ficha const r = await fetch(`${window.API_BASE}/checkout/oxxo`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ prueba_ids: ids }), }); const d = await r.json(); if (!r.ok) { setError(d.error || 'Error al procesar'); setLoading(false); return; } // Guardar datos del pago en sessionStorage para que la pantalla OXXO los muestre sessionStorage.setItem('sd_oxxo_pago', JSON.stringify(d)); navigate('/pago/oxxo'); } } catch { // Sin API (local sin servidor) → demo if (metodo === 'mercadopago') navigate('/pago/mercadopago'); else { sessionStorage.removeItem('sd_oxxo_pago'); navigate('/pago/oxxo'); } } setLoading(false); }; return (
Checkout

Confirma tu compra

Materias seleccionadas ({items.length})

{items.map(p => (
{p.icono || '📘'}
{p.nombre}
Simulador completo · acceso permanente
{window.formatPrice(p.precio || p.precio_mxn)}
))}

Método de pago

Resumen

Subtotal ({items.length} {items.length === 1 ? 'materia' : 'materias'}){window.formatPrice(subtotal)}
{descuento > 0 &&
Descuento 3+ materias (15%)-{window.formatPrice(descuento)}
}
Total{window.formatPrice(total)}
{error &&
{error}
}

Pago único. Acceso permanente a las materias compradas.

); }; // ══════════════════════════════════════════════════════════════ // MERCADO PAGO — pantalla de transición / demo fallback // En producción el usuario ya fue redirigido a MP directamente. // Esta pantalla se muestra solo si MP no está configurado (sandbox=true). // ══════════════════════════════════════════════════════════════ window.PaymentFlow.MercadoPago = function MercadoPago({ navigate, store }) { const [paying, setPaying] = React.useState(false); const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS; const items = store.cart.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean); const sub = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0); const total = sub - (items.length >= 3 ? Math.round(sub * 0.15) : 0); const confirm = () => { setPaying(true); setTimeout(() => { store.grantPremium(store.cart); store.clearCart(); navigate('/pago/confirmado'); }, 1600); }; return (
Checkout · Mercado Pago (demo)
Total a pagar
{window.formatPrice(total)}

Modo demo · configura MP_ACCESS_TOKEN en api/config.php para pagos reales.

); }; // ══════════════════════════════════════════════════════════════ // OXXO PAY — muestra la referencia real generada por MP // ══════════════════════════════════════════════════════════════ window.PaymentFlow.Oxxo = function OxxoPay({ navigate, store }) { // Leer datos del pago que guardó Checkout en sessionStorage const pagoData = React.useMemo(() => { try { return JSON.parse(sessionStorage.getItem('sd_oxxo_pago') || '{}'); } catch { return {}; } }, []); const PRUEBAS = store.pruebas || window.SD_SEED.PRUEBAS; const items = store.cart.map(id => PRUEBAS.find(p => p.id === id)).filter(Boolean); const sub = items.reduce((s, p) => s + (p.precio || p.precio_mxn || 0), 0); const total = pagoData.total || (sub - (items.length >= 3 ? Math.round(sub * 0.15) : 0)); // Referencia real de MP o demo const ref = pagoData.referencia || React.useMemo(() => '93000' + Math.floor(Math.random() * 1e10).toString().padStart(10, '0'), []); const expira = pagoData.expira_en ? new Date(pagoData.expira_en).toLocaleDateString('es-MX', { day: 'numeric', month: 'long', year: 'numeric' }) : null; const simularPago = () => { store.grantPremium(store.cart); store.clearCart(); sessionStorage.removeItem('sd_oxxo_pago'); navigate('/pago/confirmado'); }; return (
OXXO PAY Ficha de pago
Monto a pagar {window.formatPrice(total)}
Referencia · presenta este número en caja {/* Código de barras visual (decorativo) */}
{Array.from({ length: 60 }).map((_, i) => ( ))}
{ref}
{expira &&
Vigencia: {expira}
}
  1. Acude a cualquier tienda OXXO.
  2. Indica en caja que harás un pago de OXXO Pay.
  3. Dicta o muestra la referencia de esta ficha.
  4. Realiza el pago en efectivo.
  5. Tu acceso premium se activa automáticamente al acreditarse el pago (1-24h).
{pagoData.barcode_url ? Ver ficha completa ↗ : }
{pagoData.sandbox && ( )}
); }; // ══════════════════════════════════════════════════════════════ // PENDIENTE — OXXO no acreditado todavía // ══════════════════════════════════════════════════════════════ window.PaymentFlow.Pendiente = function Pendiente({ navigate, store }) { return (

Pago pendiente de acreditación

Estamos esperando que OXXO confirme tu pago. Suele tardar entre 1 y 24 horas. En cuanto se acredite recibirás un correo y tu acceso premium se activará solo — no necesitas hacer nada más.

Mientras tanto puedes:

  • Practicar con los simuladores muestra (gratis)
  • Revisar el contenido del examen USICAMM
  • Cerrar esta página — recibirás un correo al acreditarse
); }; // ══════════════════════════════════════════════════════════════ // CONFIRMADO — pago exitoso (MP redirige aquí con ?payment_id=) // ══════════════════════════════════════════════════════════════ window.PaymentFlow.Confirmado = function Confirmado({ navigate, store }) { // MP agrega ?payment_id=xxx&status=approved al volver al sitio const hashQuery = new URLSearchParams(window.location.hash.split('?')[1] || ''); const mpStatus = hashQuery.get('status'); const mpPayId = hashQuery.get('payment_id'); // Refrescar premium desde la API una vez llegamos aquí. // El webhook de MP ya pudo haberlo acreditado; este fetch lo confirma en el cliente. React.useEffect(() => { const tok = localStorage.getItem('sd_token'); if (!tok) return; fetch(`${window.API_BASE}/me/accesos`, { headers: { Authorization: `Bearer ${tok}` } }) .then(r => r.ok ? r.json() : null) .then(d => { if (d?.accesos?.length) store.grantPremium(d.accesos); }) .catch(() => {}); store.clearCart(); sessionStorage.removeItem('sd_oxxo_pago'); }, []); return (

¡Pago confirmado!

Tu acceso premium ya está activo. Practica las veces que necesites.

{mpPayId && (
Referencia MP: {mpPayId}
)}
); };