// Yomee — Profile Screen ("Mi perfil")
// Entered from the avatar in the Radiografía header. Covers RF-033 → RF-038:
// • Mi información (RF-033)
// • Notificaciones (RF-034)
// • Privacidad y seguridad (RF-035)
// • Mi suscripción (RF-036)
// • Términos y privacidad (RF-037)
// • Cerrar sesión (RF-038)
//
// Uses the project's existing design tokens (var(--yo-*) + Onest/Geist Mono).
// Profile chip colors map to the 4 personas already defined elsewhere
// (Totalero / Optimizador / Escalador / Reiniciador).
//
// All sub-screens (suscripción, info, notificaciones, privacidad, legales)
// live in this same file as small inner views, switched via local state.
// They share a single PageShell with a sticky back-header.
/* global React, RadioIcon, useYomeeUser, BottomSheetModal, TERMS_BODY, PRIVACY_BODY, AyudaScreen */
const { useState: useStatePF, useEffect: useEffectPF } = React;
// Body background — matches Radiografía body so the transition feels seamless.
const PF_BG = "rgb(238, 235, 230)";
const PF_FG = "var(--yo-ink)";
const PF_FG_MUTED = "#6b6b6b";
const PF_FG_SOFT = "#9aa0a6";
const PF_LINE = "rgba(20,32,28,0.08)";
const PF_BRAND = "var(--yo-forest-700)"; // #19553e — primary brand action
const PF_DANGER = "var(--yo-red-strong)"; // #dc4046 — destructive
const PF_IVORY = "#fbf7ee"; // upgrade / privacy banner
// Profile chip colors — match RADIO_HEADER from RadiografiaScreen.jsx
const PROFILE_CHIP = {
totalero: { bg: "#1a6b3f", label: "Totalero" },
optimizador: { bg: "#185fa5", label: "Optimizador" },
escalador: { bg: "#7c2d12", label: "Escalador" },
reiniciador: { bg: "#4c1d95", label: "Reiniciador" },
};
// Subscription tiers per persona (mocked).
const TIER_BY_PERSONA = {
ana: { tier: "Free", price: null, cta: "Mejora tu plan" },
luis: { tier: "Plus", price: "$99 MXN/mes", cta: null },
carlos: { tier: "Free", price: null, cta: "Mejora tu plan" },
sofia: { tier: "Pro", price: "$199 MXN/mes", cta: null },
};
// ────────────────────────────────────────────────────────────────────
// Reusable building blocks (scoped to this file so they don't collide)
// ────────────────────────────────────────────────────────────────────
function PFAvatar({ initials, size = 64, fontSize = 22, bg }) {
return (
{initials}
);
}
function PFCard({ children, style }) {
return (
);
}
// Section row — icon + title (+ optional subtitle) + chevron.
function PFRow({ icon, title, subtitle, subtitleColor, onClick, last, danger }) {
const [pressed, setPressed] = useStatePF(false);
return (
setPressed(true)}
onMouseUp={() => setPressed(false)}
onMouseLeave={() => setPressed(false)}
onTouchStart={() => setPressed(true)}
onTouchEnd={() => setPressed(false)}
style={{
display: "flex",
alignItems: "center",
gap: 14,
width: "100%",
padding: "14px 16px",
background: pressed ? "#f5f5f5" : "transparent",
border: "none",
borderBottom: last ? "none" : `1px solid ${PF_LINE}`,
cursor: "pointer",
textAlign: "left",
transition: "background 100ms ease-out",
fontFamily: "inherit",
}}
>
{icon}
{title}
{subtitle && (
{subtitle}
)}
);
}
// Sticky header with back arrow + title.
function PFHeader({ title, onBack }) {
return (
);
}
function PageShell({ title, onBack, children }) {
return (
);
}
// ────────────────────────────────────────────────────────────────────
// Inline icons — kept tiny and consistent (24px stroke, 1.6 weight)
// We don't reuse RadioIcon here because the section icons are distinct
// from the dashboard's categorical iconography.
// ────────────────────────────────────────────────────────────────────
function PFIcon({ name }) {
const props = {
width: 22,
height: 22,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.6,
strokeLinecap: "round",
strokeLinejoin: "round",
};
switch (name) {
case "star":
return (
);
case "user":
return (
);
case "bell":
return (
);
case "shield":
return (
);
case "help":
return (
);
case "doc":
return (
);
case "trash":
return (
);
case "external":
return (
);
case "logout":
return (
);
default:
return null;
}
}
// ────────────────────────────────────────────────────────────────────
// Identity hero card — used in the main /perfil view
// ────────────────────────────────────────────────────────────────────
function IdentityCard({ user, profileKey, onEdit }) {
const chip = PROFILE_CHIP[profileKey] || PROFILE_CHIP.totalero;
return (
Tu perfil se actualiza conforme avanzas. Cada estado de cuenta nuevo refina tu radiografía.
);
}
// ────────────────────────────────────────────────────────────────────
// Logout confirmation modal
// ────────────────────────────────────────────────────────────────────
function LogoutModal({ onCancel, onConfirm }) {
return (
¿Cerrar sesión?
Tendrás que volver a iniciar sesión la próxima vez.
Sí, cerrar
Cancelar
);
}
// ────────────────────────────────────────────────────────────────────
// MAIN PROFILE VIEW
// ────────────────────────────────────────────────────────────────────
function MainProfileView({ user, profileKey, tier, notifsOn, onNav, onLogout }) {
return (
onNav("info")} />
{/* SECTIONS LIST */}
}
title="Mi suscripción"
subtitle={
tier.tier === "Free"
? "Plan Free · Mejora tu plan"
: `Plan ${tier.tier} · ${tier.price}`
}
subtitleColor={tier.tier === "Free" ? PF_BRAND : PF_FG_MUTED}
onClick={() => onNav("suscripcion")}
/>
}
title="Mi información"
subtitle="Nombre, email, ciudad"
onClick={() => onNav("info")}
/>
}
title="Notificaciones"
subtitle={notifsOn ? "Activadas" : "Desactivadas"}
onClick={() => onNav("notificaciones")}
/>
}
title="Privacidad y seguridad"
subtitle="Tus datos están protegidos"
onClick={() => onNav("privacidad")}
/>
}
title="Centro de ayuda"
subtitle="Preguntas frecuentes y soporte"
onClick={() => onNav("ayuda")}
last
/>
{/* LOGOUT */}
{/* VERSION FOOTER */}
yomee.ai · v1.0.0 · Hecho con 🦔 en México
);
}
function LogoutButton({ onClick }) {
const [pressed, setPressed] = useStatePF(false);
return (
setPressed(true)}
onMouseUp={() => setPressed(false)}
onMouseLeave={() => setPressed(false)}
onTouchStart={() => setPressed(true)}
onTouchEnd={() => setPressed(false)}
style={{
width: "100%",
height: 56,
borderRadius: 16,
background: pressed ? "rgba(51,51,51,0.04)" : "transparent",
color: PF_DANGER,
border: "1.5px solid var(--yo-ink)",
padding: "0 20px",
fontFamily: "var(--font-mono)",
fontSize: 13,
fontWeight: 700,
letterSpacing: "0.14em",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: 10,
transition: "background 160ms ease-out",
whiteSpace: "nowrap",
}}
>
CERRAR SESIÓN
);
}
// ────────────────────────────────────────────────────────────────────
// SUB-VIEW: Mi suscripción (RF-036)
// ────────────────────────────────────────────────────────────────────
const TIER_BENEFITS = {
Free: [
"1 estado de cuenta al mes",
"Radiografía básica",
"Detección de oportunidades",
],
Plus: [
"Hasta 5 estados de cuenta al mes",
"Radiografía completa multibanco",
"Recomendaciones personalizadas",
"Recordatorios de pago",
],
Pro: [
"Estados de cuenta ilimitados",
"Plan de consolidación con Pocopin",
"Análisis de patrones avanzados",
"Soporte prioritario",
],
};
const NEXT_TIER = { Free: "Plus", Plus: "Pro", Pro: null };
const TIER_PRICE = { Free: "Gratis", Plus: "$99 MXN/mes", Pro: "$199 MXN/mes" };
function SuscripcionView({ tier, onBack }) {
const next = NEXT_TIER[tier.tier];
const benefits = TIER_BENEFITS[tier.tier] || [];
const nextBenefits = next ? TIER_BENEFITS[next] || [] : [];
return (
{/* Current tier card */}
Tu plan actual
{tier.tier}
· {TIER_PRICE[tier.tier]}
{benefits.map((b, i) => (
{b}
))}
{/* Upgrade card (ivory) */}
{next && (
Sigue creciendo · {next}
Lo que sumas con {next}
{nextBenefits.map((b, i) => (
+
{b}
))}
Mejorar mi plan
{TIER_PRICE[next]} · Cancela cuando quieras
)}
{/* Payment history */}
Historial de pagos
{tier.tier === "Free" ? (
Aún no tienes pagos. Tu plan {tier.tier} no tiene cargo.
) : (
[
{ date: "01 Oct 2025", amount: tier.price },
{ date: "01 Sep 2025", amount: tier.price },
{ date: "01 Ago 2025", amount: tier.price },
].map((row, i, arr) => (
{row.date}
{row.amount}
))
)}
);
}
// ────────────────────────────────────────────────────────────────────
// SUB-VIEW: Mi información (RF-033)
// ────────────────────────────────────────────────────────────────────
function InfoView({ user, onBack }) {
const [name, setName] = useStatePF(user.name || "");
const [city, setCity] = useStatePF("CDMX");
const [dob, setDob] = useStatePF("12 / 03 / 1992");
return (
Guardar cambios
);
}
function Field({ label, value, onChange, readOnly, last }) {
return (
{label}
onChange && onChange(e.target.value)}
style={{
marginTop: 4,
width: "100%",
background: "transparent",
border: "none",
padding: 0,
fontFamily: "var(--font-body)",
fontSize: 15,
fontWeight: 400,
color: readOnly ? PF_FG_MUTED : PF_FG,
letterSpacing: "0.005em",
outline: "none",
}}
/>
);
}
// ────────────────────────────────────────────────────────────────────
// SUB-VIEW: Notificaciones (RF-034)
// ────────────────────────────────────────────────────────────────────
const NOTIF_OPTIONS = [
{
key: "carga",
title: "Recordatorio de carga mensual",
desc: "Avisos para subir tu estado de cuenta cada mes.",
def: true,
},
{
key: "oportunidades",
title: "Oportunidades de mercado",
desc: "Cuando detectamos algo que puede ayudarte a ahorrar.",
def: true,
},
{
key: "novedades",
title: "Novedades de yomee",
desc: "Nuevas funciones y mejoras en la app.",
def: false,
},
{
key: "pagos",
title: "Recordatorios de pago próximo",
desc: "Antes de la fecha límite para que no se te olvide.",
def: true,
},
];
function NotificacionesView({ values, setValues, onBack }) {
return (
{NOTIF_OPTIONS.map((opt, i, arr) => (
setValues({ ...values, [opt.key]: v })}
last={i === arr.length - 1}
/>
))}
Puedes cambiar tus preferencias en cualquier momento. No usamos tus datos para anuncios.
);
}
function ToggleRow({ title, desc, value, onChange, last }) {
return (
onChange(!value)}
style={{
width: 44,
height: 26,
borderRadius: 999,
background: value ? PF_BRAND : "#d4d4d4",
border: "none",
padding: 2,
cursor: "pointer",
flexShrink: 0,
marginTop: 2,
transition: "background 160ms ease-out",
position: "relative",
}}
>
);
}
// ────────────────────────────────────────────────────────────────────
// SUB-VIEW: Privacidad y seguridad (RF-035)
// ────────────────────────────────────────────────────────────────────
function PrivacidadView({ onBack, onOpenLegal, onDeleteAccount }) {
return (
{/* Ivory banner */}
Tus estados de cuenta originales se eliminan en cuanto procesamos los datos
(NIST 800-88). Solo guardamos los números, nunca el documento.
{/* 3 rows: Términos, Aviso de privacidad, Eliminar cuenta */}
}
title="Términos y condiciones"
onClick={() => onOpenLegal("terms")}
/>
}
title="Aviso de privacidad"
onClick={() => onOpenLegal("privacy")}
/>
}
title="Eliminar cuenta"
danger
onClick={onDeleteAccount}
last
/>
);
}
// ────────────────────────────────────────────────────────────────────
// SUB-VIEW: Términos y aviso de privacidad (RF-037)
// ────────────────────────────────────────────────────────────────────
function LegalesView({ onBack }) {
return (
}
title="Términos de uso"
subtitle="Condiciones del servicio"
onClick={() => {}}
/>
}
title="Aviso de privacidad LFPDPPP"
subtitle="Cómo tratamos tus datos personales"
onClick={() => {}}
/>
}
title="Licencias"
subtitle="Software de terceros"
onClick={() => {}}
last
/>
);
}
// ────────────────────────────────────────────────────────────────────
// Generic placeholder sub-view (Centro de ayuda + privacy children)
// ────────────────────────────────────────────────────────────────────
function PlaceholderView({ title, body, onBack }) {
return (
{body}
);
}
// ────────────────────────────────────────────────────────────────────
// Top-level ProfileScreen — owns the in-section route + logout modal
// ────────────────────────────────────────────────────────────────────
function ProfileScreen({ onBack, onLoggedOut }) {
// Active persona drives identity + chip color + tier.
const yomeeUser = (window.useYomeeUser && window.useYomeeUser()) || {};
const personaKey = yomeeUser.personaKey || "ana";
const profileKey = yomeeUser.profile || "totalero";
const tier = TIER_BY_PERSONA[personaKey] || TIER_BY_PERSONA.ana;
// In-section sub-route (lives inside ProfileScreen so the parent route
// stays "perfil" and the back button on the main view returns to the
// dashboard, not to a stale sub-view).
const [sub, setSub] = useStatePF("main");
// Legal modal opened from Privacidad: "terms" | "privacy" | null.
// Reuses the same BottomSheetModal + TERMS_BODY / PRIVACY_BODY shipped
// from Shared.jsx and used by sign-up/sign-in.
const [legal, setLegal] = useStatePF(null);
const [showLogout, setShowLogout] = useStatePF(false);
// Notification toggles — local state, persisted only for the session.
const [notifs, setNotifs] = useStatePF(() => {
const init = {};
NOTIF_OPTIONS.forEach((o) => (init[o.key] = o.def));
return init;
});
const notifsOn = Object.values(notifs).some(Boolean);
// Keep the inner route consistent if persona switches mid-flow.
useEffectPF(() => {
setSub("main");
setLegal(null);
setShowLogout(false);
}, [personaKey]);
// "Eliminar cuenta" — provisional: opens yomee.ai in a new tab.
function handleDeleteAccount() {
window.open("https://www.yomee.ai", "_blank");
}
function handleLogoutConfirm() {
setShowLogout(false);
if (onLoggedOut) onLoggedOut();
}
return (
{sub === "main" && (
setSub(k)}
onLogout={() => setShowLogout(true)}
/>
)}
{sub === "suscripcion" && (
setSub("main")} />
)}
{sub === "info" && (
setSub("main")} />
)}
{sub === "notificaciones" && (
setSub("main")}
/>
)}
{sub === "privacidad" && (
setSub("main")}
onOpenLegal={(k) => setLegal(k)}
onDeleteAccount={handleDeleteAccount}
/>
)}
{sub === "ayuda" && (
setSub("main")} />
)}
{/* Reused legal modals — same components/copy as sign-up/sign-in. */}
setLegal(null)}
>
{TERMS_BODY}
setLegal(null)}
>
{PRIVACY_BODY}
{showLogout && (
setShowLogout(false)}
onConfirm={handleLogoutConfirm}
/>
)}
);
}
window.ProfileScreen = ProfileScreen;