Zymplo Mobile · PR #2793 (merged) + PR #2810 → develop

Los 3 pendientes también cerrados: Google Calendar, 2FA y "Invitá amigos"

Segunda pasada sobre lo que había quedado documentado como "requiere backend" — se investigó cada uno hasta encontrar qué era realmente construible, y se construyó de verdad (no un parche cosmético).

PR #2793 → develop · MERGED PR #2810 → develop · en revisión medido 2026-07-28 · 12:55Z
0
errores TypeScript nuevos (ambos repos)
0
errores ESLint nuevos
1469+42
tests verdes (zymplo-api + mobile)
12/12
bugs originales con causa raíz resuelta

Fixes cerrados al 100% (mobile-only)

01Comunidad apuntaba a un dominio muerto

Corregido
Antes
Botón "Abrir comunidad" abría community.zymplo.comERR_NAME_NOT_RESOLVED (Discourse nunca se desplegó).
Causa raíz
DNS nunca aprovisionado para ese subdominio.
app/(tabs)/profile/community.tsx
'https://community.zymplo.com'
'https://whatsapp.com/channel/0029VbCtZKuJENy6259hMk2Y'

02Botón de cámara en ZyChat abría la galería

Corregido
Antes
El ícono de cámara visible en la barra de chat llamaba al picker de galería, no a la cámara en vivo.
Causa raíz
Handler cruzado: onPress={'{onPickImage}'} en vez de onOpenCamera (que ya existía y funcionaba bien detrás del botón "+").
components/ai/ZyChatScreen.tsx
onPress={onPickImage}   // abre galería
onPress={onOpenCamera}  // abre cámara nativa

03Tab "IA" con ícono genérico → "ZyChat" con isotipo real

Corregido
Antes
Sparkle ✨ genérico de librería de íconos, label "IA" en los 3 idiomas.
Ahora
Isotipo real de Zymplo (ZymploMark, mismo componente del header del chat) + label "ZyChat" (pt/es/en). Mismo fix en el empty-state del chat.

04Banner falso "Sin conexión a internet"

Corregido
Antes
Aparecía con wifi lleno. Ya había un fix previo (debounce 2 fallos) pero sin protección contra checks simultáneos.
Causa raíz
2-3 disparadores del mismo probe() (mount + evento NetInfo ambiguo + interval) podían correr en paralelo y sumar 2 "fallos" de un solo instante de lentitud, cruzando el umbral sin que hubiera 2 fallos reales separados en el tiempo.
hooks/useNetworkStatus.ts
const probe = async () => {
  if (inFlight.current) return;  // guard de concurrencia
  inFlight.current = true;
  try { ... } finally { inFlight.current = false; }
}

05Home: nombre duplicado + pills solapadas

Corregido
Antes
"Buenas noches, Carl…" cortado + "Carlos" repetido debajo + 3 pills (nivel/puntos/racha) amontonadas sin espacio.
Causa raíz
Mismo campo (empresa.nome) renderizado 2 veces; contenedor de texto sin flexShrink; fila de pills sin flexWrap.
app/(tabs)/index.tsx
{empresa?.segmento ? <Text>{empresa.nome}</Text> : null}
{empresa?.segmento && empresa.nome !== greetingName ? <Text>{empresa.nome}</Text> : null}

greetingTextWrap: { flex: 1, minWidth: 0 }
headerPills: { flexWrap: 'wrap', maxWidth: '58%' }

06"Mi negocio": columnas pegadas ("GASTOSA COBRAR")

Corregido
Antes
Grid de 4 stats sin ancho fijo por ítem — "GASTOS" y "A COBRAR" terminaban en la misma línea sin espacio entre sí.
Causa raíz
flexWrap empaquetaba por ancho de contenido, no por columna — nunca era un grid 2×2 real.
app/(tabs)/index.tsx · Orbit()
<View style={{ width: '50%', ... }}>  // fuerza 2 columnas exactas

07"Copiar link" en recibos compartía una URL que nunca existió

Corregido
Antes
Compartía zymplo.app/receipt/:id — 404 garantizado, no hay ninguna página web así en ningún repo.
Ahora
Comparte el mismo resumen de texto que ya funciona en el botón de WhatsApp. Botón renombrado "Compartir" (antes decía "Copiar link" sin copiar nada).

08"Invitá amigos": código y botones muertos

Corregido · ver #12 para el resto
Antes
"Tu código" quedaba en "···" para siempre. "Compartir" y "Copiar link" no hacían nada, sin ningún error visible.
Causa raíz
Leía (s as any).token, un campo inexistente (el real es firebaseToken) → nunca mandaba el header de autorización → 401 silencioso (sin chequear response.ok).
app/(tabs)/profile/referrals.tsx
const token = useAuthStore((s) => (s as any).token) ?? '';
const token = useAuthStore((s) => s.firebaseToken) ?? '';
if (!r.ok) throw new Error(`http_${r.status}`);  // antes: silencio total
// + estado de error visible con botón "Intentar de nuevo"

09Mismo bug de campo, repetido en 6 pantallas más

Corregido
Antes
DailyScratchCard, wheel, leaderboard, privacy (exportar/borrar datos LGPD), audit-log, shop — todos con (s as any).user?.id ?? 1: si el bug no se disparaba silencioso, atribuía todo al usuario 1 fijo para cualquiera.
Por qué importa
El más serio es privacy.tsx: exportar/borrar datos (LGPD) nunca funcionaba para ningún usuario real.
6 archivos
const userId = useAuthStore((s) => (s as any).user?.id) ?? 1;
const userId = useAuthStore((s) => s.userId);  // campo real, sin default falso

