Documentação Técnica · Navegação Híbrida

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.

LeafletPreview + Matching
MapsNavegação Motorista
WazeAlternativa
JWTAuth Segura
01

Visão Geral

A regra de ouro da arquitetura híbrida

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.

Princípio central: o mapa interno (Leaflet) serve para pré-visualizar, calcular e mostrar informação. O mapa externo (Google Maps/Waze) serve apenas para o motorista navegar turn-by-turn — via deep link com as coordenadas já resolvidas.
03

🚗 App do Motorista

APIs e funções — aceitar, navegar, gerir

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 Google
Geocoding API (via Cloud Function) Converter coordenadas GPS em endereço legível Card "Localização atual" Google
Distance Matrix API (via Cloud Function) Não usada diretamente pelo app. Backend usa para matching Backend Google
Princípio do menor privilégio: o app do motorista nunca recebe CPF do passageiro, apenas o primeiro nome e o rating. A localização do passageiro é arredondada até ao momento do pickup.
04

📱 App do Passageiro

APIs e funções — pedir, escolher, acompanhar

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 Google
Geocoding API Converter localização atual (GPS) em endereço legível Card de origem Google
Directions API (via Cloud Function) Calcular rota + distância + tempo com trânsito → preço e ETA Confirmação de viagem Google
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 Google
Nota: o passageiro não abre Google Maps nem Waze. Toda a experiência acontece dentro do app, no Leaflet. O motorista é que navega externamente e a sua posição chega em tempo real via Firestore.
05

🔁 APIs Partilhadas

Invocadas apenas por Cloud Functions

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)
06

🔥 Firebase como Orquestrador

O cérebro por trás das APIs do Google
storageFirestore

Fonte única de verdade. Localizações, estado das viagens, cache de geocoding e directions.

drivers/{id} trips/{id}
boltCloud Functions

Backend serverless. Chama as APIs do Google, valida JWT e escreve no Firestore.

matchDriver calculateRoute
lockFirebase Auth

Gera o JWT após login. Cada chamada a Cloud Function verifica este token.

JWT RS256 custom claims
notificationsCloud Messaging

Notificações push com payload assinado. Nunca inclui dados sensíveis.

FCM tokens data-only
tuneRemote Config

Feature flags: app de navegação preferido, raio de busca, intervalo de update.

nav_app raio_km
analyticsAnalytics

Métricas anonimizadas. Nunca envia PII (CPF, celular, localização precisa).

api_usage nav_app_choice
07

🔒 Segurança de Dados do Utilizador

Motoristas, passageiros e PII
PII — qualquer dado que identifica ou pode identificar uma pessoa. No Federal Pro: nome, CPF, celular, email, localização precisa, foto, dados bancários.
Dado Classificação Onde guardar Encriptação Retenção
Nome completoPIIFirestoreTLS + at-restAtivo + 5 anos
CPFPII SensívelFirestore encriptadoAES-256-GCMAtivo + 5 anos
CelularPIIFirestore + AuthTLS + at-restAtivo
EmailPIIFirebase AuthHash + saltAtivo
Localização atualPII SensívelFirestore (TTL 90d)TLS90 dias
Histórico viagensInternoFirestoreTLS + at-rest2 anos
Dados bancáriosPCI-DSSNunca no FirestoreTokenizado (Stripe)Nunca
Foto perfilPIICloud Storage privadoSigned URLs (15min)Ativo
visibility_offMinimização

Recolhe apenas o necessário. Não peças CPF para agendar viagem. Não guardes localização exata depois de 90 dias.

lockAES-256 no CPF

CPF encriptado antes de chegar ao Firestore, com chave no Google Secret Manager.

badgeMascarar PII

Mostra só últimos 2 dígitos do CPF, últimos 4 do celular. O utilizador confirma, não lê.

historyAuditoria

Cada leitura de PII é registada em audit_logs com uid, action, timestamp, ip.

08

🔐 JWT & Autenticação

Login, cadastro e verificação em cada pedido

O JWT (JSON Web Token) é o mecanismo central. Cada pedido carrega um token que prova a identidade. O backend nunca confia no cliente.

JWT — estrutura do payload
// 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
}
timerID Token (JWT)

Duração: 1 hora. Guardado apenas em memória (nunca em disco). Autoriza pedidos às APIs.

refreshRefresh Token

Duração: 30 dias. Guardado no SecureStore (Keychain/Keystore). Obtém novo ID Token quando expira.

functions/middleware.ts — withAuth
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;
};
09

