# Guía Frontend — Integración con el Backend App: Expo Router + React Native + TypeScript. Backend: Express en `http://localhost:3000` (ya está listo, no hay que tocarlo). > Esta guía está pensada para probar en **emulador de Android**. > Si prueban en otro entorno, sólo cambien la `API_URL` (sección 1). --- ## 0. Endpoints del backend | Método | Endpoint | Para qué | |---|---|---| | POST | `/api/auth/register` | Crear cuenta | | POST | `/api/auth/login` | Iniciar sesión y obtener JWT | | GET | `/api/auth/me` | Datos del usuario (requiere token) | | POST | `/api/tracking/gps-update` | Recibe posición del camión, regresa ETA + notificaciones | --- ## 1. Configurar la URL del backend Crear `frontend/src/config/api.ts`: ```ts // IPs según el entorno: // Android Emulator → http://10.0.2.2:3000 (default aquí) // iOS simulator → http://localhost:3000 // Dispositivo real → http://:3000 (ej. 192.168.1.20) // Expo Go web → http://localhost:3000 export const API_URL = "http://10.0.2.2:3000"; ``` > `10.0.2.2` es la dirección especial del emulador de Android para alcanzar el `localhost` de la laptop. No usen `localhost` desde el emulador, no funciona. > Si prueban en un celular físico, sáquen la IP de la laptop con `ipconfig` (Windows) y pongan algo como `http://192.168.1.20:3000`. La laptop y el celular tienen que estar en el **mismo Wi-Fi**. --- ## 2. Cliente HTTP simple Crear `frontend/src/lib/api.ts`: ```ts import { API_URL } from "../config/api"; let authToken: string | null = null; export const setAuthToken = (token: string | null) => { authToken = token; }; export const apiFetch = async ( path: string, options: RequestInit = {}, ): Promise => { const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; 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; }; ``` --- ## 3. Servicios por feature ### `frontend/src/services/auth.service.ts` ```ts import { apiFetch, setAuthToken } from "../lib/api"; export interface AuthUser { id: number; email: string; name: string; } export const register = (data: { name: string; email: string; password: string }) => apiFetch<{ id: number; email: string; name: string }>("/api/auth/register", { method: "POST", body: JSON.stringify(data), }); export const login = async (data: { email: string; password: string }) => { const res = await apiFetch<{ user: AuthUser; token: string }>("/api/auth/login", { method: "POST", body: JSON.stringify(data), }); setAuthToken(res.token); return res; }; export const getMe = () => apiFetch("/api/auth/me"); ``` ### `frontend/src/services/tracking.service.ts` ```ts import { apiFetch } from "../lib/api"; export type NotificationType = | "ROUTE_START" | "TRUCK_PROXIMITY" | "ROUTE_COMPLETED" | "DELAY" | "MECHANICAL_FAILURE"; export interface GpsUpdatePayload { truckId: string; routeId: string; lat: number; lng: number; speed: number; status: "EN_RUTA" | "DETENIDO" | "FINALIZADO" | "FALLA"; positionId?: number; timestamp?: string; } export interface EtaResult { etaMinutes: number; arrivalWindow: { from: string; to: string }; message: string; } export interface BackendNotification { userId: number; type: NotificationType; title: string; body: string; } export interface GpsUpdateResponse { message: string; truck: { truckId: number; routeId: string; status: string }; eta: EtaResult; notifications: BackendNotification[]; } export const sendGpsUpdate = (payload: GpsUpdatePayload) => apiFetch("/api/tracking/gps-update", { method: "POST", body: JSON.stringify(payload), }); ``` --- ## 4. Mapear los tipos del backend al componente `AlertItem` `AlertItem` ya acepta `started | near | danger | completed`. Crear `frontend/src/lib/notification-mapper.ts`: ```ts import type { NotificationType } from "../services/tracking.service"; export const notificationTypeToAlertType = ( type: NotificationType, ): "started" | "near" | "danger" | "completed" => { switch (type) { case "ROUTE_START": return "started"; case "TRUCK_PROXIMITY": return "near"; case "ROUTE_COMPLETED": return "completed"; case "DELAY": return "danger"; case "MECHANICAL_FAILURE": return "danger"; } }; ``` --- ## 5. Pantalla de Login (`src/app/login.tsx`) ```tsx import { useState } from "react"; import { View, Text, StyleSheet, Alert } from "react-native"; import { useRouter } from "expo-router"; import InputField from "../components/InputField"; import PrimaryButton from "../components/PrimaryButton"; import { login } from "../services/auth.service"; export default function LoginScreen() { const [email, setEmail] = useState("ana@test.com"); const [password, setPassword] = useState("123456"); const [loading, setLoading] = useState(false); const router = useRouter(); const handleLogin = async () => { try { setLoading(true); await login({ email, password }); router.replace("/"); } catch (err) { Alert.alert("Error", err instanceof Error ? err.message : "Login failed"); } finally { setLoading(false); } }; return ( Iniciar sesión ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 24, justifyContent: "center" }, title: { fontSize: 24, fontWeight: "bold", marginBottom: 20 }, }); ``` --- ## 6. Pantalla principal con ETA en tiempo real (`src/app/index.tsx`) ```tsx import { useEffect, useState } from "react"; import { ScrollView, RefreshControl, View } from "react-native"; import EtaCard from "../components/EtaCard"; import { sendGpsUpdate, type GpsUpdateResponse, type BackendNotification, } from "../services/tracking.service"; // Para la demo: el frontend simula el avance del camión. // En producción esto vendría de un GPS real → backend → app vía websockets/polling. const SIMULATION_STEPS = [ { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:00:00Z" }, { positionId: 2, lat: 20.5185, lng: -100.8450, speed: 45, timestamp: "2026-05-22T06:12:00Z" }, { positionId: 3, lat: 20.5215, lng: -100.8142, speed: 22, timestamp: "2026-05-22T06:25:00Z" }, { positionId: 4, lat: 20.5212, lng: -100.8175, speed: 15, timestamp: "2026-05-22T06:38:00Z" }, { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 40, timestamp: "2026-05-22T07:40:00Z" }, ]; export default function HomeScreen() { const [stepIndex, setStepIndex] = useState(0); const [data, setData] = useState(null); const [notifications, setNotifications] = useState([]); const fetchNext = async () => { const step = SIMULATION_STEPS[stepIndex % SIMULATION_STEPS.length]; try { const res = await sendGpsUpdate({ truckId: "101", routeId: "RUTA-01", status: "EN_RUTA", ...step, }); setData(res); setNotifications((prev) => [...res.notifications, ...prev]); setStepIndex((i) => i + 1); } catch (err) { console.error(err); } }; useEffect(() => { fetchNext(); }, []); return ( }> {/* Mapear notifications a aquí si quieren mostrarlas en la home también */} ); } ``` > **Truco del demo:** cada "pull to refresh" avanza el camión un paso. Sin tocar el backend, pueden mostrar las 5 fases del recorrido. --- ## 7. Pantalla de Alertas (`src/app/alerts.tsx`) ```tsx import { FlatList, View } from "react-native"; import AlertItem from "../components/Alertltem"; import { notificationTypeToAlertType } from "../lib/notification-mapper"; import type { BackendNotification } from "../services/tracking.service"; type Props = { notifications: BackendNotification[] }; export default function AlertsScreen({ notifications }: Props) { return ( String(i)} renderItem={({ item }) => ( )} /> ); } ``` > **Compartir notificaciones entre pantallas:** lo más simple para el demo es subirlas a un `Context` o Zustand. Si tienen tiempo. Para hackathon, pueden duplicar la lógica o pasarlas por params. --- ## 8. Cómo probar el flujo completo en el emulador de Android ### Pre-requisitos - Backend corriendo: `npm run dev` dentro de `/backend`. Debe imprimir `Server is running on port 3000`. - Android Studio con AVD (emulador) abierto. - Frontend con dependencias instaladas: `npm install` dentro de `/frontend`. ### Pasos 1. **Levantar el frontend:** ```powershell cd frontend npm run android ``` Eso abre Expo y lanza la app en el emulador. 2. **Crear un usuario de prueba** (sólo la primera vez). Desde Postman: ``` POST http://localhost:3000/api/auth/register Content-Type: application/json { "name": "Ana López", "email": "ana@test.com", "password": "123456" } ``` (Ana ya está en el mock con `routeId: "RUTA-01"`, así que recibirá las notificaciones del camión 101.) 3. **En la app del emulador**, iniciar sesión con: - Email: `ana@test.com` - Password: `123456` 4. **En la pantalla principal**, hacer **pull-to-refresh** repetidamente. Cada pull avanza el camión: | Paso | positionId | Notificación esperada | |---|---|---| | 1 | 1 | (ninguna) | | 2 | 2 | "¡Ruta Iniciada!" | | 3 | 3 | (ninguna) | | 4 | 4 | "Camión Cercano" | | 5 | 8 | "Servicio Finalizado" | 5. **Pantalla de alertas** debe mostrar las notificaciones acumuladas con los iconos y colores correctos. --- ## 9. Problemas comunes en Android emulator | Síntoma | Causa | Solución | |---|---|---| | `Network request failed` | Apunta a `localhost` | Usar `http://10.0.2.2:3000` en `API_URL` | | `TypeError: Failed to fetch` (en web) | Falta CORS | Avisar al del backend para que active `cors()` | | 401 al hacer login | Usuario no existe | Registrar primero con Postman (sección 8, paso 2) | | `notifications: []` aunque cambies positionId | El cache ya envió esa noti antes | Reiniciar el backend (`Ctrl+C` y `npm run dev`) | | ETA negativo o raro | El `timestamp` que envías está atrás del target | Es normal, el mensaje sale como "El camión ya pasó..." | --- ## 10. Checklist mínimo - [ ] `src/config/api.ts` con `http://10.0.2.2:3000` - [ ] `src/lib/api.ts` (fetch wrapper) - [ ] `src/services/auth.service.ts` - [ ] `src/services/tracking.service.ts` - [ ] `src/lib/notification-mapper.ts` - [ ] Pantalla de login funcionando con `ana@test.com` - [ ] Pantalla principal mostrando `EtaCard` con datos reales - [ ] Pantalla de alertas mostrando `AlertItem` con notificaciones reales - [ ] Probar las 5 fases del recorrido con pull-to-refresh --- ## 11. Lo que NO necesitan hacer - ❌ Push notifications reales (Firebase / Expo Notifications) — sólo lista in-app - ❌ Mapas con GPS real — los datos vienen del mock del backend - ❌ AsyncStorage para el token — para el demo basta con estado en memoria - ❌ Modificar nada del `/backend`