Estructura del Proyecto
nvito-client es una aplicación móvil nativa para iOS y Android construida con React Native y Expo. Es el 4to proyecto del ecosistema Nvito y complementa las aplicaciones web existentes ofreciendo una experiencia nativa optimizada para el día del evento.
Stack Tecnológico
| Tecnología | Versión | Propósito |
|---|---|---|
| Expo | 54.x | Plataforma de desarrollo React Native |
| React Native | 0.81.x | Framework de UI nativa |
| TypeScript | 5.9 | Tipado estático |
| Expo Router | 6.x | Navegación file-based con layouts anidados |
| NativeWind | 4.x | Tailwind CSS para React Native |
| Tailwind CSS | 3.4 | Estilos utilitarios |
| TanStack React Query | 5.x | Cache y fetching de datos |
| expo-camera | 17.x | Cámara y escaneo QR |
| expo-av | 16.x | Grabación y reproducción de audio |
| expo-notifications | 0.32.x | Push notifications (Expo Push API) |
| expo-secure-store | 15.x | Almacenamiento seguro de tokens |
| expo-image-picker | 17.x | Selección y captura de fotos |
| expo-location | 19.x | Geolocalización |
| expo-haptics | 15.x | Feedback háptico nativo |
| date-fns | 4.x | Formateo de fechas |
| Zod | 4.x | Validación de esquemas |
| react-native-reanimated | 4.x | Animaciones de alto rendimiento |
| react-native-gesture-handler | 2.x | Gestos nativos |
| @expo/vector-icons | 15.x | Iconografía (Ionicons) |
Configuración de Plataforma
| Aspecto | iOS | Android |
|---|---|---|
| Identificador | com.nvito.client | com.nvito.client |
| Soporte tablet | Sí (iPad) | Sí |
| Deep links | Associated Domains (applinks:nvito.mx) | Intent Filters (nvito.mx/join) |
| Permisos | Cámara, Micrófono, Fotos, Ubicación | CAMERA, RECORD_AUDIO, LOCATION |
| Arquitectura | New Architecture habilitada | New Architecture habilitada |
Estructura de Directorios
Estructura de Directorios
Navegación y Rutas
La app usa Expo Router v6 con navegación file-based. La estructura de navegación se adapta según el rol del usuario autenticado (HOST o GUEST).
Diagrama de Navegación
Navegacion — nvito-client
Descripción de Rutas
| Ruta | Rol | Descripción |
|---|---|---|
/ | — | Redirect a login o app según estado de sesión |
/(auth)/login | — | Login anfitrión con código de evento + PIN |
/(auth)/join | — | Acceso invitado via deep link o código QR |
/(app)/(host)/dashboard | HOST | Dashboard con métricas en tiempo real |
/(app)/(host)/guests | HOST | Lista de invitados con búsqueda y filtros |
/(app)/(host)/scanner | HOST | QR Scanner para check-in de invitados |
/(app)/(host)/tables | HOST | Mesas: vista lista + plano visual read-only |
/(app)/(host)/more | HOST | Menú con acceso a memorias, itinerario, regalos y logout |
/(app)/(host)/memorias | HOST | Vista unificada: Fotos, Audios y Mensajes con segmented control |
/(app)/(host)/itinerary | HOST | Itinerario del evento (componentes compartidos con GUEST) |
/(app)/(host)/gift-registry | HOST | Mesa de regalos read-only (fondo, tiendas, cuentas bancarias) |
/(app)/(host)/gallery | HOST | Galería de fotos (hidden tab, retrocompatibilidad) |
/(app)/(host)/audio-guestbook | HOST | Mensajes de audio (hidden tab, retrocompatibilidad) |
/(app)/(host)/messages | HOST | Mensajes de texto (hidden tab, retrocompatibilidad) |
/(app)/(guest)/home | GUEST | QR pass, countdown, info rápida |
/(app)/(guest)/itinerary | GUEST | Itinerario del evento + ubicaciones |
/(app)/(guest)/gallery | GUEST | Galería colaborativa + subir fotos |
/(app)/(guest)/interact | GUEST | Audio guestbook, mensajes, mesa de regalos |
Tabs del Anfitrión
| Tab | Icono | Pantalla |
|---|---|---|
| Dashboard | bar-chart-outline | Estadísticas en vivo del evento |
| Invitados | people-outline | Lista de invitados con estados |
| Scanner | scan-outline | QR Scanner (tab central destacado) |
| Mesas | grid-outline | Mesas con vista lista y plano visual |
| Más | menu-outline | Memorias, itinerario, mesa de regalos, compartir, logout |
Tabs del Invitado
| Tab | Icono | Pantalla |
|---|---|---|
| Inicio | home-outline | QR pass + countdown |
| Programa | calendar-outline | Itinerario + ubicaciones |
| Galería | camera-outline | Ver + subir fotos |
| Interactuar | mic-outline | Audio, mensajes, regalos |
Autenticación Móvil
La app usa un sistema de autenticación independiente de Clerk, basado en JWT con un secreto dedicado (MOBILE_JWT_SECRET). Esto permite que los invitados accedan sin necesidad de crear una cuenta.
Flujo de Autenticación del Anfitrión
Autenticacion — Anfitrion (HOST)
Flujo de Autenticación del Invitado
Autenticacion — Invitado (GUEST)
Almacenamiento de Sesión
| Dato | Storage | Propósito |
|---|---|---|
accessToken (JWT) | expo-secure-store | Autenticación de requests |
refreshToken | expo-secure-store | Renovación del JWT |
| Session data (rol, eventId) | expo-secure-store | Restauración de sesión |
Auto-refresh de Tokens
El AuthContext programa automáticamente la renovación del JWT:
- Al hacer login, se programa un timer basado en
expiresIndel JWT - El refresh se ejecuta cuando quedan < 2 minutos de vida
- Al volver del background, se programa un refresh conservador inmediato
- La rotación de refresh tokens garantiza que cada token solo se use una vez
JWT Payload
// HOST
{ sub: eventId, role: "HOST", userId: string, sessionId: string }
// GUEST
{ sub: eventId, role: "GUEST", guestId: string, sessionId: string }
Pantallas del Anfitrión (HOST)
Dashboard
Pantalla principal del anfitrión con estadísticas del evento en tiempo real.
| Sección | Descripción |
|---|---|
| Header | Nombre del evento + fecha |
| Countdown | Días, horas, minutos hasta el evento |
| Stats Cards | Total invitados, confirmados, check-ins, fotos |
| RSVP Ring | Gráfico circular de confirmaciones |
| Actividad reciente | Feed de últimos RSVP, check-ins, fotos |
- Polling cada 30 segundos para datos en vivo
- Pull-to-refresh manual
- Layout adaptativo: cards en grid para tablet
Invitados
Lista completa de invitados con búsqueda y filtros.
| Funcionalidad | Descripción |
|---|---|
| Búsqueda | Por nombre del invitado |
| Filtros | Por status: todos, confirmados, pendientes, declinados |
| Guest Card | Nombre, status badge, grupo, # acompañantes. Pressable para abrir detalle |
| Stats Header | Contadores por status |
| Guest Detail Sheet | Bottom sheet modal al tocar un invitado (ver abajo) |
- FlatList virtualizada para rendimiento con listas grandes
- Layout responsive: 2 columnas en tablet
Detalle de Invitado (Bottom Sheet)
Al tocar un GuestCard, se abre un modal slide-up con el detalle completo del invitado (useGuestDetail hook). Secciones:
| Sección | Contenido |
|---|---|
| Header | Avatar (iniciales), nombre completo, status badge, indicador check-in |
| Info | Grupo (con color), mesa, asiento, asistentes (checkedIn/groupSize) |
| RSVP | Status, cantidad de asistentes, mensaje, fecha de respuesta |
| Necesidades | Restricciones dietarias (chips), alergias, necesidades especiales, notas dietéticas |
| Notas | Notas generales del invitado |
| Tags | Etiquetas como chips de colores |
Componentes: guests/guest-detail-sheet.tsx, guests/guest-card.tsx (pressable).
Scanner (QR Check-in)
Scanner QR fullscreen para registrar la asistencia de invitados.
QR Scanner — Check-in
| Característica | Detalle |
|---|---|
| Scanner | expo-camera CameraView con barcode scanning |
| Tipos QR | ['qr'] |
| Check-in parcial | Para grupos, permite indicar cuántos asisten |
| Estadísticas en vivo | Header con checked-in / total |
| Polling | Cada 15 segundos para stats actualizados |
| Feedback | expo-haptics (success, warning, error) |
| Check-in manual | Búsqueda por nombre para check-in sin QR (ver abajo) |
| Historial | Lista de últimos check-ins debajo del scanner |
Check-in Manual por Nombre
Botón de búsqueda en el header del Scanner que abre un modal slide-up para registrar invitados sin necesidad de escanear QR:
| Funcionalidad | Descripción |
|---|---|
| Búsqueda | Input de texto, se activa al escribir 2+ caracteres |
| Resultados | Lista de invitados con avatar, nombre, grupo, status y badge check-in |
| Selección | Al tocar un invitado, se muestra el selector de asistentes (si groupSize > 1) |
| Confirmación | Botón para confirmar el check-in manual con la cantidad de asistentes |
| Resultado | Reutiliza el CheckInResultModal existente con feedback háptico |
Componentes: scanner/manual-check-in-sheet.tsx, scanner/hooks/use-manual-search.ts.
Mesas (HOST)
Pantalla read-only para visualizar la distribución y asignación de mesas del evento. La gestión (crear, editar, asignar) se realiza exclusivamente desde el admin web.
| Funcionalidad | Descripción |
|---|---|
| Toggle vista | Selector segmentado Lista / Plano |
| Stats summary | Barra con total mesas, capacidad, ocupados, disponibles, % |
| Vista Lista | FlatList con TableCard (forma visual, barra de ocupación, invitados). Layout responsive: 2 columnas en tablet |
| Vista Plano | ScrollView con pinch-to-zoom (0.5x–3x). Mismas dimensiones y auto-layout que el admin (900x800, TABLE_SLOT=180px) |
| Detalle modal | Sheet slide-up con stats de mesa + lista de invitados asignados |
| Colores ocupación | Verde (menos de 70%), Amarillo (70-90%), Rojo (más de 90%) |
| Formas visuales | ROUND (círculo), SQUARE, RECTANGLE, OVAL — con borde y fondo por ocupación |
| Pull-to-refresh | En vista lista |
| Empty state | "No hay mesas configuradas. Configura las mesas desde el panel de administración" |
Componentes: tables.tsx (orquestador), tables/stats-summary.tsx, tables/table-card.tsx, tables/table-list.tsx, tables/table-visual.tsx, tables/table-plan.tsx, tables/table-detail-sheet.tsx.
Memorias
Vista unificada que fusiona Galería, Audio Guestbook y Mensajes en una sola pantalla con segmented control e infinite scroll. Accesible desde el menú "Más", reemplaza los 3 items separados.
| Funcionalidad | Descripción |
|---|---|
| Segmented control | 3 segmentos: Fotos, Audios, Mensajes — con badges de pendientes |
| Header | Back button + título "Memorias" |
| Infinite scroll | Cada segmento usa useInfiniteQuery con carga progresiva (20 items por página) |
Segmento Fotos
| Funcionalidad | Descripción |
|---|---|
| Grid | 3 columnas (móvil) / 5 columnas (tablet) |
| Thumbnail | Foto con badge del nombre del invitado |
| Viewer modal | Fullscreen con caption, autor, fecha relativa |
| Infinite scroll | useInfiniteGallery() — carga más fotos al llegar al final de la lista |
Segmento Audios
| Funcionalidad | Descripción |
|---|---|
| Filtros | Todos / Pendientes / Aprobados |
| Reproductor | Play/pause inline con expo-av |
| Moderación | Aprobar o rechazar mensajes pendientes (flujo completo) |
| Badge | Contador de audios pendientes en segmented control |
| Infinite scroll | useInfiniteAudioMessages() — carga progresiva de audios |
Segmento Mensajes
| Funcionalidad | Descripción |
|---|---|
| Filtros | Todos / No leídos |
| Indicador | Punto violeta + fondo destacado para no leídos |
| Marcar como leído | El host puede marcar mensajes individuales como leídos |
| Avatar | Inicial del nombre del invitado |
| Badge | Contador de no leídos en segmented control |
| Infinite scroll | useInfiniteMessages() — carga progresiva de mensajes |
Componentes: memorias.tsx (orquestador), memorias/segmented-control.tsx, memorias/memorias-gallery.tsx, memorias/memorias-audios.tsx, memorias/memorias-messages.tsx, memorias/photo-thumbnail.tsx, memorias/photo-viewer.tsx, memorias/message-card.tsx.
Itinerario (HOST)
Programa del evento con timeline visual idéntico al del invitado, accesible desde el menú "Más". Reutiliza los mismos componentes compartidos (src/components/itinerary/).
| Funcionalidad | Descripción |
|---|---|
| Header | Back button + título "Itinerario" |
| Timeline | Línea vertical con items ordenados por hora |
| Item activo | Destacado con punto primary cuando es la hora actual |
| Items pasados | Estilo atenuado (opacity reducida) |
| Ubicaciones | Cards con icono por tipo + link a Google Maps |
| Empty state | "El itinerario aún no está disponible" |
Componentes compartidos HOST/GUEST: src/components/itinerary/timeline-item.tsx (exporta isCurrentItem(), isPastItem(), TimelineItem), src/components/itinerary/location-card.tsx (LocationCard con icono por tipo de ubicación).
Mesa de Regalos (HOST)
Vista read-only del estado de la mesa de regalos del evento. Accesible desde el menú "Más". No incluye widget de Stripe ni botón de contribución (el HOST no dona a su propio fondo).
| Funcionalidad | Descripción |
|---|---|
| Header | Back button + título "Mesa de Regalos" |
| Fondo en efectivo | Barra de progreso, monto recaudado/meta (centavos→pesos MXN), porcentaje, mensaje |
| Tiendas/registros | Cards con nombre de tienda, descripción, link externo |
| Cuentas bancarias | Nombre del banco + descripción |
| Nota | Texto adicional del anfitrión |
| Empty state | "No hay mesa de regalos configurada" |
| Conversión | Valores en centavos convertidos a pesos con Intl.NumberFormat('es-MX') |
| Colores progreso | Verde (al menos 100%), Ambar (al menos 75%), Violeta (default) |
Componentes: gift-registry.tsx (orquestador), gift-registry/cash-fund-progress.tsx, gift-registry/registry-item-card.tsx.
Pantallas del Invitado (GUEST)
Home
Pantalla principal del invitado con bienvenida personalizada, pase QR y countdown al evento.
| Sección | Descripción |
|---|---|
| Bienvenida | Saludo personalizado con nombre del invitado |
| QR Code | Código QR grande para check-in |
| Countdown | Días, horas, minutos hasta el evento |
| Info rápida | Hora, mesa asignada, grupo |
| Nombre del evento | Título prominente + fecha |
Itinerario
Programa del evento con timeline visual. Las LocationCards incluyen información enriquecida.
| Funcionalidad | Descripción |
|---|---|
| Timeline | Línea vertical con items ordenados por hora |
| Item activo | Destacado basado en la hora actual |
| Ubicaciones | Links a mapas para cada item |
| Código de vestimenta | Destacado visual llamativo por ubicación |
| Estacionamiento | Indicador de disponibilidad e instrucciones |
| Accesibilidad | Información de accesibilidad por ubicación |
| Info | Hora, título, descripción por item |
Interactuar
Pantalla con secciones condicionales (service gating) para que el invitado interactúe con el evento. Cada tipo de contenido tiene un límite por invitado.
| Sección | Servicio requerido | Límite por invitado | Descripción |
|---|---|---|---|
| Mis fotos | GALLERY / GUEST_UPLOADS | 3 fotos | Solo muestra fotos subidas por el invitado con contador de restantes |
| Audio Guestbook | AUDIO_GUESTBOOK | 3 audios | Grabar mensaje de voz + ver mis audios con badge de estado (pendiente/aprobado/rechazado) |
| Mensaje | GUEST_MESSAGES | 3 mensajes | Input de texto para dejar mensaje a anfitriones con contador de restantes |
| Empty state | — | — | Cuando ninguna sección está habilitada |
Los límites se consultan mediante endpoints dedicados (/my-count) que retornan { count, limit }. Cuando el invitado alcanza el límite, el botón de acción se deshabilita con un mensaje informativo.
Hospedaje
Pantalla accesible desde "Mas" con toda la información de hospedaje del evento.
| Sección | Descripción |
|---|---|
| Info de viaje | Aeropuerto más cercano, estacionamiento, tips |
| Hoteles | Cards con foto, dirección, teléfono, distancia, botones "Ver sitio" y "Reservar" |
| Transporte | Opciones de transporte con iconos por tipo |
Más
Menú con opciones condicionales según servicios habilitados.
| Opción | Servicio requerido | Descripción |
|---|---|---|
| Mesa de regalos | GIFT_REGISTRY | Links a tiendas + fondo en efectivo |
| Hospedaje | ACCOMMODATION | Hoteles, transporte, tips de viaje |
| Compartir evento | — | Siempre disponible |
Service Gating
Las secciones y pantallas del invitado se muestran condicionalmente según los servicios contratados para el evento. El sistema usa dos mecanismos complementarios:
useEventServices(): Hook que obtiene la lista de servicios habilitados del evento. Si los servicios aún no se han cargado, se muestran todas las secciones como fallback.useRequireService(serviceNames): Guard hook que verifica si al menos uno de los servicios requeridos está habilitado. Retorna{ allowed, isLoading }para proteger pantallas accesibles via deep link o navegación directa.
Las vistas completas (Galería, Audio Guestbook, Mensajes, Mesa de Regalos, Hospedaje) solo se renderizan si el servicio correspondiente está contratado en el evento.
Arquitectura del API Client
El sistema de comunicación con el backend sigue una arquitectura en capas, similar a nvito-admin pero adaptada para autenticación móvil.
Flujo de Check-in — Estados
escaneando→procesandoQR detectadoprocesando→exitosoCheck-in OKprocesando→ya_registradoYa checked-inprocesando→invalidoQR no validoprocesando→checkin_parcialGrupo > 1checkin_parcial→exitosoConfirmar cantidadcheckin_parcial→escaneandoCancelarexitoso→escaneandoAuto-cerrar (3s)ya_registrado→escaneandoAuto-cerrar (3s)invalido→escaneandoAuto-cerrar (3s)API Client (client.ts)
Fetch wrapper que encapsula todas las operaciones HTTP:
- Métodos:
get<T>,post<T>,patch<T>,delete<T> - Base URL: Configurable via
EXPO_PUBLIC_API_URL(default:http://localhost:3000/v1) - Auth interceptor: Inyecta
Bearer tokendesdeSecureStoreautomáticamente - Auto-refresh: Si recibe 401, intenta renovar el JWT con el refresh token
- Error handling: Clase
ApiErrorconstatusCodeymessage
Services (7 archivos)
| Servicio | Archivo | Endpoints |
|---|---|---|
| Auth | auth.service.ts | Login, guest access, refresh, logout, push token |
| Events | events.service.ts | Datos del evento, stats, servicios, itinerario, ubicaciones |
| Guests | guests.service.ts | Lista de invitados, check-in, QR pass, RSVP stats |
| Media | media.service.ts | Galería (paginada), upload URLs, audio guestbook (paginado, moderación), mensajes (paginados, leídos), conteos por invitado, gift registry |
| Payments | payments.service.ts | Cash fund intent (Stripe), confirmación de pago |
| Sharing | sharing.service.ts | Compartir evento, generar card de preview |
Prefijo de Endpoints Móviles
Todos los endpoints consumidos por la app móvil usan el prefijo /v1/mobile/ y están protegidos por el MobileAuthGuard (independiente de Clerk). Los endpoints son wrappers ligeros que reutilizan los servicios existentes del backend.
Endpoints de Media Móvil
| Método | Path | Rol | Descripción |
|---|---|---|---|
GET | /mobile/events/:id/gallery | HOST/GUEST | Galería paginada (server-side) |
GET | /mobile/events/:id/gallery/my-count | GUEST | Conteo de fotos + límite del invitado |
POST | /mobile/events/:id/gallery/upload-url | HOST/GUEST | URL pre-firmada para subir foto |
POST | /mobile/events/:id/gallery | HOST/GUEST | Registrar foto subida |
GET | /mobile/events/:id/audio-guestbook | HOST/GUEST | Audios paginados (server-side) |
GET | /mobile/events/:id/audio-guestbook/my-count | GUEST | Conteo de audios + límite del invitado |
POST | /mobile/events/:id/audio-guestbook/upload-url | HOST/GUEST | URL pre-firmada para subir audio |
POST | /mobile/events/:id/audio-guestbook | HOST/GUEST | Registrar audio subido |
PATCH | /mobile/events/:id/audio-guestbook/:audioId/approve | HOST | Aprobar audio |
DELETE | /mobile/events/:id/audio-guestbook/:audioId | HOST | Rechazar y eliminar audio |
GET | /mobile/events/:id/messages | HOST | Mensajes paginados (server-side) |
GET | /mobile/events/:id/messages/mine | GUEST | Mensajes del invitado actual |
GET | /mobile/events/:id/messages/my-count | GUEST | Conteo de mensajes + límite del invitado |
POST | /mobile/events/:id/messages | GUEST | Enviar mensaje de felicitación |
PATCH | /mobile/events/:id/messages/:messageId/read | HOST | Marcar mensaje como leído |
Gestión de Estado
React Query 5
La app usa TanStack React Query como único sistema de gestión de estado del servidor:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 min stale
gcTime: 24 * 60 * 60 * 1000, // 24h en cache
retry: 2,
},
},
});
Query Key Factory
El archivo keys.ts centraliza todas las claves de cache:
queryKeys.events.detail(eventId) // ['events', eventId]
queryKeys.events.stats(eventId) // ['events', eventId, 'stats']
queryKeys.guests.list(eventId, params) // ['guests', eventId, 'list', params]
queryKeys.media.gallery(eventId, page) // ['media', eventId, 'gallery', page]
queryKeys.media.myPhotoCount(eventId) // ['media', eventId, 'my-photo-count']
queryKeys.media.audioMessages(eventId) // ['media', eventId, 'audio-messages']
queryKeys.media.myAudioCount(eventId) // ['media', eventId, 'my-audio-count']
queryKeys.media.messages(eventId) // ['media', eventId, 'messages']
queryKeys.media.myMessages(eventId) // ['media', eventId, 'my-messages']
queryKeys.media.myMessageCount(eventId) // ['media', eventId, 'my-message-count']
queryKeys.media.giftRegistry(eventId) // ['media', eventId, 'gift-registry']
Hooks de React Query
| Hook | Tipo | Descripción |
|---|---|---|
useEvent | Query | Datos del evento actual |
useEventStats | Query | Estadísticas con polling |
useGuests | Query | Lista de invitados con filtros |
useGallery | Query | Fotos paginadas (server-side) |
useInfiniteGallery | InfiniteQuery | Fotos con infinite scroll (Host Memorias) |
useMyPhotoCount | Query | Conteo y límite de fotos del invitado |
useAudioMessages | Query | Mensajes de audio (primera página) |
useInfiniteAudioMessages | InfiniteQuery | Audios con infinite scroll (Host Memorias) |
useMyAudioCount | Query | Conteo y límite de audios del invitado |
useMessages | Query | Mensajes de texto (primera página) |
useInfiniteMessages | InfiniteQuery | Mensajes con infinite scroll (Host Memorias) |
useMyMessages | Query | Mensajes enviados por el invitado actual |
useMyMessageCount | Query | Conteo y límite de mensajes del invitado |
useGuestDetail | Query | Detalle completo de un invitado |
useGuestStats | Query | Estadísticas de invitados |
useGiftRegistry | Query | Mesa de regalos del evento |
useCheckIn | Mutation | Registrar check-in |
useManualCheckIn | Mutation | Check-in manual por nombre (sin QR) |
useUploadPhoto | Mutation | Subir foto con presigned URL |
useUploadAudio | Mutation | Subir audio con presigned URL |
useApproveAudio | Mutation | Aprobar mensaje de audio |
useRejectAudio | Mutation | Rechazar y eliminar mensaje de audio |
useMarkMessageAsRead | Mutation | Marcar mensaje de texto como leído |
useSendMessage | Mutation | Enviar mensaje de texto |
AuthContext (Descomposición SOLID)
El contexto de autenticación fue descompuesto siguiendo SRP en 4 archivos con responsabilidad única:
| Archivo | LOC | Responsabilidad |
|---|---|---|
AuthContext.tsx | ~95 | Provider orquestador (login, logout, render) |
auth/auth-reducer.ts | ~54 | Estado, acciones y reducer puro |
auth/auth-storage.ts | ~86 | Persistencia tipada en SecureStore |
auth/use-session-restore.ts | ~68 | Restauración de sesión + callbacks API + auto-refresh |
El contexto gestiona el estado de sesión con un useReducer:
| Propiedad | Tipo | Descripción |
|---|---|---|
isAuthenticated | boolean | Usuario autenticado |
isLoading | boolean | Cargando sesión |
role | 'HOST' | 'GUEST' | null | Rol del usuario |
eventId | string | null | ID del evento activo |
guestId | string | null | ID del invitado (GUEST) |
userId | string | null | ID del usuario (HOST) |
accessToken | string | null | JWT actual |
refreshToken | string | null | Token de renovación |
Acciones del reducer:
RESTORE_SESSION— Restaurar sesión desde SecureStore al montarLOGIN— Login exitoso (host o guest)LOGOUT— Cerrar sesión y limpiar tokensTOKENS_REFRESHED— Actualizar tokens tras refreshSET_LOADING— Cambiar estado de carga
QR Scanner y Check-in
Flujo Completo de Check-in
Upload de Fotos
Datos del QR
Cada invitado tiene un QR único que contiene su guestId. Al escanear:
- La app extrae el
guestIddel contenido del QR - Envia
POST /v1/mobile/events/:id/check-incon elguestId - El API verifica que el invitado pertenece al evento y no está ya registrado
- Si el invitado tiene
groupSize > 1, se muestra un modal para seleccionar cuántos asisten
Check-in Parcial (Grupos)
Cuando un invitado tiene acompañantes, el scanner muestra:
- Nombre del invitado y total del grupo
- Stepper numérico para seleccionar asistentes
- Rango: 1 hasta
groupSize - Soporte para re-escaneo (agregar más personas después)
Media: Galería y Audio Guestbook
Upload de Fotos
Grabacion de Audio
Grabación de Audio
Push Notifications — Registro de Token
Límites de Media
| Tipo | Límite |
|---|---|
| Imagen max width | 2048 px |
| Calidad de imagen | 0.8 (80%) |
| Duración max audio | 120 segundos (2 min) |
| Fotos por invitado | 3 |
| Audios por invitado | 3 |
| Mensajes por invitado | 3 |
Los límites por invitado se verifican tanto en el frontend (contadores visuales, botón deshabilitado) como en el backend (validación server-side que retorna 403 al exceder el límite).
Push Notifications
Registro de Push Token
Arquitectura del API Client — nvito-client
Configuración del Handler
La app configura las notificaciones en foreground para mostrar alertas, sonido, badge y banners:
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
Eventos que Disparan Push
| Evento | Destinatario | Mensaje |
|---|---|---|
| Nuevo RSVP | HOST | "{nombre} confirmó asistencia" |
| Check-in | HOST | "{nombre} llegó al evento" |
| Foto subida | HOST | "Nueva foto de {nombre}" |
| Audio subido | HOST | "Nuevo mensaje de voz de {nombre}" |
| Cambio en evento | GUEST | "El evento fue actualizado" |
| Recordatorio | GUEST | "Recuerda: {evento} es mañana" |
Canal de Android
Para Android, se configura un canal de notificaciones dedicado:
Notifications.setNotificationChannelAsync('default', {
name: 'Nvito',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#7C3AED',
});
Deep Linking
La app soporta deep links para acceso directo de invitados.
URIs Soportados
| URI | Propósito |
|---|---|
nvito://join/{accessToken} | Acceso directo invitado (custom scheme) |
nvito://event/{eventCode} | Acceso anfitrión (solicita PIN) |
https://nvito.app/join/{token} | Universal link (iOS Associated Domains) |
Configuración
iOS — Associated Domains:
"associatedDomains": ["applinks:nvito.app"]
Android — Intent Filters:
"intentFilters": [{
"action": "VIEW",
"autoVerify": true,
"data": [{
"scheme": "https",
"host": "nvito.app",
"pathPrefix": "/join"
}],
"category": ["BROWSABLE", "DEFAULT"]
}]
Custom Scheme:
"scheme": "nvito"
Design System
Fuente Única de Verdad: src/theme/colors.ts
Todos los colores de la aplicación están centralizados en un único archivo src/theme/colors.ts exportado como as const para type safety. El archivo tailwind.config.js importa estos colores directamente, eliminando la duplicación.
Antes: ~130 instancias de colores hex hardcodeados dispersos en 12+ archivos.
Después: 0 colores hardcodeados; todos referencian colors.* desde el theme.
Paleta de Colores
| Token | Color | Hex | Uso |
|---|---|---|---|
primary | Violeta | #7C3AED | Acciones principales, tabs activos, badges |
secondary | Rosa | #EC4899 | Acentos, elementos decorativos |
success | Verde | #10B981 | Check-in exitoso, confirmados |
warning | Ambar | #F59E0B | Pendientes, alertas suaves |
error | Rojo | #EF4444 | Errores, declinados, logout |
info | Azul | #3B82F6 | Información, links |
background | Blanco | #FFFFFF | Fondo principal |
surface | Gris claro | #F9FAFB | Fondo de cards y secciones |
text-primary | Gris oscuro | #111827 | Texto principal |
text-secondary | Gris medio | #6B7280 | Texto secundario |
text-muted | Gris claro | #9CA3AF | Texto deshabilitado |
Integración con Tailwind
// tailwind.config.js
const { colors } = require('./src/theme/colors');
module.exports = {
theme: { extend: { colors } },
};
NativeWind (Tailwind CSS para React Native)
La app usa NativeWind v4 que permite usar clases de Tailwind CSS directamente en componentes React Native:
<View className="flex-1 bg-background p-4">
<Text className="text-xl font-heading text-text-primary">Titulo</Text>
<Text className="text-sm text-text-secondary mt-1">Descripción</Text>
</View>
Tipografía
| Token | Fuente | Uso |
|---|---|---|
font-sans | Inter | Texto general |
font-heading | Inter-Bold | Títulos y encabezados |
Responsive / Tablet
La app detecta si se ejecuta en tablet mediante el hook useResponsive() y adapta los layouts:
| Aspecto | Móvil | Tablet |
|---|---|---|
| Grid de galería | 3 columnas | 5 columnas |
| Cards de stats | Stack vertical | Grid 2x2 |
| Lista de invitados | 1 columna | 2 columnas |
| Padding general | 16px | 24px |
Iconografía
Se usa Ionicons via @expo/vector-icons con estilo outline consistente en toda la app.
Variables de Entorno
| Variable | Descripción | Default |
|---|---|---|
EXPO_PUBLIC_API_URL | URL base de nvito-api | http://localhost:3000/v1 |
EXPO_PUBLIC_APP_NAME | Nombre de la aplicación | Nvito |
EXPO_PUBLIC_ENV | Entorno actual | development |
Arquitectura SOLID
El proyecto aplica Single Responsibility Principle (SRP) sistemáticamente. Las pantallas grandes fueron descompuestas en orquestadores livianos + sub-componentes + hooks custom.
Descomposición de Pantallas
| Pantalla | Antes (LOC) | Después (LOC) | Sub-componentes | Hooks extraídos |
|---|---|---|---|---|
scanner.tsx | 737 | ~80 | 5 (modals, stats, list, permissions) | use-scanner.ts |
dashboard.tsx | 326 | ~120 | 4 (stat-card, countdown, rsvp, activity) | — |
guests.tsx | 331 | ~150 | 3 (guest-card, stats-bar, filters) | — |
gallery.tsx (guest) | 303 | ~120 | 2 (thumbnail, viewer) | use-photo-upload.ts |
audio-guestbook.tsx | 266 | ~117 | 2 (audio-item, filter-tabs) | use-audio-player.ts |
| Total | 1,963 | ~587 | 16 | 3 |
Reducción total: 70% menos líneas en archivos orquestadores, distribuidas en componentes enfocados y reutilizables.
Descomposición del AuthContext
| Archivo | LOC | Responsabilidad |
|---|---|---|
AuthContext.tsx | ~95 | Provider: login/logout callbacks, render children |
auth/auth-reducer.ts | ~54 | Reducer puro: estado, acciones, transiciones |
auth/auth-storage.ts | ~86 | Persistencia: save/restore/clear en SecureStore tipado |
auth/use-session-restore.ts | ~68 | Efecto: restaurar sesión al montar, configurar callbacks API, programar refresh |
Validación con Zod
El proyecto incluye schemas Zod v4 para validar respuestas de API en runtime, complementando la seguridad de tipos de TypeScript en compilación.
Schemas Disponibles
| Archivo | Schemas | Propósito |
|---|---|---|
validations/auth.ts | loginResponseSchema, refreshResponseSchema | Validar respuestas de login y refresh |
validations/event.ts | eventDetailSchema, eventStatsSchema | Validar datos del evento y estadísticas |
validations/guest.ts | checkInResultSchema, guestQrPassSchema | Validar resultado de check-in y QR pass |
Cada schema tiene tests que verifican datos válidos, campos faltantes y tipos incorrectos.
Testing
El proyecto cuenta con 32 suites de prueba y 257 tests individuales, todos con 100% de tasa de éxito.
Framework
| Herramienta | Versión | Propósito |
|---|---|---|
| Jest | vía jest-expo | Test runner |
| jest-expo | Preset | Configuración para Expo/React Native |
| @testing-library/react-native | 13.x | Renders de hooks y componentes |
| @testing-library/jest-native | Matchers | Matchers nativos (toBeOnTheScreen, etc.) |
Mocks Globales
El archivo test/setup.ts configura mocks de 10 módulos nativos que no están disponibles en Node.js:
| Módulo | Mock |
|---|---|
expo-secure-store | In-memory store con get/set/delete |
expo-crypto | UUID fijo + hash mock |
expo-camera | CameraView + useCameraPermissions |
@react-native-async-storage/async-storage | In-memory store completo |
@react-native-community/netinfo | Conectividad siempre true |
expo-constants | Config con API_URL local |
expo-notifications | Permisos + push token mock |
expo-haptics | Feedback háptico no-op |
expo-router | useRouter, useLocalSearchParams, etc. |
Cobertura por Area
| Área | Suites | Tests | Qué prueban |
|---|---|---|---|
| Utils | 2 | 17 | date.ts (formatEventDate, formatRelative, getCountdown), format.ts |
| API errors | 1 | 12 | ApiError constructor, isUnauthorized, isForbidden, isNotFound, isServerError |
| Servicios API | 6 | 38 | auth, events, guests, media, payments, sharing |
| Hooks queries | 6 | 26 | use-event, use-event-stats, use-guests, use-media, use-payments, use-sharing |
| Storage | 2 | 21 | secure-store (7 funciones), offline-queue (6 operaciones) |
| Contexts | 4 | 33 | AuthContext (login/logout/restore), ConnectivityContext, auth-reducer, auth-storage |
| Validaciones Zod | 3 | 39 | auth, event, guest schemas (datos validos + invalidos) |
| Theme | 1 | — | Colors structure |
| Total | 32 | 257 |
Calidad del Código
| Indicador | Valor |
|---|---|
| TypeScript strict | Habilitado |
as any | 0 |
| JSDoc | 100% en métodos públicos (~80 JSDoc comments) |
| Zod schemas | 6 schemas con tests |
| Test suites | 32 |
| Tests individuales | 257 |
| Tasa de éxito | 100% |
| Bugs corregidos | 3 (import bug, as any, hooks duplicados) |
| Colores hardcodeados | 0 (130+ migrados a theme) |
| Componentes >300 LOC | 0 (5 descompuestos via SOLID) |
| Design tokens centralizados | Sí (theme/colors.ts → tailwind.config.js) |
| Violaciones SRP | 0 |
tsc --noEmit | 0 errores |
Scripts de Desarrollo
# Iniciar servidor de desarrollo
npm start
# Iniciar con target específico
npm run ios # Simulador iOS
npm run android # Emulador Android
# Tests
npm run test # Jest watch mode
npm run test:run # Jest single run
npm run test:cov # Jest con reporte de coverage
# Lint y formateo
npm run lint # ESLint
npm run format # Prettier
Referencias
- Código fuente:
nvito-client/ - API Client:
src/api/client.ts - Services:
src/api/services/(7 archivos) - Query Hooks:
src/hooks/queries/(7 archivos) - Query Keys:
src/hooks/queries/keys.ts - AuthContext:
src/contexts/AuthContext.tsx+src/contexts/auth/ - ConnectivityContext:
src/contexts/ConnectivityContext.tsx - Validations:
src/validations/(3 schemas) - Theme:
src/theme/colors.ts - Storage:
src/storage/(secure-store, offline-queue) - Notifications:
src/hooks/use-notifications.ts - Shared Components:
src/components/itinerary/(timeline-item, location-card) - Types:
src/types/(7 archivos) - Tests:
test/setup.ts+**/__tests__/(29 suites)