country-picker.tsx · findCountryCodeByPhone01«Mi perfil» es una pantalla con dos tabs
(app)/account/profile/page.tsx · profile-tabs.tsx · patrón payments.tsx/(app)/profile — hoy un placeholder de MF-03 — toma el molde exacto de la pantalla de Pagos (MF-07): pantalla de stack fuera de (tabs), StackHeader con «Mi perfil» y su descripción, y los dos tabs de la web — Información personal / Direcciones — con el subrayado de marca: la línea --grad-marca bajo el tab activo, que se desliza al cambiar. Se entra desde la banda de identidad de Más, desde «Mis direcciones» (que abre ?tab=addresses) y desde cualquier deep link. Arriba del formulario, cuando el perfil no está completo, vive la card de completitud (§03).
El molde de Pagos, no uno nuevo
StackHeader de MF-04 (atrás con hit de 44, título display 24, descripción Poppins 14), cuerpo con
gap:12y los tabs con subrayado--grad-marca(variante nueva deTabsen mobile-ui, pedida por Fabrizio). La ley de componentización (DECISIONS #28) manda: cero pantallas artesanales — esta ruta solo compone.?tab=addresses, sincronizado de verdadLa web usa
defaultValuey nunca refleja el tab en la URL. La app usauseSyncedTabParam(el hook que MF-07 dejó enhooks/navigation/): cambiar de tab hacerouter.setParams, así que atrás, deep links y notificaciones aterrizan en el tab correcto. Valores inválidos caen apersonal, como la web.Los dos tabs de la web, con sus nombres
VALID_TABS = ['personal','addresses']y los labels deaccount.profile.tabs.*: «Información personal» y «Direcciones». Sin contador en los tabs: aquí no hay nada pendiente que contar — el pendiente lo cuenta la card de completitud.Sin avatar y sin empresa, a propósito
La web no tiene foto de perfil (el avatar de iniciales vive en el top bar y en Más, ya portados en MF-03) ni campo empresa en el
PATCH. El brief los asumía; el inventario dice que no existen. No se inventan superficies: paridad es paridad.Pares que caben en 390
Nombre|Apellido, Tipo|Cédula y Género|Fecha van de a dos por fila — el mismo
md:grid-cols-2de la web, aplicado donde el contenido lo permite. Correo y Teléfono van a lo ancho: uno lleva candado y el otro lleva el selector de país.
02El formulario, campo a campo
profile-form.tsx · schemas/user/profile-schema.ts · country-picker.tsx (MF-02)Los 7 campos del PATCH users/me — nombre, apellido, teléfono, tipo y número de documento, género, fecha de nacimiento — más el correo en solo-lectura (candado, fondo muted: se cambia por soporte, no por el form). Validación con los mensajes zod de la web verbatim; todos los campos son obligatorios, así que el botón guarda el perfil entero o no guarda nada. La única pieza que la web no tiene: el selector de país del teléfono, que el brief pide y el registro de MF-02 ya dejó portado (trigger de 104 px, 101 países, matching longest-dial-first).
profile-schema.ts, verbatimEl selector que la web no tiene (propuesta)
El brief pide teléfono con país; el
ProfileFormweb es un Input plano con placeholder+58 412 1234567. La app usa elcountry-pickerdel registro (MF-02): sheet con buscador, Venezuela primero y los corredores después, código entabular-nums. Al cargar,findCountryCodeByPhoneparte el número guardado en país + nacional.Mensajes verbatim, error por campo
Los textos de
profile-schema.tstal cual («La cédula debe tener al menos 6 caracteres», «Selecciona un género»…). El campo inválido pinta el bordedestructivecon su ring, el label acompaña, y el error va debajo con el icono — texto y color, nunca solo color.Guardar: spinner, campos quietos, toast al volver
PATCH users/mecon las 7 claves deUpdateUserData; pending = spinner en el botón y controles deshabilitados. La web no da ningún feedback de éxito (deuda anotada): la app dispara el toast Sileo de MF-01 «Cambios guardados» e invalida['profile','me'].PII con
ph-no-captureTodos los inputs de datos personales llevan la máscara de PostHog, como la web. La cédula V/E replica el enum real (
DNIType = 'V' | 'E') — el pasaporte no existe en la API (gap reportado, DECISIONS de MF-02); el hint del documento de MF-02 sigue valiendo: solo números, 6–10 dígitos.
03El % del perfil vive en el perfil
use-profile-completion.ts · user-context.tsx:44-91 · pasos de /complete-profile (MF-02)La web calcula el % (computeCompletion: 4 ítems de 25 % — correo verificado, teléfono verificado, documento, dirección) pero nunca lo muestra donde el usuario va a arreglarlo: solo en el banner global y en el gate modal, ya portados en MF-03. En la app, la card vive arriba del formulario: barra --grad-fresh, el % en display, y solo los ítems que faltan, cada uno tappable hacia su paso de /complete-profile — eso ES la re-verificación de correo y teléfono: se reusa el flujo de MF-02, no se duplica. Completo el último ítem, la card celebra y se despide: el único momento orquestado de esta MF.
Aquí, los 4 ítems completos
Esta maqueta muestra la card expandida — los 3 hechos en verde y el que falta en ámbar — que es como se ve al tocarla. Colapsada (§01) muestra solo título, barra y los pendientes: en un teléfono no hay sitio para celebrar lo ya hecho.
Cada fila lleva a su paso
«Verificar correo electrónico» y «Verificar teléfono» abren los pasos de
/complete-profilede MF-02 (OTP por correo, código por WhatsApp); «Agregar cédula» enfoca el campo del form; «Agregar dirección de envío» cambia al tab Direcciones. Nada nuevo que mantener: portar, no duplicar.0 · 25 · 50 · 75 · 100
El % solo puede valer múltiplos de 25 (
Math.round(completed/4*100)). La barra es--grad-fresh— el degradado aparece donde algo avanza, la regla de motion de la casa — sobre rielgray-100, radios concéntricos con la card.El final: llena, vira, se despide
Al completarse el último ítem: la barra 75 → 100 en 420 ms ease-out, la placa ámbar vira a verde y el check se dibuja en 260, «75 %» cede a «100 %» y el título a «Perfil completo». Háptica
success. Tras 1,2 s la card se pliega y no vuelve (isComplete). Reduced motion: estado final directo.Banner y gate no se tocan
El banner ámbar global y el gate modal ya existen en
apps/mobile(MF-03) con las mismas 4 etiquetas. Esta card no los reemplaza: es la vista de detalle cuando el usuario ya está en su perfil.
04Direcciones: el almacén primero, tus puertas después
addresses-form.tsx · warehouse-pickup-card.tsx · address-card.tsx · users/me/addressesParidad con el tab web, regla de dominio incluida: retirar en almacén = no tener dirección principal. La WarehousePickupCard va siempre primera; «Hacer principal» ahí llama PATCH primary/clear y apaga el flag de todas. Después, las direcciones en el orden de la API (principal primero). Cada card: alias en display editable inline, badges «Principal» y «Sin verificar» (migradas de v1), las líneas del AddressDisplayBlock, y las acciones. Sin límite de direcciones — la API no lo tiene.
Elige cómo recibes tu mercancía: la retiras en el almacén destino o te la entregamos en una de tus direcciones.
AgregarRetiras tu mercancía en el almacén destino de Mogos Group cuando llegue. No se despacha a domicilio.
Hacer principalElige cómo recibes tu mercancía: la retiras en el almacén destino o te la entregamos en una de tus direcciones.
Retiras tu mercancía en el almacén destino de Mogos Group cuando llegue. No se despacha a domicilio.
Opción principalSin direcciones de entrega
Retiras en el almacén destino. Agrega una dirección si prefieres que te la llevemos.
Agregar direcciónLa pickup card manda la regla
isPickupDefault = !addresses.some(a => a.isPrimary). Sin direcciones, el almacén es «Principal» y su botón queda «Opción principal» deshabilitado. Con una principal, ofrece «Hacer principal» →PATCH users/me/addresses/primary/clear(optimista: todas aisPrimary:false).Una columna, no un wrap
La web pone las cards en
flex-wrapconmax-w-sm; a 390 px eso ya ES una columna. La card hereda su anatomía completa: icono brand, badges a la derecha, alias display 22 extralight (Lemon Milk 200), líneas delAddressDisplayBlocken su orden — unidad, escrita (icono documento), geocodificada en gris, referencia (icono pin), Maps/Waze.«Sin verificar» con su tooltip
Exclusivo de migradas de v1: badge warning con
MapPinOff; el tap muestra «Dirección migrada — confirma tu ubicación para verificarla» (en RN, un tooltip de long-press no existe: va como hint bajo el badge al tocarlo) y «Editar» abre el mapa en modo verificación (§06).Maps y Waze abren el mapa del sistema
Linking.openURLcon las mismas URLs de la web (google.com/maps?q=lat,lng·waze.com/ul?ll=). Solo aparecen si hay coordenadas — una migrada en 0,0 no engaña a nadie.«Agregar» arriba, vacío que invita
Con direcciones, el botón sm vive junto a la descripción de la sección (la web lo pone en el header). Vacío: la pickup card +
Emptycon «Agregar dirección» — el mismo componente de mobile-ui de siempre.Los endpoints que faltan son la fase
El kit móvil de MF-02 solo trae GET y POST. Esta MF añade
PATCH :id,DELETE :id,set-primaryyprimary/clearalib/address/addresses.ts, con mutations optimistas comouse-manage-user-addressesweb.
05Alias inline, principal optimista, eliminar con freno
inline-alias-editor.tsx · use-manage-user-addresses.ts · AlertDialogTres micro-flujos que en la web ya están resueltos y aquí solo cambian de dedo. El lápiz junto al alias abre el editor inline (máx. 50, guarda en blur/Enter, Esc revierte, vacío no guarda); «Hacer principal» es optimista — la lista se reordena al instante y el badge salta de card — y la papelera frena con el AlertDialog de la web, palabra por palabra.
maxLength 50 · blur/Enter guarda · Esc revierteElige cómo recibes tu mercancía: la retiras en el almacén destino o te la entregamos en una de tus direcciones.
AgregarAlertDialog · DELETE users/me/addresses/:idElige cómo recibes tu mercancía: la retiras en el almacén destino o te la entregamos en una de tus direcciones.
AgregarEl editor inline, sin pantalla nueva
Input de 32 con ring brand, auto-seleccionado,
maxLength 50. Guarda en blur o Enter solo si cambió y no quedó vacío; Esc revierte; el error cae al toast «No se pudo actualizar el nombre. Intenta de nuevo.» y devuelve el foco al lápiz — el contrato exacto deinline-alias-editor.tsx.Principal: optimista y con reorden
setPrimaryreescribe la caché (isPrimary: id === tocada) y la lista se reordena con layout animation de 260 ms; el badge «Principal» aparece en la nueva y desaparece de la vieja sin esperar red. Rollback si el PATCH falla. Hápticalight.Eliminar: el mismo freno de la web
Título, cuerpo y botones verbatim. En el teléfono el destructivo va arriba y a lo ancho (pulgar), «Cancelar» ghost debajo. Al confirmar, la card se pliega en 260 ms y las demás suben; si era la principal, el almacén vuelve a ser «Principal» solo — es la regla de dominio, no un efecto.
Háptica y foco
warningal abrir el dialog de eliminar,successal completarse. El foco vuelve al botón «Agregar» si la lista quedó vacía — nunca a un elemento que ya no existe.
06El mapa de MF-02, en modo edición
components/auth/complete-profile/address/* · address-form-dialog.tsx · DECISIONS #19Crear y editar no estrenan pantalla: abren la captura fullscreen de MF-02 — mapa a sangre, pin fijo al centro, hoja inferior en tres alturas, campos por tipo de lugar — con un back flotante porque ahora se llega desde el perfil. Editar siembra el pin en las coordenadas guardadas y precarga todos los campos. Las migradas de v1 (needsVerification o lat/lng en 0,0) no siembran nada: la franja ámbar lo dice y «Guardar» queda bloqueado hasta que haya un punto real. Sin mapa no se guarda — la regla de la web, intacta.
Editar dirección
Ubicación pendiente
Marca un punto en el mapa para detectar tu dirección.
Guardar direcciónEl kit entero se reusa, con contexto nuevo
AddressCapture, su provider y sus 21 archivos quedan como están; MF-09 les pasainitialAddress(edición) y el back flotante. Los textos vienen de la copia web deladdress-form-dialog: crear «Marca tu ubicación exacta…», editar «Ajusta el marcador y los detalles…», verificar la frase con la referencia de la migrada.Migradas: el pin no se regala
isMigratedAddress = needsVerification || (lat===0 && lng===0)— sembrar el pin en 0,0 pondría a la gente en el golfo de Guinea. La franja ámbar explica, el buscador y «Usar mi ubicación» ayudan, y al guardar con punto real la dirección queda verificada: el badge desaparece de la card.Nueva = principal, como la web
defaultPrimary = !isEditing: toda dirección nueva se guarda principal (y el almacén deja de serlo). Editar no toca el flag. ElcreateAddressBodydel kit hoy fuerzaisPrimary:true— pasa a ser condicional en esta MF.Sin mapa no se guarda
Si Google Maps no carga: «No se pudo cargar Google Maps. No es posible guardar una dirección sin mapa.» + Reintentar; permiso denegado → atajo a los ajustes del sistema.
canSave = isReady && !mapsLoadError, el gating literal de la web.
07Sin pantalla de ajustes: Más cierra lo que falta
decisión Fabrizio 2026-09-20 · /account/mas (MF-03) · fichas App Store / PlayLa web no tiene /account/settings — y la app tampoco. Paridad estricta: el perfil son sus dos tabs y nada más; idioma y cerrar sesión ya viven en Más (MF-03) y no se duplican. Lo único app-only que entra — mínimo, y al fondo de Más, debajo de Cerrar sesión — son las dos filas de Legal (las fichas de App Store y Play piden términos y privacidad a mano) y la versión como pie de página, no como fila. Novedades, soporte, eliminar cuenta y cualquier otra idea de «ajustes» quedan fuera de esta MF — eliminar cuenta va anotada como deuda para MF-15 (App Store 5.1.1(v) la exigirá al publicar).
Cuenta
Accesos
Preferencias
Legal
Mogos · Versión 1.0.0
Más scrolleada: la banda de identidad ya pasó
La maqueta muestra Más con el scroll al fondo — la banda de identidad y el tope de «Cuenta» quedaron arriba. Todo lo que existe en MF-03 sigue intacto: cero rediseño de Más; solo crece el fondo.
Legal: dos filas, salen de la app
«Términos y Condiciones» y «Políticas de Privacidad» abren el navegador (
SITE_URL+ ruta cuando exista — la web autenticada hoy no tiene esos enlaces: el registro los pinta sinhref, deuda anotada). Icono de enlace externo, no chevron de navegación.La versión es un pie, no una fila
APP_VERSION(portado delib/version.ts) en gris 11 centrado bajo el último grupo — el DoD del brief pide «ver la versión» y esto la da sin inventar una pantalla. En dev, un long-press puede copiar build + runtime para soporte.Lo que quedó fuera, y por qué
Novedades/changelog (vive en el sidebar web — sin equivalente móvil), soporte WhatsApp/correo (la web solo lo tiene en el dropdown de ayuda), notificaciones (MF-10), eliminar cuenta (deuda para MF-15: App Store 5.1.1(v) la exigirá al publicar, y no hay endpoint ni flujo web hoy) y cualquier pantalla «Ajustes». Paridad estricta: si la web lo gana, la app lo gana en su MF siguiente.
08Estados: esqueleto anatómico, error que invita
profile-form-skeleton.tsx · address-card-skeleton.tsx · Empty (mobile-ui)El skeleton copia la anatomía real — tabs, card de completitud, pares de campos, botón — para que la carga no «salte» al llegar los datos. El error de direcciones usa Empty variant="error" con Reintentar; el formulario con error de red conserva lo escrito (nunca se borra un form por un timeout). Offline: la ConnectionAlert global de MF-03 + caché de queries.
ProfileFormSkeleton, anatomía 1:1Empty variant="error" + ReintentarNo pudimos cargar tus direcciones
Revisa tu conexión e intenta de nuevo.
Reintentar09Lo que hay que escribir en Moti
Moti + Reanimated · escala de MF-01 · useReducedMotion| Pieza | Qué anima | Duración · easing | Reduced motion |
|---|---|---|---|
| Tabs Personal / Direcciones | la línea --grad-marca se desliza al tab activo; el contenido entra con fundido + 12 px | 260 ms · ease-out | swap directo |
| Completitud · 75 → 100 | barra llena con --grad-fresh; placa ámbar vira a verde; check se dibuja; % y título cruzan en fade; la card se pliega tras 1,2 s | 420 · 260 · 160 ms | estado final directo |
| Hacer principal | layout animation: la card sube a su nueva posición; el badge entra con pop | 260 ms · ease-in-out | reorden directo |
| Eliminar dirección | la card se pliega en alto + fundido; las de abajo suben | 260 ms · ease-in-out | desaparece |
| Alias inline | display ↔ input con fundido corto; el ring entra con el foco | 160 ms · ease-out | swap directo |
| Hoja del mapa (MF-02) | tres alturas con arrastre; abre 420, cierra 260 | 420 / 260 ms | fundido 80 ms |
| Dialog de eliminar dirección | velo marino 70 % + scale desde .96 | 260 ms · ease-out | fundido 80 ms |
| Toast «Cambios guardados» | Sileo de MF-01: baja, espera, sube | 260 ms · ease-out | swap directo |
El único momento orquestado es el 75 → 100 de la completitud (§03, botón «Repetir»). El degradado aparece solo donde algo avanza (la barra). Nada pulsa ni supera los 640 ms. Háptica: success al completar el perfil y al guardar, light al hacer principal, warning al abrir eliminar dirección.
10Lo cerrado y lo que decide Fabrizio
docs/specs/2026-09-21-mobile-perfil.md · HOLD hasta OK- Cerrada · Fabrizio
El molde es Pagos; los tabs, subrayados
Pantalla de stack + StackHeader +
useSyncedTabParam, con los tabs en la variante de subrayado--grad-marca(nueva en mobile-ui, reusable). Cero componentes nuevos donde ya hay stack compartido; hooks enhooks/, schemas enschemas/, la ruta solo compone. - Cerrada · paridad
El formulario es el de la web
7 campos obligatorios, mensajes zod verbatim, correo readonly, V/E sin pasaporte (gap de la API, reportado), fecha
dd 'de' MMMM 'de' yyyy, PII conph-no-capture. Sin avatar-foto y sin empresa: no existen en la web. - Propuesta · 1
Teléfono con selector de país
El brief lo pide; la web no lo tiene (Input plano). Se reusa el
country-pickerde MF-02 (101 países, +58 default, longest-dial-first). Si Fabrizio prefiere paridad estricta, se cae a Input plano sin rediseño. - Propuesta · 2
El % de completitud en el perfil
Card sobre el formulario con barra
--grad-freshy solo los ítems pendientes, cada uno hacia su paso de MF-02 — eso es también la re-verificación de correo/teléfono. La web solo tiene banner + gate (ya portados, intactos). - Cerrada · dominio
El almacén es la ausencia de principal
WarehousePickupCardprimera siempre; «Hacer principal» ahí =primary/clear. Dirección nueva = principal (defaultPrimary = !isEditing); elisPrimary:truehardcodeado del kit pasa a condicional. - Cerrada · reuso
El mapa es el de MF-02
Captura fullscreen con back flotante para crear, editar y verificar; migradas sin pin sembrado y con franja ámbar; sin mapa no se guarda. La fase añade PATCH/DELETE/set-primary/clear al kit.
- Cerrada · Fabrizio 2026-09-20
Sin pantalla de ajustes: paridad estricta
El perfil son sus dos tabs y nada más; idioma y cerrar sesión viven solo en Más (MF-03). Lo único app-only, al fondo de Más: Legal (Términos + Privacidad) y la versión como pie. Novedades, soporte y notificaciones quedan fuera hasta que la web (o MF-10) los tenga. Eliminar cuenta sale de v1: queda como deuda de MF-15 (App Store 5.1.1(v) — necesitará decisión de flujo y probablemente endpoint).
- Deuda anotada
Lo que la web debe y la app no repite
Sin i18n en perfil/direcciones (≈20 strings hardcodeados — la app crea las claves es/en/zh);
PATCH users/mesin feedback de éxito; «Términos» del registro sin<a href>; «Ver todo» del changelog muerto;/account/masweb alterna solo es↔zh;ProfileSnapshotmóvil singender/birthdate; enum DNI sin pasaporte. - Testing
DoD en iPhone = Fabrizio
Editar nombre y teléfono, agregar dirección desde el mapa, marcarla principal, editar el alias, borrar otra, ver el % subir, cambiar idioma, ver la versión y cerrar sesión. C9 automática:
expo export --platform web+ visual-qa 390×844 contra estas maquetas.