/* global React */ // Shared primitives — buttons w/ unified hover (mint) + press (forest) states, // and a slide-up bottom-sheet modal. const { useState: useStateS, useEffect: useEffectS } = React; // ──────────────────────────────────────────────── // Color tokens for the unified button states // default → ink (#333) // hover → mint-700 // pressed → forest-700 (slightly deeper, shows the press) // ──────────────────────────────────────────────── const BTN_DEFAULT = "var(--yo-ink)"; const BTN_HOVER = "var(--yo-mint-700)"; const BTN_PRESS = "var(--yo-forest-700)"; window.YO_BTN = { BTN_DEFAULT, BTN_HOVER, BTN_PRESS }; // ──────────────────────────────────────────────── // PrimaryButton — Yomee Design System / CTA // // Spec (DO NOT diverge): // • Height: 56px · Border radius: 16px (rounded-square — NOT pill) // • Font: var(--font-mono), 700, 13px, letter-spacing 0.14em, UPPERCASE (CSS-enforced) // • Press effect: scale(0.985) over 100ms // • Width: 100% of parent (callers control max-width via wrapper) // // Variants: // variant="primary" (default) // bg: var(--yo-ink) → mint-700 hover → forest-700 press // fg: #fff // variant="accent" (requires accentColor prop) // bg: accentColor → brightness 0.93 hover → 0.88 press // fg: #fff // Use case: profile-colored CTAs (e.g. UploadScreen per-profile primary) // variant="outlined" // bg: transparent → ink/4% hover → ink/10% press // fg: var(--yo-ink) · border: 1.5px solid var(--yo-ink) // Use case: secondary action paired with a primary CTA in the same stack // // Props: // children — label (sentence case; CSS uppercases it) // onClick — handler // leftIcon — optional ReactNode rendered absolute-left // trailingArrow — when true, renders a → after the label // variant — "primary" | "accent" | "outlined" // accentColor — hex/CSS color (required when variant === "accent") // style — escape hatch for spacing only (do NOT override colors/font) // disabled — dims and disables // // Usage: // Continuar // // Crear mi radiografía // // Ver demo // ──────────────────────────────────────────────── function PrimaryButton({ children, onClick, leftIcon, trailingArrow = false, variant = "primary", // "primary" | "accent" | "outlined" accentColor, // required only when variant === "accent" (e.g. "#2BAE80") style, disabled, }) { const [hover, setHover] = useStateS(false); const [pressed, setPressed] = useStateS(false); // Variant-aware visual tokens. PrimaryButton is the only CTA component in the // system; it adapts to three visual treatments without breaking the spec. let bg, fg, borderStyle; if (variant === "outlined") { // Transparent fill, ink border, ink text. Hover fills with light ink overlay, // press deepens it. No accent color allowed — outlined is always ink. fg = "var(--yo-ink)"; borderStyle = "1.5px solid var(--yo-ink)"; bg = disabled ? "transparent" : pressed ? "rgba(20,32,28,0.10)" : hover ? "rgba(20,32,28,0.04)" : "transparent"; } else if (variant === "accent") { // Accent uses caller-provided color as default. Hover/press are simulated // via brightness filter to avoid defining 4×3 tokens (one per profile). if (!accentColor) { console.warn("PrimaryButton variant='accent' requires accentColor prop"); } fg = "#fff"; borderStyle = "none"; bg = disabled ? "rgba(20,32,28,0.22)" : (accentColor || "var(--yo-ink)"); } else { // "primary" — original ink behavior, unchanged. fg = "#fff"; borderStyle = "none"; bg = disabled ? "rgba(20,32,28,0.22)" : pressed ? BTN_PRESS : hover ? BTN_HOVER : BTN_DEFAULT; } // Brightness filter for accent hover/press (no-op for primary/outlined) const filter = (variant === "accent" && !disabled) ? (pressed ? "brightness(0.88)" : hover ? "brightness(0.93)" : "none") : "none"; return ( ); } // ──────────────────────────────────────────────── // BottomSheetModal — slides up from the bottom inside the // IOSDevice (uses position:absolute with respect to the device). // ──────────────────────────────────────────────── function BottomSheetModal({ open, title, onClose, children }) { // Mount-then-animate: 'rendered' controls DOM presence, 'entered' controls // the open transform. We mount FIRST in the closed state, then flip 'entered' // on the next frame so the transition actually runs. const [rendered, setRendered] = useStateS(false); const [entered, setEntered] = useStateS(false); useEffectS(() => { if (open) { setRendered(true); // Wait two frames so the closed transform is committed before flipping. const r1 = requestAnimationFrame(() => { const r2 = requestAnimationFrame(() => setEntered(true)); // Stash inner id so we can cancel cleanup.r2 = r2; }); const cleanup = { r1 }; return () => { cancelAnimationFrame(cleanup.r1); if (cleanup.r2) cancelAnimationFrame(cleanup.r2); }; } else { setEntered(false); } }, [open]); // Lock body scroll while open (no-op inside iframe but tidy) useEffectS(() => { if (open) document.body.style.overflow = "hidden"; else document.body.style.overflow = ""; return () => { document.body.style.overflow = ""; }; }, [open]); if (!rendered) return null; return (
{ // Only unmount on the SHEET's transition (not the scrim's) if (!open && e.propertyName === "transform") setRendered(false); }} style={{ position: "absolute", inset: 0, zIndex: 100, pointerEvents: open ? "auto" : "none", }} > {/* Scrim */}
{/* Sheet */}
{/* Drag handle */}
{/* Header */}

{title}

{/* Content */}
{children}
); } // ──────────────────────────────────────────────── // Fake Spanish content for the legal modals // ──────────────────────────────────────────────── const TERMS_BODY = ( <>

