// rsvp.jsx — Confirmación personalizada + seguridad del enlace. // Exporta: RSVP, AccessGate, useSecurity. const { T: T3 } = window; // —————————————————————————— SEGURIDAD DEL ENLACE —————————————————————————— // Estrategia (la más eficiente sin servidor propio): // 1) Cada enlace lleva ?id=INVxxx&k=TOKEN. Sin el token correcto, no abre. // 2) En la 1ª apertura se "vincula" el enlace a este dispositivo (localStorage). // 3) Se registra cada apertura en la hoja (id + huella de dispositivo + hora). // Si el mismo id se abre desde OTRO dispositivo → el Apps Script marca // "POSIBLE REENVÍO" y te envía un correo de alerta. Ver la guía incluida. function getDeviceId() { let d = localStorage.getItem('dd_device'); if (!d) { d = 'dev_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36); localStorage.setItem('dd_device', d); } return d; } function post(payload) { const url = window.CONFIG.WEBHOOK_URL; const body = JSON.stringify({ ...payload, secret: window.CONFIG.formSecret }); if (!url) { console.log('[RSVP modo demo] payload →', JSON.parse(body)); return Promise.resolve({ demo: true }); } return fetch(url, { method: 'POST', mode: 'no-cors', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body }) .then(() => ({ ok: true })); } function useSecurity(invite) { React.useEffect(() => { if (!invite || invite.status !== 'ok') return; const id = invite.id, dev = getDeviceId(); const claimKey = 'dd_claim_' + id; const firstDevice = localStorage.getItem(claimKey); const isNewDeviceForThisLink = firstDevice && firstDevice !== dev; // raro en el mismo equipo if (!firstDevice) localStorage.setItem(claimKey, dev); // registra la apertura (el servidor decide si es reenvío comparando dispositivos) post({ type: 'apertura', id, nombre: invite.nombre, deviceId: dev, nuevoDispositivoLocal: !!isNewDeviceForThisLink, ua: navigator.userAgent, referrer: document.referrer || '', ts: new Date().toISOString(), }).catch(() => {}); }, [invite && invite.id, invite && invite.status]); } // —————————————————————————— PUERTA DE ACCESO —————————————————————————— function AccessGate({ invite, children }) { if (invite.status === 'ok' || invite.status === 'none') return children; // denied / invalid → pantalla de bloqueo const denied = invite.status === 'denied'; return (
Cada invitación de Diana & Daniel es única y personal. Por favor abre el enlace completo que recibiste, o escríbenos para reenviártelo.
{yes ? `Gracias, ${(prev?.nombre || confirmName).split(' ')[0]}. Guardamos tu lugar con mucho cariño y contamos los días para celebrar contigo.` : 'Lamentamos que no puedas acompañarnos. Gracias por tomarte un momento para avisarnos; estarás en nuestros pensamientos ese día.'}
{isOk ? (acomp ? <>Reservamos un lugar para ti y {acomp}. Confírmanos antes del 15 de septiembre.> : maxPases === 1 ? <>Reservamos un lugar a tu nombre. Confírmanos antes del 15 de septiembre.> : <>Tenemos {maxPases} lugares reservados a tu nombre. Confírmanos antes del 15 de septiembre.>) : 'Vista previa del formulario. Comparte un enlace personal (con ?id=…) para activar el saludo y los pases de cada invitado.'}