Frontend — OptiRuta
App móvil en Expo SDK 56 + React Native 0.85 + TypeScript con expo-router.
Para el README general, ver ../README.md. Para la guía de integración con el backend (compañeros), ver INTEGRATION.md.
Tabla de contenidos
- Stack
- Estructura
- Cómo correr
- Configurar la URL del backend
- Pantallas
- Estado global (AppContext)
- Comunicación con el backend
- Componentes reutilizables
- Roles y navegación
- Notificaciones
- Troubleshooting
Stack
- Expo SDK 56 (Sept 2025)
- React Native 0.85 + React 19
- TypeScript estricto
- expo-router 56 — navegación basada en archivos
- @expo/vector-icons — iconos Ionicons
- expo-image — imágenes optimizadas
- Sin librerías de estado externas (solo Context)
- Sin librerías de UI externas (StyleSheet nativo)
Estructura
frontend/
├── app.json ← config de Expo (name="OptiRuta", slug, etc.)
├── tsconfig.json ← paths "@/*" → "./src/*"
├── assets/
│ ├── images/ ← icons del SO, splash
│ └── illustrations/ ← ilustraciones del UI (camión, botes, etc.)
└── src/
├── app/ ← rutas de expo-router (cada archivo = pantalla)
│ ├── _layout.tsx ← Tabs + AppProvider + Toast
│ ├── index.tsx ← Home (ETA + estado de ruta)
│ ├── login.tsx
│ ├── register.tsx
│ ├── addresses.tsx ← validación de domicilio
│ ├── alerts.tsx ← timeline de notificaciones
│ ├── feedback.tsx ← buzón de retro
│ ├── guide.tsx ← calendario + guía de separación
│ ├── admin.tsx ← panel admin
│ └── profile.tsx ← perfil + ajustes
├── components/
│ ├── EtaCard.tsx
│ ├── Alertltem.tsx ← (sic) typo histórico, no romper
│ ├── InputField.tsx
│ ├── PrimaryButton.tsx
│ ├── QuickAction.tsx
│ ├── SectionTitle.tsx
│ ├── StatusBadge.tsx
│ ├── CollectionCalendar.tsx
│ └── NotificationToast.tsx
├── config/
│ └── api.ts ← URL del backend (cambia según dispositivo)
├── constants/
│ ├── colors.ts
│ └── theme.ts
├── context/
│ └── AppContext.tsx ← estado global + polling
├── data/
│ └── mocks/
│ └── routes.mock.ts ← catálogo de routeId/name (para el calendario)
├── hooks/
├── lib/
│ ├── api.ts ← apiFetch (wrapper de fetch + auth token)
│ ├── notifications.ts ← (preparado para Dev Build; no se importa en Go)
│ └── notification-mapper.ts
└── services/
├── auth.service.ts
├── tracking.service.ts
├── addresses.service.ts
├── feedback.service.ts
└── admin.service.ts
Cómo correr
cd frontend
npm install
npx expo start -c # -c limpia caché de Metro
Después:
a→ Android emulatori→ iOS simulator (solo Mac)w→ web- Escanear QR con Expo Go en celular físico
Configurar la URL del backend
Edita src/config/api.ts:
// Para Android emulator local:
export const API_URL = "http://10.0.2.2:8080";
// Para celular físico (mismo Wi-Fi o hotspot):
// export const API_URL = "http://192.168.X.X:8080";
| Entorno | URL |
|---|---|
| Android Studio emulator | http://10.0.2.2:8080 |
| iOS Simulator | http://localhost:8080 |
| Celular físico Android/iOS (mismo Wi-Fi) | http://<IP-LAN>:8080 |
| Celular físico (hotspot) | http://<IP-LAN-hotspot>:8080 |
| Web | http://localhost:8080 |
Si estás en red de escuela/oficina con AP isolation, usa hotspot del celular.
Pantallas
| Ruta | Archivo | Rol | Descripción |
|---|---|---|---|
/ |
index.tsx |
USER | Home: título + hero del camión + card ETA + banner estado de ruta + banner preventivo + 4 acciones rápidas + consejo |
/login |
login.tsx |
público | Login con email + password |
/register |
register.tsx |
público | Registro con nombre, email, password ≥ 6 caracteres |
/addresses |
addresses.tsx |
USER | Lista de colonias + selector + guardar (PUT /me) |
/alerts |
alerts.tsx |
USER | Hero + estado de ruta + timeline vertical con dots de color por severidad |
/guide |
guide.tsx |
USER | Calendario semanal + 4 cards (Orgánicos, Reciclables, Sanitarios, Especiales) + Recuerda |
/feedback |
feedback.tsx |
USER | Tipo de mensaje (4 opciones) + estrellas si es rating + textarea |
/profile |
profile.tsx |
USER | Avatar circular + cards Tu zona/Ruta + opciones (Mi domicilio, Buzón, Reiniciar demo, Ayuda, Cerrar sesión) + Tu impacto |
/admin |
admin.tsx |
ADMIN | Reportes del día + lista de rutas + cancelar/reanudar |
Estado global (AppContext)
src/context/AppContext.tsx
Proveedor único que expone:
interface AppContextValue {
user: AuthUser | null; // { id, name, email, role }
eta: EtaResult | null;
notifications: InboxNotification[];
route: RouteState | null;
loading: boolean;
toast: InboxNotification | null;
dismissToast: () => void;
login: (email, password) => Promise<void>;
register: (name, email, password) => Promise<void>;
logout: () => void;
refreshStatus: () => Promise<void>;
}
Polling
- Arranca cuando
user≠ null Yuser.role≠ "ADMIN" - Cada 30 segundos llama
GET /api/tracking/status - Limpia el interval cuando el user cierra sesión
- Detecta notificaciones nuevas (vía Set de IDs ya vistos) y dispara el
toast
Logout
- Borra token con
setAuthToken(null) - Resetea user, eta, route, notifications, toast
- Limpia el Set de IDs ya vistos
Comunicación con el backend
lib/api.ts
let authToken: string | null = null;
export const setAuthToken = (token: string | null) => { authToken = token; };
export const apiFetch = async <T>(path: string, options: RequestInit = {}): Promise<T> => {
const headers = { "Content-Type": "application/json", ...options.headers };
if (authToken) headers.Authorization = `Bearer ${authToken}`;
const res = await fetch(`${API_URL}${path}`, { ...options, headers });
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
return body as T;
};
El token se setea automáticamente tras login o register (dentro de auth.service.ts).
Services (capa fina sobre apiFetch)
- auth.service.ts — register, login, getMe
- tracking.service.ts — getMyStatus, sendGpsUpdate, resetDemo
- addresses.service.ts — listColonias, getMyAddress, setMyAddress
- feedback.service.ts — submitFeedback, listMyFeedback
- admin.service.ts — listAllRoutes, cancelRoute, resumeRoute
Componentes reutilizables
| Componente | Para qué |
|---|---|
EtaCard |
Card grande del ETA con "X min" gigante + ventana |
AlertItem (Alertltem.tsx) |
Item de notificación con icon + título + body + tiempo + badge por tipo |
InputField |
Wrap de TextInput con estilos del proyecto |
PrimaryButton |
Botón principal verde |
QuickAction |
Card cuadrada con icon circular + título (usada en home) |
SectionTitle |
Título grande de sección |
StatusBadge |
(no usado actualmente) |
CollectionCalendar |
Calendario mensual con colores por día según ruta del user |
NotificationToast |
Banner verde animado que cae cuando llega una notif nueva |
Roles y navegación
USER
- Ve las tabs: Inicio, Alertas, Guía, Perfil
- Polling activo
- Al loguearse, lo redirige a
/(Home) - Si entra a
/sin login → redirect a/login - Si entra a una pantalla protegida sin domicilio validado → algunas muestran CTA "Validar domicilio"
ADMIN
- Ve solo la tab: Admin
- Las demás tabs del user están ocultas (
href: null) - NO hace polling de
/status - Al loguearse desde
/loginaterriza en/y desde ahí redirect a/admin - Sus acciones (cancelar / reanudar) refrescan el listado cada 15 s
Notificaciones
En foreground (Toast in-app)
- Cuando llega una notificación nueva del polling, se setea
toasten el AppContext - El componente
NotificationToastanima un banner verde que cae desde arriba - Se autodescarta a los 5 segundos
- Funciona en cualquier pantalla porque vive en
_layout.tsx
Push del SO con app cerrada
- No funciona en Expo Go SDK 53+ — Expo removió el soporte
- El módulo
expo-notificationsestá instalado y el helperlib/notifications.tsestá listo - Para activarlo en producción → crear Development Build con EAS
Troubleshooting
| Síntoma | Causa | Fix |
|---|---|---|
Network request failed |
API_URL apunta a dirección inalcanzable desde el dispositivo |
Usa 10.0.2.2:8080 (emulador) o IP LAN (físico) |
Failed to fetch en navegador web |
CORS | Activar cors() middleware en el backend |
401 al cargar /status |
Token vencido o no enviado | Logout + login otra vez |
| "Cannot find module ../config/api.js" | Importaste con .js (estilo backend) |
En frontend, importa sin extensión: from "../config/api" |
Unable to resolve module |
Metro caché stale | npx expo start -c |
| App muestra "12 min / En Camino" hardcoded | Te perdiste de actualizar EtaCard con props del backend |
Verificar que home pasa minutes y status |
| Versión de SDK incompatible | Expo Go diferente al del proyecto (56) | Actualizar Expo Go en App Store / Play Store |
| Tab Admin no aparece | Login devolvió role ≠ "ADMIN" | Verifica logueado con admin@test.com / admin123 |
Para extender
- Nueva pantalla: crea archivo en
src/app/X.tsx. expo-router la detecta. Si no quieres que aparezca en el tab bar, agrégala conoptions={{ href: null }}en_layout.tsx. - Nuevo service: crea
services/X.service.tsque useapiFetch. Importa donde lo necesites. - Nuevo dato global: agrega al
AppContexty expón en el value. - Imágenes nuevas: ponlas en
assets/illustrations/y úsalas conrequire().
Frase de arquitectura para la exposición
"El frontend hace polling controlado cada 30 s al endpoint
/api/tracking/status. Esto evita sobrecargar el servidor y es predecible. El AppContext detecta notificaciones nuevas con un Set de IDs ya vistos y dispara un Toast in-app. Para push del SO con app cerrada migraríamos a Development Build y expo-notifications real, pero la decisión de Expo Go nos limita en esta etapa."