Segunda pasada · los 3 pendientes, construidos de verdad (PR #2810)

10Google Calendar "no verificado" — la causa real no era Google

Corregido 100% · sin bloqueos
Lo que se creía
Que faltaba que Google terminara de aprobar la verificación.
Lo que era en realidad
Google ya había aprobado el scope calendar.events el 26/jul. El cartel rojo salía porque la app, ante cualquier fallo de red transitorio, caía en un OAuth directo desde el celular con un client_id que nunca pasó por Google — código viejo, sin usar en ningún otro lado.
app/(tabs)/profile/google-calendar.tsx
catch { const tokens = await signInWithGoogle(); ... }  // fallback sin verificar
catch { Alert.alert(error, 'No pudimos conectar. Probá de nuevo.'); }  // reintenta el MISMO flujo verificado
// + se borró lib/googleCalendar.ts entero (código muerto, sin otro uso)

112FA (app autenticadora + WhatsApp) — construido completo

Código 100% listo · falta 1 paso de DBA
Qué se construyó
TOTP real (verificado contra los 5 vectores de prueba oficiales del estándar RFC 6238) + códigos de respaldo + WhatsApp OTP (canal ya conectado a Meta, no un simulado) + cifrado del secreto (AES-256-GCM) + confirmar contraseña para desactivar. 37 tests automáticos, todos verdes.
Qué falta (1 paso, no código)
La tabla en la base de datos Oracle todavía no existe — dejé el script SQL listo para que un DBA lo corra en un solo paso, y la función queda apagada (TWO_FACTOR_ENABLED=false) hasta ese momento, exactamente igual que se hace con cada país nuevo de facturación en este proyecto.
migrations/2026-07-28_two_factor_auth.sql
-- Tabla ZMP.ZMP_TWO_FACTOR lista para aplicar
-- + variable TWOFA_ENC_KEY en Doppler
-- + flip TWO_FACTOR_ENABLED=true cuando esté aplicada

12"Invitá amigos" — puente de autenticación real construido

Código 100% listo · falta 1 paso de DBA
Qué se construyó
Un endpoint nuevo que toma la sesión con la que ya estás adentro de la app y la cambia por el tipo de acceso que la pantalla de referidos necesita — el celular ahora lo hace automático y en caché, sin que el usuario note nada.
Qué falta (1 paso, no código)
Ese intercambio necesita buscar tu WhatsApp a partir de tu empresa — ese buscador (endpoint de lectura, sin tabla nueva) todavía no existe en Oracle. Documentado y listo para que el DBA lo cree en un paso.
zymplo-api/.../auth.routes.ts
POST /api/mobile/auth/bridge-from-ords
  → intercambia tu sesión actual por el token que "Invitá amigos" necesita

Lo único que queda pendiente ahora — y es de infraestructura, no de código

Dos archivos SQL, listos para aplicar en un solo paso cada uno, más 2 variables de configuración. Sin eso, 2FA e "Invitá amigos" completo simplemente no tienen dónde guardar sus datos — no es que falte programarlos, ya está programado y probado.

migrations/2026-07-28_two_factor_auth.sql Crea la tabla de 2FA. Después: variable TWOFA_ENC_KEY en Doppler + TWO_FACTOR_ENABLED=true.
migrations/2026-07-28_referrals_owner_contact_handler.sql Crea el buscador de WhatsApp por empresa (solo lectura, reusa un cruce de datos que ya existe hoy).
Ruleta, ranking, recompensa diaria, mini-tienda, workflows/asistentes/analytics Mismo patrón de fondo (pantallas ya construidas sin servidor del otro lado) pero de MENOR impacto hoy — ruleta y ranking ya están apagados por defecto. Quedan fuera de esta tanda; se puede armar un plan aparte si se prioriza.
PR #2793 → develop
MERGED · 18 archivos · 42/42 tests
Ver PR #2793 →
PR #2810 → develop
2FA + bridge referrals + fix Google Calendar · 43 archivos · 1469+42 tests · CI corriendo
Ver PR #2810 →
Resumen final · para leer solo esto y ya saber qué hacer

Ahora los 12 problemas de las fotos originales tienen su causa real encontrada y su arreglo de código 100% terminado y probado con robots automáticos. Google Calendar ya quedó perfecto, sin nada pendiente. Para 2FA y "Invitá amigos" completo, el código está listo pero necesita que alguien con acceso a la base de datos (Luz o Victor) corra 2 archivos SQL cortos — es un trámite de minutos para ellos, no una tarea de programación que falte.

  1. Entrá a este link del PR #2810 y esperá que los checks verdes terminen.
  2. Cuando estén verdes, aprobalo vos o pedile a Luz que lo revise — se mergea a develop, nunca directo a main.
  3. Pedile a Luz o Victor que corran los 2 archivos SQL nuevos (migrations/2026-07-28_*.sql) y configuren TWOFA_ENC_KEY en Doppler — con eso, 2FA e "Invitá amigos" quedan funcionando de punta a punta.
  4. Para que todo esto se vea en la app real de tu teléfono, hace falta un build nuevo a TestFlight/Play — decime si querés que lo dispare.