Última actualización: 28 de abril de 2026

1. Aceptación del servicio

Al utilizar Yomee aceptas estos términos y condiciones en su totalidad. Si no estás de acuerdo con alguno de ellos, te pedimos abstenerte de usar la aplicación. Yomee es una herramienta de orientación financiera personal y no constituye asesoría profesional ni una oferta de productos crediticios.

2. Uso de la cuenta

Eres responsable de mantener la confidencialidad de tus credenciales y de toda actividad que se realice desde tu cuenta. Yomee podrá suspender o cancelar tu acceso si detecta uso indebido, fraudulento o contrario a la legislación aplicable en los Estados Unidos Mexicanos.

3. Diagnóstico financiero (Radiografía)

Los resultados del diagnóstico se generan a partir de la información que tú proporcionas y de cálculos públicos sobre el Costo Anual Total (CAT). Las recomendaciones tienen carácter informativo; te invitamos a contrastarlas con un asesor de confianza antes de tomar decisiones financieras importantes.

4. Recomendaciones de productos

Yomee podrá sugerir tarjetas, créditos o productos de instituciones financieras reguladas. Estas sugerencias se basan en tu perfil; no implican aprobación automática y los términos finales dependen de cada emisor. Yomee no cobra comisión por mostrarte estas recomendaciones.

5. Limitación de responsabilidad

Yomee se ofrece "tal cual" y hace su mejor esfuerzo por mantener la información actualizada. No nos hacemos responsables por pérdidas derivadas de decisiones tomadas con base en el diagnóstico, ni por interrupciones del servicio fuera de nuestro control.

6. Modificaciones

Podemos actualizar estos términos cuando sea necesario. Te notificaremos de cambios relevantes a través de la app y por correo electrónico. El uso continuado del servicio después de la notificación implica tu aceptación de la nueva versión.

7. Contacto

