12 KiB
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:
// IPs según el entorno:
// Android Emulator → http://10.0.2.2:3000 (default aquí)
// iOS simulator → http://localhost:3000
// Dispositivo real → http://<IP-de-tu-laptop>: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.2es la dirección especial del emulador de Android para alcanzar ellocalhostde la laptop. No usenlocalhostdesde el emulador, no funciona.
Si prueban en un celular físico, sáquen la IP de la laptop con
ipconfig(Windows) y pongan algo comohttp://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:
import { API_URL } from "../config/api";
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: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
};
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
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<AuthUser & { role: string }>("/api/auth/me");
frontend/src/services/tracking.service.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<GpsUpdateResponse>("/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:
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)
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 (
<View style={styles.container}>
<Text style={styles.title}>Iniciar sesión</Text>
<InputField placeholder="Email" value={email} onChangeText={setEmail} />
<InputField placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
<PrimaryButton title={loading ? "Cargando..." : "Entrar"} onPress={handleLogin} />
</View>
);
}
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)
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<GpsUpdateResponse | null>(null);
const [notifications, setNotifications] = useState<BackendNotification[]>([]);
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 (
<ScrollView refreshControl={<RefreshControl refreshing={false} onRefresh={fetchNext} />}>
<EtaCard
minutes={data?.eta.etaMinutes ?? 0}
status={data?.eta.message ?? "Cargando..."}
/>
<View>
{/* Mapear notifications a <AlertItem /> aquí si quieren mostrarlas en la home también */}
</View>
</ScrollView>
);
}
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)
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 (
<View style={{ flex: 1, padding: 16 }}>
<FlatList
data={notifications}
keyExtractor={(_, i) => String(i)}
renderItem={({ item }) => (
<AlertItem
title={item.title}
description={item.body}
time="ahora"
type={notificationTypeToAlertType(item.type)}
/>
)}
/>
</View>
);
}
Compartir notificaciones entre pantallas: lo más simple para el demo es subirlas a un
Contexto 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 devdentro de/backend. Debe imprimirServer is running on port 3000. - Android Studio con AVD (emulador) abierto.
- Frontend con dependencias instaladas:
npm installdentro de/frontend.
Pasos
-
Levantar el frontend:
cd frontend npm run androidEso abre Expo y lanza la app en el emulador.
-
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.) -
En la app del emulador, iniciar sesión con:
- Email:
ana@test.com - Password:
123456
- Email:
-
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" -
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.tsconhttp://10.0.2.2:3000src/lib/api.ts(fetch wrapper)src/services/auth.service.tssrc/services/tracking.service.tssrc/lib/notification-mapper.ts- Pantalla de login funcionando con
ana@test.com - Pantalla principal mostrando
EtaCardcon datos reales - Pantalla de alertas mostrando
AlertItemcon 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