MF-07 · pagos · sep 2026

Mobile · pagos

Pagar desde el teléfono, con el membrete de siempre.

Lo que el cliente debe, lo que ya pagó, y la puerta para pagar. Pendientes e historial son pantallas nativas del producto (Contexto B), calmas y con las cifras al frente. El portal de pagos ya tiene su propio sistema (Contexto A: membrete marino sobre papel) y la app no lo re-viste ni lo esconde: la cabecera noche del sheet es el membrete del portal, dibujado nativo, y debajo corre el portal en modo embed dentro de una WebView (DECISIONS #15). Carga, sin conexión, error y éxito se pintan en el mismo papel, así que nunca hay un salto de sistema a mitad del pago.

Pantalla
/payments · 2 tabs
Portal
WebView · embed
Cabecera
membrete nativo
Estados
carga · offline · error · 3DS
Vuelta
éxito nativo + badges
Pantallas
23 a 390×844

01Pendientes: el monto que falta, al frente

pending-payments-list.tsx · payments/me/pending

La pantalla /payments ya existe como placeholder de MF-03 y se abre desde Más, desde «Ver todos» del home y desde las notificaciones. Es una pantalla de stack (fuera de (tabs)) con el StackHeader de MF-04, y lleva los dos tabs de la web como Tabs pill a lo ancho. La card es la de la página web, campo a campo, en una columna: tipo de cobro, ruta, código, cotización, estatus, «Total | Pendiente», vencimiento y «Pagar ahora».

9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
Pendiente total
$1.930,00
3 pagos1 vencido
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
FleteDesde USA
MOGOS-F1087
QUO-20260812-0031
Vencido
Total
$380,00
Pendiente
$380,00
Vencido · 3 días
Pagar ahora
Cotización
COT-0952
QUO-20260910-0007
Pendiente
Total
$300,00
Pendiente
$300,00
Pagar ahora
  1. La banda «Pendiente total»

    payments/me/pending ya devuelve summary.totalPending y count, y la web no los pinta. En el teléfono, donde la lista no cabe entera, la banda responde «¿cuánto debo?» antes del primer scroll, y baja en cuanto un pago se confirma. No hace falta tocar el backend. El chip «1 vencido» se calcula en el cliente a partir de los ítems.

  2. La card, campo a campo

    Badge neutro con el icono de tipo (payment-entity-types.ts), CorridorBadge solo para fletes (almacén, cotizaciones y ventas no traen ruta), entityCode en display 18, quotationNumber en muted, y a la derecha el estatus. El bloque «Total | Pendiente» va en un plate gris de radio 10, concéntrico con la card de 14.

  3. Un solo mapa de estatus

    La web tiene dos: la página pinta Pendiente en ámbar y Parcial en azul, y el widget del home, al revés. La app usa el del widget, que ya adoptaron MF-03 y MF-06: Vencido en rojo, Parcial en ámbar y Pendiente en brand. El mismo pago no puede cambiar de color entre el home y Pagos.

  4. Vencimiento con texto, no solo color

    «Vence 22 sept 2026» con reloj, o «Vencido · 3 días» en rojo. El estado se lee en palabras y además en color. Sin dueDate, la fila no se muestra (nunca «Sin fecha»).

  5. «Pagar ahora» abre el sheet

    Mientras POST payment-gateway/links responde, el botón muestra el spinner con «Procesando...» y los demás botones quedan deshabilitados. Así un segundo toque no crea un segundo enlace. Si el POST falla, sale un toast de error y no se abre ningún sheet.

  6. Pull-to-refresh y la ruta del shell

    RefreshControl invalida ['payments','pending'] y ['users','sidebar-counts']. El chip de ruta del top bar no filtra esta lista: la web tampoco lo hace, y un pago de almacén no tiene corredor.

02Historial: por mes, plegable, con su comprobante

payment-history-list.tsx · payment-history-row.tsx · payments/me/history

Paridad con la web: grupos por mes con «N pagos · total» a la derecha y filas que se despliegan. Cambian tres cosas porque el soporte es un teléfono: el scroll infinito reemplaza el botón «Cargar más» (FlashList con onEndReached, página de 20), el comprobante se abre en el PdfViewer de MF-04 con compartir nativo, y el enlace a la entidad dice qué abre según su tipo.

9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
Septiembre 20263 pagos · $2.370,00
MOGOS-F0998
3 sept · Transferencia bancaria · 00412398
$450,00USD
RutaDesde USA
MétodoTransferencia bancaria
Número de comprobanteREC-2026-0398
CotizaciónQUO-20260821-0044
EquivalenteBs 163.800,00
Tasa 364,00 Bs/$ · 3 sept 2026
Ver fleteComprobante
Agosto 20262 pagos · $980,00
Cargando historial...
  1. La fila cerrada

    Plate de 36 con el icono y el tono del tipo, el código sin truncar (la ruta pasa al detalle: a 390 px el badge se comía el código) y una sublínea «fecha · método · referencia (o número de comprobante)». A la derecha, el monto tabular con la moneda en versalitas. La fila completa es el área táctil de 64 px y el chevron indica el estado.

  2. Tonos del sistema, no de Tailwind

    La web pinta los tipos con purple y sky, que están fuera de la escala. La app usa flete en brand, almacén en ámbar (esa escala sí existe en el producto), cotización en lima-soft con marino y venta en celeste-soft con marino.

  3. Desplegada: los datos y dos acciones

    Método, número de comprobante, cotización y notas (si vienen). Las acciones son «Ver flete» o «Ver cotización» según el tipo (la web dice «Ver envío» para todo), que se omite en almacén y ventas porque la app no tiene esas rutas, y «Comprobante», deshabilitado con «Comprobante aún no disponible» si falta receiptPdfUrl.

  4. El equivalente en Bs

    La API ya manda equivalentAmount, equivalentRate y equivalentRateCapturedAt para los métodos en bolívares, y el esquema Zod de la web los descarta. En Venezuela esa línea responde «¿cuánto salió de mi cuenta?». Va en un plate gris dentro de la fila desplegada y solo aparece cuando los datos vienen.

  5. Agrupar por fecha de pago

    La API ordena por paidAt y la web agrupa por createdAt, porque su esquema descarta paidAt. Un pago creado el 31 y verificado el 1 puede caer en el mes equivocado. La app agrupa por paidAt y usa createdAt como respaldo.

  6. Lo que no aparece

    El historial solo lista pagos verificados y no anulados. Un Zelle en verificación no aparece en el historial y su pendiente sigue igual, y por eso la pantalla «Estamos verificando tu Zelle» (§08) lo dice antes de cerrar.

03Estados de las listas

Empty · Skeleton · connection-pill.tsx (MF-03)

Se copian los seis estados de la web. Los skeletons son anatómicos: repiten la forma de la card y de la fila, no barras genéricas. Un error nunca tira una lista que ya cargó: si falla la página siguiente del historial, aparece una línea roja al pie con reintento. Sin conexión, las listas muestran lo último que cargaron y el aviso es el ConnectionAlert global de MF-03, que ya cubre todo el stack (app). No hay un banner propio de pagos.

Pendientes · carga resumen + 2 cards
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes Historial
Pendientes · vacío invita al historial
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes Historial

No tienes pagos pendientes

Cuando tengas pagos pendientes aparecerán aquí.

Ver historial
Pendientes · error Empty error + reintento
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes Historial

Error al cargar pagos

No pudimos cargar tus pagos pendientes. Por favor intenta de nuevo.

Reintentar
Historial · carga 2 meses × 3 filas
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
Historial · vacío promete el comprobante
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial

Aún no tienes pagos registrados

Cuando realices un pago aparecerá aquí con su comprobante.

Historial · error de página la lista se queda
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
Septiembre 20263 pagos · $2.370,00

Mientras carga, el tab «Pendientes» no muestra contador: nunca un 0 falso (la misma regla de los chips de MF-06). En el vacío de pendientes, «Ver historial» es la siguiente acción natural. Mientras se refresca, la lista queda al 60 % sin skeleton (isPlaceholderData), como en la web.

04Por dónde se entra y el badge

pending-payments.tsx (MF-03) · freight-payment-alert.tsx (MF-06) · (tabs)/mas.tsx

Tres puertas, y ninguna obliga a pasar por la lista. En la web, «Pagar» del home y «Pagar ahora» de la alerta del flete abren el portal directamente. Hoy la app navega a /payments (home) o muestra el toast «Funcionalidad de pago próximamente» (alerta). MF-07 conecta las dos al mismo PaymentPortalSheet. El contador vive en la fila Pagos de Más y en el punto rojo del tab, ambos alimentados por sidebar-counts.pendingPayments.

Home «Pagar» crea el enlace sin salir
9:41●●●
Todas las rutas
3FS
Pagos pendientes
Ver todos
$1.250,00Desde China
Procesando...
Fecha límite22 sept 2026
CódigoQUO-20260828-0012
ReferenciaMOGOS-F1042
EstatusParcial
$380,00Desde USA
Pagar
Fecha límite13 sept 2026
EstatusVencido
Detalle del flete la alerta de MF-06 ya paga
9:41●●●
MOGOS-F1042
Marítimo · Yiwu → Caracas
InformaciónEnvíosSeguimiento
Resumen
En tránsito
OrigenYiwu
DestinoCaracas
Salida04/09/2026
Llegada estimada18/10/2026
Más fila Pagos con contador + punto en el tab
9:41●●●
Todas las rutas
3FS
FS
Fabrizio Spiotta
fabrizio@cresco.so

Cuenta

Pagos3
Cotizaciones3
Mis direcciones

La alerta del flete paga pendingAmount neto: los fees de servicio viajan con su pago, no con el flete (lib/freights/service-fees.ts, portado en MF-06). Por eso «Fees de servicio: $21,70» corresponde al pago anterior con tarjeta, y el fee de este pago lo calcula el portal cuando se elige el método (§06).

05El portal: ¿Contexto A o shell móvil?

DECISIONS #15 · apps/payments-portal · proposal/mogos-pagos.html

El enfoque ya está decidido (WebView en v1). Queda la forma. El portal habla otro sistema: marino y papel, Neue Haas, cifras tabulares, el filo de --grad-marca. La app habla el producto. Hay que decidir quién pone la cabecera y qué se ve mientras la web no está lista. Se evaluaron tres formas:

A · sheet del producto

Cabecera blanca + portal completo

Es la traducción literal del sheet web («Pago seguro · Realizar pago»). Si carga el portal completo, suma su propio membrete debajo y quedan dos cabeceras. Si carga el embed, el cuerpo marino/papel cuelga de una barra blanca que no le pertenece.

Descartada: dos sistemas apilados
B · membrete nativo · elegida

El membrete es nativo, el cuerpo es el portal

La cabecera noche del sheet (la de MF-03) se dibuja con el membrete del portal: --grad-hero, wordmark vectorial, «Pago seguro» y el filo de marca. Debajo, el portal en embed=true, que ya existe sin cabecera. Carga, error y éxito se pintan en el mismo papel.

Elegida: un solo objeto de principio a fin
C · navegador del sistema

expo-web-browser con el portal completo

Es la opción más barata y la que más confianza da a Apple, pero no tiene puente: no llega onMessage, la vuelta depende de un deep link y no hay actualización inmediata de pendientes y badges, que es justo el DoD. Queda como salida de emergencia («Abrir en el navegador», §07).

Solo como fallback

Cerrado por Fabrizio: B. La marca ya separa las dos cosas: Contexto A es para el dinero y Contexto B para operar. Poner el membrete nativo le dice al cliente «aquí se paga» antes de que cargue un byte, y hace que la espera tenga la forma del portal y no la de un sheet genérico. Además resuelve el único punto frágil de la WebView, porque la app, que conoce su origen, puede mostrar en el membrete a qué host está conectada (§07). La cabecera noche ya es un patrón de MF-01/MF-03 (sheet de crear, Próximamente), así que no se introduce nada nuevo en el shell.

06PaymentPortalSheet: membrete de noche, papel de día

payment-portal-sheet.tsx · payment-portal-embed.tsx · react-native-webview

Se presenta como page sheet a pantalla casi completa, sobre la pantalla desde la que se abrió, que queda con velo marino. No se cierra arrastrando: el gesto choca con el scroll de la WebView y un deslizamiento accidental perdería un formulario de Stripe a medio llenar. Se cierra con la ✕ del membrete o con el botón atrás de Android. Debajo del membrete corre el portal real en embed, la misma pasarela que la web: resumen, montos rápidos, los cuatro métodos (Stripe, PayPal, Binance Pay y Zelle) y el desglose del fee de servicio. No hay SDK de pago nativo.

9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
  1. El membrete, pieza por pieza

    Primera fila: wordmark vectorial blanco con el punto rojo, «Pago seguro» con escudo celeste y la ✕ de 44. Segunda fila, separada por una línea fina: Referencia (entityCode) y Conexión con candado y el host de EXPO_PUBLIC_PAYMENTS_PORTAL_URL. Abajo cierra el filo de 3 px de --grad-marca.

  2. El filo mide la carga

    La regla del portal dice que el degradado marca movimiento. Aquí el filo es la barra de progreso: crece con onLoadProgress y se completa en PAYMENT_READY (no en onLoad, que llega antes de validar el token).

  3. El desglose es del portal, no de la app

    «Abono a tu flete», «Fee de servicio Mogos» (solo el monto, nunca el porcentaje) y «Total a cobrar» (o «Total a transferir» en Zelle) los calcula y los pinta el portal, y el servidor los recalcula. La app no duplica esa lógica: las listas muestran montos netos y el fee aparece cuando se elige el método.

  4. Idioma y tema

    El enlace se abre con ?embed=true&lang=es|en|zh desde i18next. El idioma solo se cambia en Más, así que con el sheet abierto no puede cambiar y SET_LANG no hace falta en v1. El portal es light-only, igual que la nota de entrega de MF-06: la pantalla es la del cobro.

La ortografía del portal tiene deuda visible («Resumen del Pago», «Opciones Rapidas», «Monto Especifico», «Descripcion», «encriptacion»). La maqueta ya muestra la versión corregida en sentence case, y el arreglo va como fase chica en apps/payments-portal con spanish-i18n-review.

07Estados del sheet: nativos y en papel

NetInfo · onError · onHttpError · PAYMENT_ERROR · onShouldStartLoadWithRequest

Todo lo que no es el portal lo pinta la app, con las piezas del portal: papel --hueso, placa circular, título en display, CTA marino con filo y botón fantasma. El membrete no cambia, salvo la fila de Conexión, que dice la verdad: host seguro, «Sin señal» o el host del banco. El «no se cobró nada» solo se dice cuando es cierto (antes de PAYMENT_READY o en un error de carga).

Cargando filo al 38 % + skeleton del portal
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Sin conexión NetInfo offline al abrir o durante
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Error de carga onError · 5xx · 20 s
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Enlace inválido PAYMENT_ERROR del token
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Verificación del banco 3DS en hooks.stripe.com
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Salir ✕ o atrás después de READY
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
¿Salir del pago?
Si todavía no confirmaste, no se cobró nada. Puedes volver a pagar desde Pagos.
Seguir pagandoSalir
  1. El skeleton tiene la forma del portal

    Resumen con borde marino, barra, bloque de monto, cuatro montos rápidos y CTA, en --line-soft sobre papel. Cuando llega PAYMENT_READY, la WebView (montada con opacidad 0 desde el principio) hace fundido sobre el skeleton. No hay flash blanco.

  2. Sin conexión no destruye nada

    Si se cae la red con el portal ya cargado, la WebView se queda como está (Stripe reintenta solo) y solo la fila Conexión pasa a «Sin señal» con punto rojo. El estado de pantalla completa aparece únicamente si no hay nada que mostrar. «Reintentar» recarga la misma URL, porque el enlace dura 24 h.

  3. Error con salida de emergencia

    «Abrir en el navegador» usa expo-web-browser con la URL sin embed. Es la opción C como fallback, equivalente al «Abrir en pestaña nueva» de la web. Al volver (AppState activo) se invalidan igual pendientes, historial y contadores.

  4. Enlace vencido: uno nuevo, no un callejón

    PAYMENT_ERROR por token inválido lleva a este estado. «Generar un enlace nuevo» repite el POST payment-gateway/links con el mismo ítem y recarga. Los errores de tarjeta (rechazada, sin saldo) los muestra el portal dentro de su formulario, y la app no los tapa.

  5. El membrete dice dónde estás

    La verificación 3DS de Stripe navega fuera del portal. La fila pasa a «Tu banco · hooks.stripe.com» con el icono de banco en celeste. Es la única señal de host que tiene el cliente dentro de una WebView, y por eso solo aparecen hosts de la lista permitida (§09).

  6. Salir con confirmación, solo cuando importa

    Antes de PAYMENT_READY, la ✕ cierra directamente. Después pide confirmación con «Seguir pagando» como acción principal. El texto no promete lo que la app no sabe: si el cliente ya envió un Zelle, se refleja cuando se verifique.

08La vuelta: éxito nativo, Zelle honesto

PAYMENT_SUCCESS · status · queryClient.invalidateQueries

En modo embed, el portal no redirige al terminar: avisa con PAYMENT_SUCCESS. La app, en ese instante, invalida primero (['payments','pending'], ['payments','history'], ['users','sidebar-counts'] y, si es un flete, ['freights', id]) y después reemplaza el cuerpo del sheet por una pantalla final nativa en papel. Hay dos finales según el status del payload: pagado (tarjeta, PayPal, Binance) o en verificación (Zelle, status: "pending_verification"). Las dos se animan al entrar en vista.

Éxito «Recibimos tu pago» · animada
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Al cerrar la card pagada se va, las cifras bajan
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 2Historial
Pendiente total
$680,00
2 pagos1 vencido
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
FleteDesde USA
MOGOS-F1087
QUO-20260812-0031
Vencido
Total
$380,00
Pendiente
$380,00
Vencido · 3 días
Pagar ahora
Cotización
COT-0952
QUO-20260910-0007
Pendiente
Total
$300,00
Pendiente
$300,00
Pagar ahora
Zelle · en el portal copiar datos y «Ya envié el pago»
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Zelle · verificando «Estamos verificando tu Zelle» · animada
9:41●●●
Pagos
Gestiona tus pagos pendientes y consulta tu historial.
Pendientes 3Historial
FleteDesde China
MOGOS-F1042
QUO-20260828-0012
Parcial
Total
$1.870,00
Pendiente
$1.250,00
Vence 22 sept 2026
Pagar ahora
Con reduced motion, las dos aparecen ya en su estado final.
  1. El único momento con brillo

    El membrete toma --grad-celeste-glow con un fundido de 420 ms, el escudo pasa a doble check con «Pago recibido», la placa --grad-fresh entra con pop ease-spring de 420 ms, el check se dibuja (260 ms) y los textos suben 12 px con stagger de 40. «Ver el comprobante» aparece solo si viene receiptUrl. No hay toast: la pantalla ya es la confirmación.

  2. La card se va, el resto se acomoda

    Al tocar «Listo», la card pagada se pliega y se desvanece (260 ms) y las demás suben. La banda «Pendiente total», el contador del tab, la fila de Más, el punto del tab y el home ya están al día por la invalidación. Si fue un abono parcial, la card se queda con el nuevo pendiente.

  3. Zelle: enviar no es recibir

    «Ya envié el pago» llega con status: "pending_verification". La app no celebra: muestra «Estamos verificando tu Zelle» con el monto enviado y el memo, y la manecilla del reloj da una sola vuelta (640 ms, sin repetir ni pulsar). Membrete «Pago enviado», sin brillo.

  4. El pendiente no desaparece hasta que ops verifique

    Con Zelle igual se invalida (el portal pudo registrar algo), pero el pendiente sigue en la lista, el badge y el home hasta que operaciones verifique el pago y la API cambie el estatus de la cotización. El texto de la pantalla lo dice antes de cerrar, así la lista sin cambios no parece un error.

09El puente: un contrato, dos cables

lib/embed/messaging.ts · use-embed-mode.ts · payment-portal-sheet.tsx

Misma pasarela que la web: apps/payments-portal embebido, con sus cuatro métodos (Stripe, PayPal, Binance Pay y Zelle) y cero SDK nativo. Hay un solo contrato de mensajes y dos cables para transportarlo: el iframe de la web y la WebView de la app. El portal elige el cable según dónde corre. La app no inyecta JavaScript para parchar el portal y no usa polling como señal de éxito.

Contrato únicoForma
Mensaje{ source: "mogos-payments", type, payload }
typePAYMENT_READY · PAYMENT_SUCCESS · PAYMENT_ERROR
PAYMENT_SUCCESS.payloadpaymentId, amount, currency, receiptUrl? y status?: "pending_verification" (Zelle; si no viene, el pago está hecho)
Portalarma el mensaje una vez
→
Cable web · iframewindow.parent.postMessage(msg, origen permitido) — nunca "*"
Cable app · WebViewwindow.ReactNativeWebView.postMessage(JSON.stringify(msg)) si existe
→
App · onMessageJSON.parse · source · origen de la URL
MomentoCable web (iframe)Cable app (WebView)
Emitirwindow.parent.postMessage(msg, origin) solo a orígenes de la allowlist, como hoywindow.ReactNativeWebView.postMessage(JSON.stringify(msg)) cuando ese objeto existe
Validarevent.origin === PORTAL_ORIGIN y sourceJSON.parse(nativeEvent.data); source === "mogos-payments" y new URL(nativeEvent.url).origin === origen de EXPO_PUBLIC_PAYMENTS_PORTAL_URL; si no, se descarta
PAYMENT_READYsin cambiosskeleton → portal, filo al 100 %, activa la confirmación de salida
PAYMENT_SUCCESSinvalida + cierrainvalida (incluido sidebar-counts) → «Recibimos tu pago», o «Estamos verificando tu Zelle» si status: "pending_verification"
PAYMENT_ERRORtoast con el mensajetoken → «Enlace inválido»; los errores de tarjeta los muestra el portal
Idioma?lang + SET_LANG?lang al abrir; el sheet es modal y el idioma no cambia con él abierto
Navegaciónsandbox del iframeonShouldStartLoadWithRequest: portal, *.stripe.com (3DS), paypal.com; Binance Pay → Linking; el resto se bloquea y se reporta a Sentry
PayPal pierde el embedreturn_url → PORTAL/successfallback de navegación: /success se trata como éxito; /error recarga el enlace en embed
Señal de éxitoel mensajeel mensaje (o el fallback de PayPal). Refetch de payments/me/pending solo como red de seguridad al cerrar o volver a primer plano, nunca como señal

Fase chica obligatoria en el portal. Hoy apps/payments-portal/lib/embed/messaging.ts sale antes de postear si window.parent === window, que es justo el caso de la WebView, así que la app nunca recibiría nada. Hay que emitir también por ReactNativeWebView cuando exista (sin tocar el cable web ni su allowlist) y agregar status: "pending_verification" al éxito de Zelle. Sin endpoints ni migraciones. Plataforma: es pago de un servicio logístico físico, no IAP; react-native-webview es dependencia nativa nueva y requiere dev build por el método warehouse (sin eas build).

10Lo que hay que escribir en Moti

Moti + Reanimated · escala de MF-01 · useReducedMotion
PiezaQué animaDuración · easingReduced motion
Tabs Pendientes / Historialel fondo blanco del trigger se desliza; el contenido entra con fundido + 12 px260 ms · ease-outswap directo
Fila de historialalto 0 → auto y el chevron gira 180°260 ms · ease-outabre ya abierta
Sheet del portalpage sheet nativo sube con velo marino al 70 %420 ms · ease-in-outfundido 80 ms
El filo cargawidth del filo sigue onLoadProgress; se completa en READY160 ms por paso · ease-outfilo completo, solo skeleton
Skeleton → portalfundido cruzado skeleton / WebView260 ms · ease-outswap directo
Éxito · «Recibimos tu pago»brillo del membrete (fundido); placa con pop; el check se dibuja a los 420 ms; textos suben 12 px con stagger 40420 ms · ease-spring (placa) · 260 ms (check)estado final directo
Zelle · verificandoplaca con pop; la manecilla del reloj da una vuelta; textos suben con stagger 40; sin brillo420 ms · ease-spring · 640 ms (manecilla)estado final directo
Card pagada salese pliega en alto + fundido; las cards de abajo suben (layout animation)260 ms · ease-in-outdesaparece
Contadoresel número del tab, de Más y de la banda cambia con fundido vertical de 6 px160 ms · ease-outswap directo

Los dos finales son los únicos momentos orquestados y los dos se pueden ver en el HTML (§08, botones «Repetir»). El degradado solo aparece donde algo avanza (el filo) o se completa (la placa de éxito). Nada pulsa ni supera los 640 ms. Háptica: success en el éxito, light en Zelle, warning al abrir la confirmación de salida.

11Lo que quedó cerrado

docs/specs/2026-09-16-mobile-pagos.md · cerrado por Fabrizio 2026-09-16
  • Cerrada · 1

    Opción B en todas las bifurcaciones

    Membrete nativo (Contexto A) y portal embed=true en el cuerpo. Los estados no-portal se pintan en papel. Sin arrastre para cerrar; ✕/atrás con confirmación después de READY. Ante cada bifurcación de forma gana la opción recomendada.

  • Cerrada · 2

    Finales animados

    «Recibimos tu pago» y «Estamos verificando tu Zelle» con el motion de la spec: placa 420 ms ease-spring, check 260, manecilla 640 en una sola vuelta, stagger 40. Reduced motion lleva al estado final.

  • Cerrada · 3

    Misma pasarela que la web

    apps/payments-portal embebido con Stripe, PayPal, Binance Pay y Zelle. Cero SDK nativo (ni Stripe RN, ni PayPal nativo, ni Binance).

  • Cerrada · 4

    Un contrato, dos cables

    { source, type: READY | SUCCESS | ERROR, payload } con status: "pending_verification" para Zelle. Web por window.parent.postMessage con allowlist (nunca "*"); app por ReactNativeWebView.postMessage, validando source y el origen de la URL. Sin JS inyectado; el polling es solo red de seguridad; PayPal usa el fallback /success y /error. Fase obligatoria en messaging.ts.

  • Cerrada · 5a

    Banda «Pendiente total»

    summary.totalPending + count + chip de vencidos, arriba de las cards. Se actualiza con la invalidación del éxito.

  • Cerrada · 5b

    Equivalente en Bs en el historial

    Línea «Equivalente · tasa · fecha» con equivalentAmount, equivalentRate y equivalentRateCapturedAt, en la fila desplegada y solo cuando vienen (métodos en bolívares). El esquema de la app no los descarta.

  • Cerrada · 5c

    Zelle «en verificación»

    UI nativa con monto y memo. El pendiente no desaparece de lista, badge ni home hasta que ops verifique.

  • Cerrada · 6

    DoD en iPhone lo hace Fabrizio, en staging

    Con el PR montado y cuentas de prueba: Stripe pk_test + 4242, PayPal y Binance sandbox, Zelle = el formulario. Nunca producción. El visual QA de los agentes cubre la UI sin dinero; el DoD con dinero no es gate de los agentes.

  • Diseño · consecuencia

    Pantalla de stack, entradas directas

    /payments con tabs Pendientes (contador) / Historial y ?tab=history para deep links. «Pagar» del home y «Pagar ahora» de la alerta del flete abren el sheet directamente; se retira el toast «próximamente».

  • Deuda anotada · para goal-phases

    Lo que la web debe y la app no repite

    Dos mapas de color de estatus (gana el del widget). Tonos purple/sky fuera de escala. «Ver envío» para todos los tipos. Agrupación por createdAt y descarte de paidAt/equivalent*. El sheet web no invalida sidebar-counts. CorridorBadge morado con nombre crudo. Ortografía del portal. PAYMENT_CANCELLED sale del contrato. Sin eventos PostHog: definir payment_*.