Si tienes preguntas sobre estos términos, escríbenos a{" "} hola@yomee.ai. Responderemos en un plazo máximo de cinco días hábiles.

); const PRIVACY_BODY = ( <>

Vigencia: 28 de abril de 2026

Quién es el responsable

Yomee Tecnología, S.A.P.I. de C.V., con domicilio en la Ciudad de México, es la responsable del tratamiento de tus datos personales. Contamos con un encargado de protección de datos disponible en privacidad@yomee.ai.

Qué información recabamos

Recabamos datos de identificación (nombre, correo), datos financieros que tú compartes (montos de deuda, pagos, tarjetas registradas) y datos técnicos (modelo de dispositivo, versión del sistema). Solo recolectamos lo estrictamente necesario para ofrecerte tu diagnóstico.

Para qué los usamos

Utilizamos tus datos para generar tu radiografía financiera, sugerirte productos con mejores condiciones y mejorar el servicio. Nunca venderemos tu información a terceros con fines publicitarios.

Con quién los compartimos

Compartimos información con proveedores tecnológicos (almacenamiento en la nube, analítica) bajo acuerdos estrictos de confidencialidad. En ningún caso entregamos datos sensibles a instituciones financieras sin tu consentimiento expreso.

Tus derechos ARCO

Puedes Acceder, Rectificar, Cancelar u Oponerte al tratamiento de tus datos en cualquier momento, escribiendo a privacidad@yomee.ai. También puedes revocar tu consentimiento o limitar el uso y divulgación de tu información.

Seguridad

Aplicamos medidas físicas, técnicas y administrativas para proteger tus datos: cifrado en tránsito y en reposo, acceso por roles y monitoreo continuo. Si detectamos un incidente relevante, te notificaremos en un plazo no mayor a 72 horas.

Cambios al aviso

Si modificamos este aviso, publicaremos la nueva versión en la app y te enviaremos un resumen de los cambios por correo electrónico antes de su entrada en vigor.

); const legalH3 = { margin: "20px 0 6px", fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: 14, letterSpacing: "0.02em", color: "var(--yo-ink)", }; const legalP = { margin: 0, fontFamily: "var(--font-body)", fontSize: 14, lineHeight: "22px", letterSpacing: "0.01em", color: "var(--yo-ink)", }; window.PrimaryButton = PrimaryButton; window.BottomSheetModal = BottomSheetModal; window.TERMS_BODY = TERMS_BODY; window.PRIVACY_BODY = PRIVACY_BODY; // ──────────────────────────────────────────────── // HelpTooltip — GLOBAL, top-right anchored. // Single source of truth for the app. Mounted once at the root via // . Header "?" buttons open it by calling // window.YomeeHelp.toggle(screenKey). Only one tooltip is open at a time. // Content is defined statically per screenKey; if no content exists, // the toggle is a no-op so the icon just doesn't do anything. // ──────────────────────────────────────────────── const HELP_COPY = { signup: { title: "¿Por qué pedimos esto?", body: "Solo usamos tu correo y contraseña para guardar tus consultas y mantener tu cuenta segura. Nunca compartimos tus datos con terceros.", }, signin: { title: "¿Necesitas ayuda?", body: "Si olvidaste tu contraseña, usa la opción de recuperación debajo del botón de acceso.", }, }; // ──────────────────────────────────────────────── // HELP_SHEET_COPY — bottom-sheet help content (separate from tooltip HELP_COPY). // One entry per screenKey. window.YomeeHelp.toggle(screenKey) opens the sheet // when the key has an entry here. Keys: "personal-info", "otp", // "hagamos-numeros", "upload". // // Each entry: { title, paragraphs: [string, ...] } // ──────────────────────────────────────────────── const HELP_SHEET_COPY = { "personal-info": { title: "¿Por qué te pedimos esto?", paragraphs: [ "Tu nombre nos permite personalizar tu experiencia y dirigirnos a ti de forma cercana.", "Tu edad nos ayuda a entender tu momento financiero y ofrecerte contexto relevante para tu etapa de vida.", "Tu género nos permite comunicarnos contigo con el lenguaje correcto.", "Ninguno de estos datos se comparte con terceros ni se usa para fines publicitarios.", ], }, "otp": { title: "¿Problemas con tu código?", paragraphs: [ "Puedes solicitar un nuevo código cada 60 segundos usando la opción 'Reenviar código' en la pantalla anterior.", "Si el problema persiste o necesitas ayuda adicional, escríbenos a hola@yomee.ai y te respondemos a la brevedad.", ], }, "hagamos-numeros": { title: "No necesitas los números exactos", paragraphs: [ "Los montos que ingreses aquí son solo para darte una primera idea de tu situación financiera.", "Puedes usar cifras aproximadas — si debes alrededor de $60,000, escribe eso. El resultado seguirá siendo útil.", "Tu radiografía real y detallada se genera a partir de tu estado de cuenta, no de estos números.", ], }, "profile-result": { title: "¿Cómo determinamos tu perfil?", paragraphs: [ "Tu perfil se basa en la relación entre tu deuda total y tu pago mensual. Es una primera lectura, no un diagnóstico definitivo.", "Totalero: pagas todo cada mes. Optimizador: pagas más del mínimo. Escalador: pagas cerca del mínimo. Reiniciador: hoy no estás pagando.", "Tu radiografía completa puede ajustar este perfil cuando analicemos tu estado de cuenta real.", ], }, "upload": { title: "Tu información está segura", paragraphs: [ "Tu estado de cuenta se usa únicamente para generar tu radiografía financiera. Nadie más tiene acceso a él.", "Una vez procesado, el archivo se elimina de forma permanente de nuestros servidores — no lo almacenamos, no lo vendemos, no lo compartimos.", "Solo conservamos los datos estructurados necesarios para mostrarte tus insights: montos, fechas y categorías.", ], }, }; // External controller — header buttons call this directly. // open(screenKey) opens that screen's tooltip; close() closes whatever's open; // toggle(screenKey) opens it (or closes if it's already that key). window.YomeeHelp = window.YomeeHelp || { _listeners: new Set(), _active: null, _subscribe(fn) { this._listeners.add(fn); return () => this._listeners.delete(fn); }, _emit() { this._listeners.forEach((fn) => fn(this._active)); }, _has(screenKey) { return !!(HELP_COPY[screenKey] || HELP_SHEET_COPY[screenKey]); }, open(screenKey) { if (!this._has(screenKey)) return; // no-op if no content this._active = screenKey; this._emit(); }, close() { this._active = null; this._emit(); }, toggle(screenKey) { if (!this._has(screenKey)) return; // no-op if no content this._active = this._active === screenKey ? null : screenKey; this._emit(); }, }; function GlobalHelpHost() { const [active, setActive] = useStateS(window.YomeeHelp._active); const [entered, setEntered] = useStateS(false); const [rendered, setRendered] = useStateS(!!window.YomeeHelp._active); const [flipUp, setFlipUp] = useStateS(false); const cardRef = React.useRef(null); useEffectS(() => { return window.YomeeHelp._subscribe(setActive); }, []); // Mount/unmount with fade in (150ms ease-out) / fade out (100ms) useEffectS(() => { if (active) { setRendered(true); const r1 = requestAnimationFrame(() => requestAnimationFrame(() => setEntered(true)) ); return () => cancelAnimationFrame(r1); } else { setEntered(false); const t = setTimeout(() => setRendered(false), 110); return () => clearTimeout(t); } }, [active]); // Flip up if the card would overflow vertically. useEffectS(() => { if (!rendered || !cardRef.current) return; const el = cardRef.current; // Measure against the device frame's content area, not the OS window. const host = el.offsetParent || document.body; const hostRect = host.getBoundingClientRect(); const rect = el.getBoundingClientRect(); const overflowsBottom = rect.bottom > hostRect.bottom - 8; setFlipUp(overflowsBottom); }, [rendered, active]); if (!rendered || !active) return null; const copy = HELP_COPY[active]; if (!copy) return null; // Anchor: 8px gap below the "?" icon. The icon sits at top:70 (auth) or // top:64+4=68 (onboarding), with height 30. Use the larger so we never // overlap. 70 + 30 + 8 = 108. const TOP_OFFSET = 108; return ( <> {/* Tap-outside scrim — covers everything below the tooltip */}
window.YomeeHelp.close()} style={{ position: "absolute", inset: 0, background: "transparent", zIndex: 9998, opacity: entered ? 1 : 0, transition: "opacity 100ms ease-out", }} />
{/* Tail pointing up-right at the ? icon (suppressed when flipped) */} {!flipUp && (
)} {flipUp && (
)}
{copy.title}
{copy.body}
); } window.GlobalHelpHost = GlobalHelpHost; // ──────────────────────────────────────────────── // HelpSheetHost — bottom-sheet help modal. // Drives off the same window.YomeeHelp controller as GlobalHelpHost, but // only renders for screenKeys that have an entry in HELP_SHEET_COPY (the // tooltip host renders the rest). Mounted once at the app root. // // Spec (per design): // • Slides up from bottom, ~200ms ease-out // • White background, 20px top border-radius // • Handle bar: 36×4, #E0E0E0, centered, 8px from top // • Backdrop: rgba(0,0,0,0.4); tap to close // • Title: 17px, Geist Mono, weight 900, #1a1a1a // • Body: 14px, #555, line-height 1.6, paragraphs separated by 12px gap // • Close CTA: "Entendido" — full-width, black bg, white, mono caps, // 16px margin-top // ──────────────────────────────────────────────── function HelpSheetHost() { const [active, setActive] = useStateS(window.YomeeHelp._active); const [rendered, setRendered] = useStateS(!!window.YomeeHelp._active); const [entered, setEntered] = useStateS(false); useEffectS(() => { return window.YomeeHelp._subscribe(setActive); }, []); // Only react to keys that belong to the sheet host. const copy = active ? HELP_SHEET_COPY[active] : null; useEffectS(() => { if (copy) { setRendered(true); const r1 = requestAnimationFrame(() => { const r2 = requestAnimationFrame(() => setEntered(true)); cleanup.r2 = r2; }); const cleanup = { r1 }; return () => { cancelAnimationFrame(cleanup.r1); if (cleanup.r2) cancelAnimationFrame(cleanup.r2); }; } else { setEntered(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [copy]); if (!rendered) return null; // copy may briefly be null while exit-animating — keep the last copy in a // ref-like fallback so the closing transition still has content to render. const displayCopy = copy || HelpSheetHost._last; if (copy) HelpSheetHost._last = copy; if (!displayCopy) return null; const close = () => window.YomeeHelp.close(); return (
{ if (!copy && e.propertyName === "transform") setRendered(false); }} style={{ position: "absolute", inset: 0, zIndex: 9997, pointerEvents: copy ? "auto" : "none", }} > {/* Backdrop */}
{/* Sheet */}
{/* Handle bar */}
{/* Content */}

{displayCopy.title}

{displayCopy.paragraphs.map((p, i) => (

{p}

))}
Entendido
); } window.HelpSheetHost = HelpSheetHost; // Legacy shim — older code paths reference HelpTooltip / window.HelpTooltip // directly. Keep them as no-op components so nothing crashes; the real // tooltip is now driven by the global controller above (window.YomeeHelp + // ). function HelpTooltip() { return null; } window.HelpTooltip = HelpTooltip;