// vita-auth.jsx — first view: sign in / create account, with the VITA heart logo function HeartLogo({ t, accent, size = 184 }) { return (
{/* soft glow — no hard circle */}
VITA
); } function AuthField({ t, icon, prefix, type = 'text', inputMode, value, onChange, placeholder, autoComplete, trailing, error, onBlur }) { const [focus, setFocus] = React.useState(false); return (
{icon} {prefix && {prefix}} onChange(e.target.value)} onFocus={() => setFocus(true)} onBlur={() => { setFocus(false); onBlur && onBlur(); }} placeholder={placeholder} style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', minWidth: 0, fontSize: 16, color: t.text, fontFamily: "'Inter', system-ui, sans-serif", fontWeight: 500, }} /> {trailing}
{error &&
{error}
}
); } // Guarda el token de acceso en el vault (memoria, nunca disco) y refleja al // usuario en Redux. Vive aquí y se repite (idéntica) en vita-root.jsx porque // es la única pieza que ambos archivos necesitan de la respuesta de auth, y // duplicar 4 líneas es más simple y robusto que inventar un acoplamiento // oculto entre dos scripts que Babel carga por separado. function guardarSesionDesdeRespuesta(dispatch, resp) { window.VitaCore.contenedor.vault.guardar({ accessToken: resp.accessToken, expiraEn: Date.now() + resp.expiresIn * 1000, }); const { sesion } = window.VitaCore.estado.acciones; dispatch(sesion.sesionIniciada(sesion.usuarioSesionDesdeBackend(resp.usuario))); } // Cada rol tiene su propia app (páginas .html separadas, mismo patrón que ya // existía para el panel médico). Si quien se loguea no es Paciente, esta // pestaña no es la suya: se navega de verdad (recarga completa) a la app de // su rol. El token de acceso vive en memoria de proceso — no sobrevive a un // window.location.href de todas formas — así que la app destino simplemente // vuelve a pedirlo con la cookie de refresh (ver el guardado que hace // vita-root.jsx / lo que haga el equivalente en cada app al arrancar). const RUTA_POR_ROL = { Medico: '/medico', Administrador: '/admin' }; function redirigirSiNoEsPaciente(rol) { const destino = RUTA_POR_ROL[rol]; if (destino) { window.location.href = destino; return true; } return false; } // Traduce los errores de RTK Query/el backend a algo que un paciente entienda. function mapAuthError(e) { const status = e && e.status; const msg = e && e.data && e.data.message; const primero = Array.isArray(msg) ? msg[0] : msg; if (status === 401) return 'Correo o contraseña incorrectos.'; if (status === 409) return primero || 'Ya existe una cuenta con este correo.'; if (status === 'FETCH_ERROR' || status === 0) return 'No pudimos conectar con Vita. Revisa tu conexión.'; return primero || 'Algo salió mal. Intenta de nuevo.'; } function AuthScreen({ t, persona, onAuthed, onToggleTheme, onFaceScan, onShowSecurity }) { const { useDispatch, api } = window.VitaCore.estado; const dispatch = useDispatch(); const [ingresar] = api.auth.useIngresarMutation(); const [registrar] = api.auth.useRegistrarMutation(); const [mode, setMode] = React.useState('login'); // login | signup const [name, setName] = React.useState(''); const [email, setEmail] = React.useState(''); const [pw, setPw] = React.useState(''); const [showPw, setShowPw] = React.useState(false); const [terms, setTerms] = React.useState(false); const [touched, setTouched] = React.useState({}); const [submit, setSubmit] = React.useState(null); // null | 'form' | 'face' const [authError, setAuthError] = React.useState(null); const accent = persona.accent; const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); const pwOk = pw.length >= 6; const nameOk = name.trim().length >= 2; const valid = mode === 'login' ? (emailOk && pwOk) : (nameOk && emailOk && pwOk && terms); const err = (k) => touched[k] || touched._submit; const emailErr = err('email') && email && !emailOk ? 'Correo no válido' : null; const pwErr = err('pw') && pw && !pwOk ? 'Mínimo 6 caracteres' : null; const nameErr = mode === 'signup' && err('name') && name && !nameOk ? 'Cuéntanos tu nombre' : null; const go = async (kind) => { if (kind === 'face') { onFaceScan && onFaceScan({ mode: 'login', method: 'face' }); return; } setTouched(s => ({ ...s, _submit: true })); setAuthError(null); if (!valid) return; if (mode === 'login') { setSubmit('form'); try { const resp = await ingresar({ email, password: pw }).unwrap(); guardarSesionDesdeRespuesta(dispatch, resp); if (!redirigirSiNoEsPaciente(resp.usuario.rol)) onAuthed(resp.usuario); } catch (e) { setAuthError(mapAuthError(e)); } finally { setSubmit(null); } return; } // Signup: la cuenta se crea de una vez contra el backend real. El // escaneo facial que sigue es un paso cosmético de bienvenida — no una // condición para que la cuenta exista. const partes = name.trim().split(/\s+/); const nombre = partes[0]; const apellido = partes.slice(1).join(' ') || partes[0]; setSubmit('form'); try { const resp = await registrar({ email, password: pw, nombre, apellido }).unwrap(); guardarSesionDesdeRespuesta(dispatch, resp); onFaceScan && onFaceScan({ enroll: true, usuario: resp.usuario }); } catch (e) { setAuthError(mapAuthError(e)); } finally { setSubmit(null); } }; const switchMode = (m) => { setMode(m); setTouched({}); setAuthError(null); }; // Layout partido: marca a la izquierda, formulario a la derecha. // Por debajo de 900px el panel de marca desaparece (no aporta y roba // espacio vertical al formulario, que es lo único accionable) y la // identidad se resuelve con un logo compacto sobre los campos. const anchoVentana = useAnchoVentana(); const partido = anchoVentana >= 900; const p = t.paleta; const VENTAJAS = [ ['heart', 'Historia clínica viva', 'Vita la construye y la mantiene al día por ti.'], ['mic', 'Consulta hablando', 'Sin formularios: le cuentas y ella registra.'], ['shield', 'Tus datos, tuyos', 'Cifrado de extremo a extremo. Tú decides quién ve qué.'], ]; return (
{partido && (
VITA

Tu salud, cuidada
todos los días.

La primera plataforma de prevención en salud por voz para América Latina.

{VENTAJAS.map(([ic, titulo, texto]) => (
{Icon[ic]({ s: 19, c: '#fff' })}
{titulo}
{texto}
))}
)}
{t.dark ? Icon.sun({ s: 17 }) : Icon.moon({ s: 17 })}
{!partido && (
VITA
Tu asistente médico digital
)}
{/* segmented toggle */}
{[['login', 'Ingresar'], ['signup', 'Crear cuenta']].map(([m, label]) => ( ))}
{/* fields */}
{mode === 'signup' && ( setTouched(s => ({ ...s, name: true }))} /> )} setTouched(s => ({ ...s, email: true }))} /> setTouched(s => ({ ...s, pw: true }))} trailing={ } /> {mode === 'login' ? ( ) : ( )}
{/* bottom actions */}
{authError && (
{authError}
)} go('form')}> {submit === 'form' ? : (mode === 'login' ? <>Ingresar {Icon.chevron({ s: 17, c: t.onAccent })} : <>{Icon.faceid({ s: 19, c: t.onAccent })} Continuar al escaneo facial)} {mode === 'login' ? ( <>
o
) : (
{Icon.faceid({ s: 18, c: accent })} Para crear tu cuenta confirmarás tu identidad con un escaneo facial. Así solo tú accedes a tu historia.
)}
); } function Spinner({ c }) { return ( ); } Object.assign(window, { AuthScreen, HeartLogo, AuthField, Spinner });