🌐 Segurança do Tráfego

HTTPS, pinning, rate limiting, WAF
enhanced_encryptionTLS 1.3

Todo o tráfego usa TLS 1.3. Proíbe HTTP no domínio.

verified_userCertificate Pinning

Fixa o certificado SSL para impedir MITM com certificados falsos.

speedRate Limiting

60/min para APIs normais, 5/min para login e pagamento.

shieldCORS rigoroso

Apenas domínios autorizados. Bloqueia * em produção.

fingerprintApp Check

Play Integrity / DeviceCheck valida que o pedido vem do teu app.

radarAnomalias

Login de IP diferente + muitas viagens em 1min → 2FA ou bloqueio.

🔒 Deep Links — Declaração Obrigatória

app.json — Expo configuration
{
  "expo": {
    "ios": {
      "infoPlist": {
        "LSApplicationQueriesSchemes": [
          "comgooglemaps",
          "waze"
        ]
      }
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "data": [{ "scheme": "https", "host": "federalpro.com.br" }],
          "category": ["BROWSABLE", "DEFAULT"]
        }

      ]
    }
  }
}
10

🛂 RBAC & Firestore Rules

Quem pode ler e escrever o quê
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
firestore.rules
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;
    }
  }
}
11

Otimização de Custos

Reduzir a fatura sem perder qualidade
mapLeaflet grátis para preview

Ao usar Leaflet para preview no motorista e tracking no passageiro, evitas 2 chamadas à Directions API por viagem.

Poupança: ~40% em Directions

navigationGoogle Maps/Waze grátis

A navegação real é responsabilidade do utilizador. Zero custo para ti.

Poupança: 100% de Navigation SDK

saveCache de Geocoding (30d)

Guarda no Firestore com hash das coordenadas arredondadas.

Poupança: ~70%

filter_altGeofire antes de Distance Matrix

Filtra por geohash primeiro. Envia só 10-20 candidatos.

Poupança: ~95%

hourglass_emptyDebounce no autocomplete

Só chama Places após 300ms sem digitar.

Poupança: ~80%

blockDesativar APIs não usadas

Não atives Maps SDK. Só Geocoding, Places, Directions, Distance Matrix.

12

Otimização de Desempenho

Latência baixa, app fluida
boltListeners vs polling

Firestore onSnapshot(). Latência ~100ms.

polylinePolyline lazy

Leaflet renderiza só pontos no viewport + Douglas-Peucker.

cloudRegião Cloud Functions

southamerica-east1. Latência 200ms → 30ms.

rocket_launchPreload paralelo

Promise.all() ao fazer login.

13

Experiência do Utilizador

Rápido, claro, seguro
flash_onFeedback <100ms

Micro-scale, foco animado, skeleton.

touch_appUm toque para navegar

Botão "Iniciar navegação" abre Google Maps/Waze com 1 toque. Sem copiar/colar coordenadas.

fingerprintBiometria

FaceID/impressão para pagamentos e mudança de senha.

errorErros amigáveis

"Sem internet" em vez de "Network request failed".

vibrationHaptics

Vibração ao receber viagem, aceitar, chegar.

dark_modeModo escuro

Motorista trabalha à noite. Reduz fadiga e poupa bateria.

14

Código Prático

Cloud Function matchDriver + openNavigation no cliente
src/screens/NewTripNotif.tsx — cliente
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 },
});
15

Resumo Geral

Tudo numa tabela
Componente Responsabilidade Onde App
Firebase AuthLogin + JWTBackendAmbos
FirestoreEstado + cacheBackendAmbos
Cloud FunctionsOrquestração + RBACBackendAmbos
Cloud MessagingPush notificationsBackendAmbos
expo-locationGPS do dispositivoClienteAmbos
LeafletPreview + trackingClienteMotorista + Passageiro
Google Maps / WazeNavegação turn-by-turnExternoSó Motorista
Geocoding APICoords ↔ endereçoCloud FunctionAmbos
Places APIAutocomplete + detailsCloud FunctionPassageiro (busca)
Directions APIRota + ETA + trânsitoCloud FunctionBackend
Distance MatrixETAs em lote (matching)Cloud FunctionBackend
Estratégia vencedora: o Leaflet interno faz todo o trabalho visual (preview, tracking, distância, tempo). O Google Maps/Waze só entra quando o motorista precisa de navegar de verdade — via deep link com 1 toque. Zero custo adicional, máxima qualidade de navegação, experiência consistente dentro da app.