// Yomee — App.jsx // Router + App shell + ReactDOM mount. Loads last so all screens are declared. /* global React, ReactDOM, IOSDevice, GlobalHelpHost, HelpSheetHost, StartScreen, SignUpScreen, SignInScreen, PersonalInfoScreen, IdentityValidationScreen, OnboardingWelcomeScreen, PushNotificationsScreen, NativePushDialog, Question1Screen, ProfileResultScreen, ProfileQuestionScreen, UploadScreen, AnalyzingScreen, ExtractionReviewScreen, RadiografiaScreen, QUESTIONS_BY_PROFILE, ProfileScreen, TweaksPanel, TweakSection, TweakRadio, TweakButton, useTweaks, FAKE_USER */ const { useState: useStateApp } = React; // ──────────────────────────────────────────────── // In-app persona switcher (prototype only) // 4 fixed personas — Ana / Luis / Carlos / Sofía — each maps to one of the // classified profiles. The active persona is selected via the floating // switcher pill (see PersonaSwitcher below). On switch, window.YomeeUser is // rewritten and the Router is force-remounted so every screen re-reads the // new values from screen 1. // ──────────────────────────────────────────────── const YOMEE_PERSONAS = [ { key: "ana", name: "Ana", email: "ana@email.com", age: "29", gender: "female", debt: 45000, payment: 45000, label: "Totalera", profile: "totalero", dot: "#1a6b3f", }, { key: "luis", name: "Luis", email: "luis@email.com", age: "32", gender: "male", debt: 65000, payment: 12000, label: "Optimizador", profile: "optimizador", dot: "#185fa5", }, { key: "carlos", name: "Carlos", email: "carlos@email.com", age: "36", gender: "male", debt: 80000, payment: 900, label: "Escalador", profile: "escalador", dot: "#7c2d12", }, { key: "sofia", name: "Sofía", email: "sofia@email.com", age: "34", gender: "female", debt: 35000, payment: 0, label: "Reiniciadora", profile: "reiniciador", dot: "#4c1d95", }, ]; function __yomeeBuildUser(persona) { return { name: persona.name, email: persona.email, age: persona.age, gender: persona.gender, debt: persona.debt, payment: persona.payment, profile: persona.profile, personaKey: persona.key, }; } // Default active persona on load: Ana (Totalero). const __YOMEE_INITIAL_PERSONA = YOMEE_PERSONAS[0]; // Helpers used across screens. function __yomeeFirstName(full) { return (full || "").split(" ")[0] || ""; } function __yomeeInitials(full) { const s = (full || "").trim(); if (!s) return ""; // First two letters of the first name, uppercased. return s.split(" ")[0].slice(0, 2).toUpperCase(); } // Sync window.YomeeUser + the Google/Apple fake-account fixtures to the // active persona. Called once at module init AND every time the user picks // a different persona from the switcher. function __yomeeApplyPersona(persona) { const u = __yomeeBuildUser(persona); window.YomeeUser = { ...u, firstName: __yomeeFirstName(u.name), initials: __yomeeInitials(u.name), }; if (window.FAKE_USER) { ["google", "apple"].forEach((k) => { if (window.FAKE_USER[k]) { window.FAKE_USER[k].name = u.name; window.FAKE_USER[k].email = u.email; window.FAKE_USER[k].initial = (u.name[0] || "M").toUpperCase(); } }); } } __yomeeApplyPersona(__YOMEE_INITIAL_PERSONA); // ──────────────────────────────────────────────── // Single global state source for the active persona. Every screen reads from // this context — no screen keeps its own local copy of persona fields // (name/email/age/gender/debt/payment) so a switch in the Demo pill // propagates instantly to all mounted screens. // ──────────────────────────────────────────────── const YomeeUserContext = React.createContext(window.YomeeUser); window.YomeeUserContext = YomeeUserContext; function useYomeeUser() { return React.useContext(YomeeUserContext); } window.useYomeeUser = useYomeeUser; const ROUTES = [ "start", "signup", "signin", "personal-info", "identity", "onb-welcome", "push", "question-1", "profile-result", "profile-questions", "upload", "analyzing", "review", "radiografia", "perfil", ]; function Router() { const [route, setRoute] = useStateApp("start"); const [direction, setDirection] = useStateApp("forward"); const [prev, setPrev] = useStateApp(null); const [authUser, setAuthUser] = useStateApp(null); // The active persona is the single source of truth for name/age/email/ // gender/debt/payment. Pull it from context so a Demo-pill switch // updates every mounted screen instantly. const yomeeUser = useYomeeUser(); // Local override for debt/monthly only when the user actually edits the // sliders during their session. null → show the active persona's values. const [debtOverride, setDebtOverride] = useStateApp(null); const debt = debtOverride != null ? debtOverride : { total: yomeeUser?.debt ?? 0, monthly: yomeeUser?.payment ?? 0 }; const [pushDialog, setPushDialog] = useStateApp(false); // Profile-question state: which step of the per-profile question list, // and whether the user reached upload by skipping. const [pqStep, setPqStep] = useStateApp(0); const [pqSkipped, setPqSkipped] = useStateApp(false); const [pqProfile, setPqProfile] = useStateApp("optimizador"); // Stored answer to the "¿Cuántas tarjetas de crédito tienes activas?" question. // null = not asked / skipped (treated as "1 tarjeta" downstream). // Possible values when set: "1 tarjeta" | "2 tarjetas" | "3 o más" const [cardsAnswer, setCardsAnswer] = useStateApp(null); // Uploaded files — lifted from UploadScreen so the Radiografía screen // can read the user's actual upload state (bank chips, progress bar, // counter), and so the user can return to Upload and add the missing // statements without losing what they already uploaded. // Each entry: { name, bank } where bank ∈ "bbva" | "nu" | "banorte". const [uploadedFiles, setUploadedFiles] = useStateApp([]); // Radiografía profile to render — driven by Tweaks. Defaults to Totalero. const [radioProfile, setRadioProfile] = useStateApp("totalero"); const [radioScenario, setRadioScenario] = useStateApp("A"); // Forced archetype on the profile-result screen — driven by Tweaks. // null = classify from debt/payment as usual. const [forcedArchetype, setForcedArchetype] = useStateApp(null); // Listen to Tweaks for profile + initial route. React.useEffect(() => { const onMsg = (e) => { if (!e?.data || typeof e.data !== "object") return; if (e.data.type === "yo-set-radio-profile") { setRadioProfile(e.data.profile); } if (e.data.type === "yo-set-radio-scenario") { setRadioScenario(e.data.scenario); } if (e.data.type === "yo-go-radiografia") { setRoute("radiografia"); } if (e.data.type === "yo-go-start") { setRoute("start"); } if (e.data.type === "yo-go-perfil") { setRoute("perfil"); } if (e.data.type === "yo-go-review") { setRoute("review"); } if (e.data.type === "yo-set-archetype") { setForcedArchetype(e.data.profile || null); } if (e.data.type === "yo-go-archetype") { if (e.data.profile) setForcedArchetype(e.data.profile); setRoute("profile-result"); } }; window.addEventListener("message", onMsg); return () => window.removeEventListener("message", onMsg); }, []); function navigate(next, dir = "forward") { if (next === route) return; setDirection(dir); setPrev(route); setRoute(next); setTimeout(() => setPrev(null), 320); } function handleAuthSuccess(provider, user) { setAuthUser({ provider, user }); navigate("personal-info"); } function renderScreen(name) { if (name === "start") return ( navigate("signup")} onSignIn={() => navigate("signin")} /> ); if (name === "signup") return ( navigate("start", "back")} onSwitchToSignIn={() => navigate("signin")} onDemo={() => alert("Ver radiografía demo")} onAuthSuccess={handleAuthSuccess} /> ); if (name === "signin") return ( navigate("start", "back")} onSwitchToSignUp={() => navigate("signup", "back")} onAuthSuccess={handleAuthSuccess} /> ); if (name === "personal-info") return ( navigate("start", "back")} onContinue={() => navigate("identity")} /> ); if (name === "identity") return ( navigate("personal-info", "back")} onContinue={() => navigate("onb-welcome")} /> ); if (name === "onb-welcome") return ( navigate("push")} /> ); if (name === "push") return ( <> setPushDialog(true)} onSkip={() => navigate("question-1")} /> {pushDialog && ( { setPushDialog(false); setTimeout(() => navigate("question-1"), 240); }} onDeny={() => { setPushDialog(false); setTimeout(() => navigate("question-1"), 240); }} /> )} ); if (name === "question-1") return ( navigate("push", "back")} onContinue={({ total, monthly }) => { setDebtOverride({ total, monthly }); // Clear any Tweaks-forced archetype so the real // classification (debt / payment) wins on the result screen. // Without this, once a tester used the Tweaks "Ir a Archetype" // flow, forcedArchetype stuck around and pinned every // subsequent Question1 → profile-result transition to // "totalero" regardless of the numbers entered. setForcedArchetype(null); navigate("profile-result"); }} /> ); if (name === "profile-result") return ( { setPqProfile(p); setPqStep(0); setPqSkipped(false); setCardsAnswer(null); // Totalero has no questions — go straight to upload. const list = (window.QUESTIONS_BY_PROFILE || {})[p] || []; if (list.length === 0) navigate("upload"); else navigate("profile-questions"); }} /> ); if (name === "profile-questions") return ( { const list = (window.QUESTIONS_BY_PROFILE || {})[pqProfile] || []; // If the just-answered question is a "cards count" question, // capture the answer. Two question variants exist across // archetypes (Optimizador/Escalador ask about active cards; // Reiniciador asks about cards with pending payments) — both // use the same option set and feed the same denominator on // the Radiografía progress card. const justAnswered = list[pqStep]; const cardsQuestions = [ "¿Cuántas tarjetas de crédito tienes activas?", "¿Cuántas tarjetas tienes con pagos pendientes?", ]; if (justAnswered && cardsQuestions.includes(justAnswered.question)) { setCardsAnswer(answer); } if (pqStep + 1 >= list.length) { setPqSkipped(false); navigate("upload"); } else { setPqStep(pqStep + 1); // stay on profile-questions; key change re-mounts } }} onSkip={() => { setPqSkipped(true); navigate("upload"); }} onBack={() => { if (pqStep === 0) navigate("profile-result", "back"); else setPqStep(pqStep - 1); }} /> ); if (name === "upload") return ( { // Profile keys are already aligned with radiografía keys. setRadioProfile(p); // Always pass through the Analyzing transition before the // next destination is decided. The transition itself is // unconditional — the routing decision (Confidence Gate vs. // straight-to-Radiografía) happens when Analyzing finishes. navigate("analyzing"); }} onDemo={() => alert("Ver radiografía demo")} /> ); if (name === "analyzing") return ( { // Confidence Gate: only when the user uploaded all 3 files do // we route through the extraction-review step. Anything less // skips it entirely (per spec). if (uploadedFiles.length === 3) { navigate("review"); } else { navigate("radiografia"); } }} /> ); if (name === "review") return ( navigate("upload", "back")} onContinue={() => navigate("radiografia")} /> ); if (name === "radiografia") return ( navigate("upload", "back")} onOpenProfile={() => navigate("perfil")} /> ); if (name === "perfil") return ( navigate("radiografia", "back")} onLoggedOut={() => navigate("start", "back")} /> ); return null; } const enterFrom = direction === "forward" ? "100%" : "-100%"; const exitTo = direction === "forward" ? "-100%" : "100%"; return (
{prev && (
{renderScreen(prev)}
)}
{renderScreen(route)}
); } function YomeeTweaks() { const [profile, setProfile] = React.useState("totalero"); const [scenario, setScenario] = React.useState("A"); const [archetype, setArchetype] = React.useState("totalero"); React.useEffect(() => { window.postMessage({ type: "yo-set-radio-profile", profile }, "*"); }, [profile]); React.useEffect(() => { window.postMessage({ type: "yo-set-radio-scenario", scenario }, "*"); }, [scenario]); React.useEffect(() => { window.postMessage({ type: "yo-set-archetype", profile: archetype }, "*"); }, [archetype]); return ( window.postMessage({ type: "yo-go-start" }, "*")} /> window.postMessage({ type: "yo-go-perfil" }, "*")} /> window.postMessage({ type: "yo-go-review" }, "*")} /> {/* Order matters: pick the arquetipo FIRST, then tap the button. The button always navigates with the currently-selected arquetipo. Previously the button sat above the radio so users tapped it before changing the selection — and because a second (Radiografía) radio below has identical labels, clicks meant for this one were landing on the wrong control, leaving arquetipo at its default ("totalero") for every navigation. */} window.postMessage( { type: "yo-go-archetype", profile: archetype }, "*", ) } /> window.postMessage({ type: "yo-go-radiografia" }, "*")} /> ); } // ───────────────────────────────────────────────────────────────────── // PersonaSwitcher — floating pill (bottom-right) that lets testers // switch between the 4 personas without touching the URL. Tap the pill // to open a card panel with one row per persona; tap a row to switch. // Tapping outside or the pill again closes the panel. // ───────────────────────────────────────────────────────────────────── function PersonaSwitcher({ activeKey, onSelect }) { const [open, setOpen] = React.useState(false); const wrapRef = React.useRef(null); React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); } }; document.addEventListener("mousedown", onDoc); document.addEventListener("touchstart", onDoc); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("touchstart", onDoc); }; }, [open]); const active = YOMEE_PERSONAS.find((p) => p.key === activeKey) || YOMEE_PERSONAS[0]; return (
{open && (
Simular usuario
{YOMEE_PERSONAS.map((p) => { const isActive = p.key === activeKey; return ( ); })}
)}
); } function App() { const [personaKey, setPersonaKey] = React.useState(__YOMEE_INITIAL_PERSONA.key); // Derive the active persona from personaKey. This is the single source of // truth shared with every screen via YomeeUserContext below. const activePersona = React.useMemo(() => { const p = YOMEE_PERSONAS.find((x) => x.key === personaKey) || YOMEE_PERSONAS[0]; return { ...__yomeeBuildUser(p), firstName: __yomeeFirstName(p.name), initials: __yomeeInitials(p.name), }; }, [personaKey]); // Mirror the active persona to window.YomeeUser + the SSO fixtures for any // legacy code paths that still read those globals. Runs synchronously // before children render so context + globals stay aligned. React.useLayoutEffect(() => { const persona = YOMEE_PERSONAS.find((p) => p.key === personaKey) || YOMEE_PERSONAS[0]; __yomeeApplyPersona(persona); }, [personaKey]); return (
); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render();