Leaflet + Google Maps / Waze
Arquitetura Híbrida para Federal Pro
Leaflet desenha rotas, calcula distância e tempo dentro da app — usado nas notificações de solicitação de viagens e entregas. Google Maps ou Waze assumem a navegação real turn-by-turn do motorista. O passageiro acompanha tudo em tempo real no mapa interno.
Visão Geral
O cliente nunca chama o Google diretamente. Os apps do motorista e do passageiro são interfaces finas que consomem dados do Firebase (Firestore + Cloud Functions), que por sua vez orquestram as chamadas às APIs do Google. Isto protege a chave, reduz custos e centraliza a segurança.
🚗 App do Motorista
O motorista precisa de ferramentas para aceitar viagens, navegar e gerir o seu estado. O foco está em rotas, localização em tempo real e notificações.
| API / Serviço | Função | Onde é usada | Tipo |
|---|---|---|---|
| Firebase Auth | Login + JWT com claims role: 'driver', categoria, status: 'aprovado' |
Ecrã de login / arranque | Firebase |
| Firestore | Listener onSnapshot em drivers/{uid} para receber atualizações de estado |
Ecrã principal (online/offline) | Firebase |
| Cloud Messaging (FCM) | Notificações push de novas viagens (raio 5 km + countdown 15s) | Segundo plano / notificações | Firebase |
| Cloud Functions | acceptTrip, rejectTrip, updatePosition |
Botões de aceitar/rejeitar | Firebase |
| expo-location | Enviar localização para o Firestore (com geohash) a cada 3–5s | Background quando online | Grátis |
| Leaflet (interno) | Preview da rota na notificação: polyline + distância + tempo | Ecrã de nova viagem | Leaflet |
| Google Maps / Waze (deep link) | Navegação turn-by-turn real após aceitar | Botão "Iniciar navegação" | Externo |
| Directions API (via Cloud Function) | Cálculo de rota + distância + ETA + duration_in_traffic |
Backend, antes da notificação | |
| Geocoding API (via Cloud Function) | Converter coordenadas GPS em endereço legível | Card "Localização atual" | |
| Distance Matrix API (via Cloud Function) | Não usada diretamente pelo app. Backend usa para matching | Backend |
📱 App do Passageiro
O passageiro precisa de pedir viagens, escolher destinos e acompanhar o motorista. O foco está em pesquisa de endereços, estimativas de preço/ETA e tracking em tempo real.
| API / Serviço | Função | Onde é usada | Tipo |
|---|---|---|---|
| Firebase Auth | Login + JWT com role: 'passenger' |
Ecrã de login / arranque | Firebase |
| Firestore | Listener em trips/{tripId} e match_requests/{uid} |
Ecrã de acompanhamento | Firebase |
| Cloud Messaging (FCM) | Notificações "motorista a chegar", "viagem aceite", "concluída" | Segundo plano | Firebase |
| Cloud Functions | createTripRequest, cancelTrip, rateDriver |
Botão "Pedir viagem" | Firebase |
| Places API (Autocomplete + Details) | Sugerir endereços + obter coordenadas do local escolhido | Ecrã de busca de destino | |
| Geocoding API | Converter localização atual (GPS) em endereço legível | Card de origem | |
| Directions API (via Cloud Function) | Calcular rota + distância + tempo com trânsito → preço e ETA | Confirmação de viagem | |
| Leaflet (interno) | Acompanhar motorista em tempo real no mapa da app | Ecrã de tracking da viagem | Leaflet |
| Distance Matrix API (via Cloud Function) | Não usada diretamente. Backend usa para matching | Backend |
🔁 APIs Partilhadas
Estas APIs são exclusivamente invocadas por Cloud Functions, nunca pelo cliente. Isto garante segurança e eficiência de custos.
| API | Função no Backend | Beneficia |
|---|---|---|
| Distance Matrix API | ETA de múltiplos motoristas (origens) até ao passageiro (destino) numa única chamada, após filtragem por geohash | Matching (ambos os apps) |
| Directions API | Rota completa com departure_time=now para obter duration_in_traffic |
Motorista (navegação) + Passageiro (preço/ETA) |
| Geocoding API | Reverse (coords → endereço) e forward (endereço → coords) | Ambos |
| Places API | Autocomplete e Place Details | Passageiro (busca) + Motorista (destino) |
🔥 Firebase como Orquestrador
Fonte única de verdade. Localizações, estado das viagens, cache de geocoding e directions.
Backend serverless. Chama as APIs do Google, valida JWT e escreve no Firestore.
Gera o JWT após login. Cada chamada a Cloud Function verifica este token.
Notificações push com payload assinado. Nunca inclui dados sensíveis.
Feature flags: app de navegação preferido, raio de busca, intervalo de update.
Métricas anonimizadas. Nunca envia PII (CPF, celular, localização precisa).
🔒 Segurança de Dados do Utilizador
| Dado | Classificação | Onde guardar | Encriptação | Retenção |
|---|---|---|---|---|
| Nome completo | PII | Firestore | TLS + at-rest | Ativo + 5 anos |
| CPF | PII Sensível | Firestore encriptado | AES-256-GCM | Ativo + 5 anos |
| Celular | PII | Firestore + Auth | TLS + at-rest | Ativo |
| PII | Firebase Auth | Hash + salt | Ativo | |
| Localização atual | PII Sensível | Firestore (TTL 90d) | TLS | 90 dias |
| Histórico viagens | Interno | Firestore | TLS + at-rest | 2 anos |
| Dados bancários | PCI-DSS | Nunca no Firestore | Tokenizado (Stripe) | Nunca |
| Foto perfil | PII | Cloud Storage privado | Signed URLs (15min) | Ativo |
Recolhe apenas o necessário. Não peças CPF para agendar viagem. Não guardes localização exata depois de 90 dias.
CPF encriptado antes de chegar ao Firestore, com chave no Google Secret Manager.
Mostra só últimos 2 dígitos do CPF, últimos 4 do celular. O utilizador confirma, não lê.
Cada leitura de PII é registada em audit_logs com uid, action, timestamp, ip.
🔐 JWT & Autenticação
O JWT (JSON Web Token) é o mecanismo central. Cada pedido carrega um token que prova a identidade. O backend nunca confia no cliente.
// Header { "alg": "RS256", "typ": "JWT", "kid": "firebase-key-1" } // Payload (claims) { "iss": "https://securetoken.google.com/federal-pro", "aud": "federal-pro", "user_id": "abc123uid", "sub": "abc123uid", "iat": 1790000000, "exp": 1790003600, // 1 hora "email": "motorista@exemplo.com", "phone_number": "+5511999998888", "role": "driver", // custom claim "categoria": "pro", // custom claim "status": "aprovado", // custom claim "nav_app": "waze" // preferência de navegação }
Duração: 1 hora. Guardado apenas em memória (nunca em disco). Autoriza pedidos às APIs.
Duração: 30 dias. Guardado no SecureStore (Keychain/Keystore). Obtém novo ID Token quando expira.
import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; export function withAuth( handler: (data: any, ctx: AuthContext) => Promise<any>, requiredRole?: 'driver' | 'passenger' | 'admin' ) { return functions.https.onCall(async (data, context) => { if (!context.auth) { throw new functions.https.HttpsError( 'unauthenticated', 'JWT ausente ou inválido' ); } const { uid, token } = context.auth; // Verifica se a conta está aprovada if (token.status !== 'aprovado') { throw new functions.https.HttpsError( 'permission-denied', 'Conta pendente de aprovação' ); } // Verifica role (RBAC) if (requiredRole && token.role !== requiredRole) { throw new functions.https.HttpsError( 'permission-denied', `Requer role '${requiredRole}'` ); } // Rate limiting const allowed = await checkRateLimit(uid, 60, '1m'); if (!allowed) { throw new functions.https.HttpsError('resource-exhausted', 'Demasiados pedidos'); } // Auditoria await admin.firestore().collection('audit_logs').add({ uid, action: 'call', fn: handler.name, timestamp: admin.firestore.FieldValue.serverTimestamp(), }); return handler(data, { uid, role: token.role, claims: token }); }); } type AuthContext = { uid: string; role: 'driver' | 'passenger' | 'admin'; claims: any; };
🌐 Segurança do Tráfego
Todo o tráfego usa TLS 1.3. Proíbe HTTP no domínio.
Fixa o certificado SSL para impedir MITM com certificados falsos.
60/min para APIs normais, 5/min para login e pagamento.
Apenas domínios autorizados. Bloqueia * em produção.
Play Integrity / DeviceCheck valida que o pedido vem do teu app.
Login de IP diferente + muitas viagens em 1min → 2FA ou bloqueio.
🔒 Deep Links — Declaração Obrigatória
{
"expo": {
"ios": {
"infoPlist": {
"LSApplicationQueriesSchemes": [
"comgooglemaps",
"waze"
]
}
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"data": [{ "scheme": "https", "host": "federalpro.com.br" }],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}
🛂 RBAC & Firestore Rules
| Role | Pode ler | Pode escrever | Endpoints |
|---|---|---|---|
| passenger | Próprio perfil, viagens onde é participante | Próprio perfil, criar pedidos | createTripRequest, getRoute |
| driver | Próprio perfil, viagens atribuídas, dados mínimos do passageiro | Próprio perfil, localização, aceitar/rejeitar | matchDriver, acceptTrip, updatePosition |
| admin | Tudo, dentro do âmbito operacional | Promover utilizadores, alterar tarifas, bloquear | promoteToDriver, blockUser |
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function isSignedIn() { return request.auth != null; } function isOwner(uid) { return request.auth.uid == uid; } function isRole(role) { return request.auth.token.role == role; } function isApproved() { return request.auth.token.status == 'aprovado'; } function isParticipant() { return request.auth.uid in resource.data.participants; } match /users/{uid} { allow read: if isOwner(uid) || isRole('admin'); allow update: if isOwner(uid) && !request.resource.data.diff(resource.data).affectedKeys() .hasAny(['role', 'status', 'cpf_encrypted']); } match /drivers/{driverId} { allow write: if isOwner(driverId) && isRole('driver') && isApproved(); allow read: if isSignedIn(); } match /trips/{tripId} { allow read: if isParticipant() || isRole('admin'); allow create: if isSignedIn() && request.resource.data.participants.hasAll([request.auth.uid]); allow update: if isParticipant(); } match /match_requests/{uid} { allow read: if isOwner(uid); allow write: if false; // só Cloud Functions } match /{document=**} { allow read, write: if false; } } }
Otimização de Custos
Ao usar Leaflet para preview no motorista e tracking no passageiro, evitas 2 chamadas à Directions API por viagem.
Poupança: ~40% em Directions
A navegação real é responsabilidade do utilizador. Zero custo para ti.
Poupança: 100% de Navigation SDK
Guarda no Firestore com hash das coordenadas arredondadas.
Poupança: ~70%
Filtra por geohash primeiro. Envia só 10-20 candidatos.
Poupança: ~95%
Só chama Places após 300ms sem digitar.
Poupança: ~80%
Não atives Maps SDK. Só Geocoding, Places, Directions, Distance Matrix.
Otimização de Desempenho
Firestore onSnapshot(). Latência ~100ms.
Leaflet renderiza só pontos no viewport + Douglas-Peucker.
southamerica-east1. Latência 200ms → 30ms.
Promise.all() ao fazer login.
Experiência do Utilizador
Micro-scale, foco animado, skeleton.
Botão "Iniciar navegação" abre Google Maps/Waze com 1 toque. Sem copiar/colar coordenadas.
FaceID/impressão para pagamentos e mudança de senha.
"Sem internet" em vez de "Network request failed".
Vibração ao receber viagem, aceitar, chegar.
Motorista trabalha à noite. Reduz fadiga e poupa bateria.
Código Prático
import { useState, useEffect } from 'react'; import { View, Text, Pressable, StyleSheet } from 'react-native'; import { RoutePreview } from '@/components/RoutePreview'; import { openNavigation } from '@/services/navigation/deepLinks'; import { acceptTrip } from '@/services/trips'; import { useAuth } from '@/auth/AuthContext'; export function NewTripNotification({ trip, onClose }: Props) { const { user } = useAuth(); const [countdown, setCountdown] = useState(15); const [accepting, setAccepting] = useState(false); // Countdown para aceitar useEffect(() => { if (countdown <= 0) { onClose(); return; } const t = setTimeout(() => setCountdown((c) => c - 1), 1000); return () => clearTimeout(t); }, [countdown]); async function handleAccept() { setAccepting(true); await acceptTrip(trip.id); // Abre Google Maps ou Waze com 1 toque await openNavigation( { lat: trip.pickupLat, lng: trip.pickupLng }, { lat: trip.currentLat, lng: trip.currentLng }, user?.navApp ?? 'google_maps' ); onClose(); } return ( <View style={s.container}> <View style={s.header}> <Text style={s.title}>Nova viagem!</Text> <Text style={s.countdown}>{countdown}s</Text> </View> {/* Leaflet preview com rota, distância e tempo */} <View style={s.mapWrap}> <RoutePreview origin={{ lat: trip.currentLat, lng: trip.currentLng }} destination={{ lat: trip.pickupLat, lng: trip.pickupLng }} polyline={trip.polyline} distanceKm={trip.distanceKm} durationMin={trip.durationMin} trafficLevel={trip.trafficLevel} /> </View> <Text style={s.passenger}> {trip.passengerName} • ★ {trip.passengerRating.toFixed(2)} </Text> <View style={s.actions}> <Pressable style={[s.btn, s.reject]} onPress={onClose}> <Text style={s.btnText}>Rejeitar</Text> </Pressable> <Pressable style={[s.btn, s.accept, accepting && { opacity: 0.6 }]} onPress={handleAccept} disabled={accepting} > <Text style={s.btnText}>Aceitar e Navegar</Text> </Pressable> </View> </View> ); } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: '#0F172A', padding: 20 }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }, title: { color: '#FFF', fontSize: 20, fontWeight: '800' }, countdown: { color: '#F59E0B', fontSize: 18, fontWeight: '800' }, mapWrap: { flex: 1, borderRadius: 16, overflow: 'hidden' }, passenger: { color: '#94A3B8', marginTop: 12, fontSize: 14 }, actions: { flexDirection: 'row', gap: 12, marginTop: 16 }, btn: { flex: 1, paddingVertical: 16, borderRadius: 12, alignItems: 'center' }, accept: { backgroundColor: '#22C55E' }, reject: { backgroundColor: '#334155' }, btnText: { color: '#FFF', fontWeight: '800', fontSize: 15 }, });
Resumo Geral
| Componente | Responsabilidade | Onde | App |
|---|---|---|---|
| Firebase Auth | Login + JWT | Backend | Ambos |
| Firestore | Estado + cache | Backend | Ambos |
| Cloud Functions | Orquestração + RBAC | Backend | Ambos |
| Cloud Messaging | Push notifications | Backend | Ambos |
| expo-location | GPS do dispositivo | Cliente | Ambos |
| Leaflet | Preview + tracking | Cliente | Motorista + Passageiro |
| Google Maps / Waze | Navegação turn-by-turn | Externo | Só Motorista |
| Geocoding API | Coords ↔ endereço | Cloud Function | Ambos |
| Places API | Autocomplete + details | Cloud Function | Passageiro (busca) |
| Directions API | Rota + ETA + trânsito | Cloud Function | Backend |
| Distance Matrix | ETAs em lote (matching) | Cloud Function | Backend |