📄 src/app/(app)/index.tsx

Auditoria de Código — 24 Problemas Encontrados

Clique em qualquer badge de status para alternar entre Pendente → Em Progresso → Corrigido. O progresso é salvo automaticamente no seu navegador.

🔥 Grave
🟠 UX / Estado
🟡 Menor / Qualidade
Progresso
0 / 24
Todos (24)
🔥 Graves (7)
🟠 UX / Estado (7)
🟡 Menores (10)
Pendentes
Em progresso
Corrigidos
01

A tarefa de background não faz nada útil 🔥 Grave

Pendente

A TaskManager.defineTask chamada BACKGROUND_LOCATION_TASK apenas imprime a latitude/longitude no console. Ela não envia nada para o backend nem atualiza o Firebase.

// ❌ Como está:
TaskManager.defineTask(BACKGROUND_LOCATION_TASK, ({ data, error }) => {
  if (data) {
    const { latitude, longitude } = data.locations[0].coords;
    console.log('[BG Location]', latitude, longitude); // só loga!
  }
});

Deveria chamar enviarLocalizacao() e updatePosition() assim como faz o efeito no foreground.

Impacto: passageiros nunca veem o motorista no mapa quando o app está minimizado.
02

A tarefa de background nunca é iniciada 🔥 Grave

Pendente

Não existe nenhuma chamada a Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, …) em nenhum lugar deste arquivo — nem no toggleOnline, nem em um useEffect.

// ❌ Falta isto em toggleOnline (quando next === true):
await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, {
  accuracy: Location.Accuracy.High,
  timeInterval: 10000,
  distanceInterval: 20,
  foregroundService: { notificationTitle: 'FederalPro', notificationBody: 'Você está online' },
});

// E isto ao ficar offline:
await Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK);

Definir a task não a inicia automaticamente. Sem isso, ela nunca roda.

Impacto: mesmo com todas as permissões concedidas, rastreamento em background não funciona.
03

perms.gpsServices nunca é inicializado corretamente 🔥 Grave

Pendente

O estado começa com gpsServices: false e canGoOnline exige perms.gpsServices === true. Como refreshPermissions() só roda quando menuVisible === true, o usuário vê o banner "Você não pode ficar online" mesmo tendo concedido tudo — até abrir o menu.

// ❌ Hoje:
useEffect(() => {
  if (menuVisible) refreshPermissions();
}, [menuVisible, refreshPermissions]);

// ✅ Deveria rodar também no mount:
useEffect(() => {
  refreshPermissions();
}, [refreshPermissions]);
Impacto: falso negativo visual — bloqueia o motorista sem motivo.
04

Efeito de auto-request não atualiza perms e ignora deps 🔥 Grave

Pendente

O efeito chama askLocation() / askBackgroundLocation(), mas nunca atualiza o estado perms depois. Além disso, faltam dependências (askLocation, askBackgroundLocation) — warning clássico do react-hooks/exhaustive-deps.

useEffect(() => {
  (async () => {
    const fg = await Location.getForegroundPermissionsAsync();
    if (fg.status !== 'granted') {
      await askLocation(); // ❌ não faz setPerms depois
      return;
    }
    ...
  })();
}, [refreshKey]); // ❌ deps incompletas
Impacto: UI mostra estado defasado das permissões até o usuário abrir o menu.
05

categoriaAtualMotorista usa só a primeira categoria 🔥 Grave

Pendente

O usuário pode marcar pro + confort + black, mas o listener só recebe a categoria da primeira posição do array. As outras são ignoradas no filtro de corridas.

const categoriaAtualMotorista = useMemo(() => {
  if (selectedCategorias.length === 0) return null;
  const primeira = categoriasDB.find((c) => Number(c.id) === selectedCategorias[0]);
  return normalizarSlug(primeira || CATEGORIAS_ESTATICAS[0]); // ❌ string única
}, [selectedCategorias, categoriasDB]);

Solução: fazer categoriaMotorista ser um array e ajustar useOuvirCorridas para filtrar por qualquer uma delas.

Impacto: motorista multi-categoria perde corridas de todas as categorias menos a primeira.
06

Seleção inicial de categorias ignora as estáticas 🔥 Grave

Pendente

Se a API de categorias falhar ou retornar vazio, o efeito desiste cedo e nenhuma categoria fica pré-selecionada — mesmo existindo as 3 estáticas (Federal Pet, Home Care, Mulher/Kids).

useEffect(() => {
  if (selectedCategorias.length > 0) return;
  if (categoriasDB.length === 0) return; // ❌ desiste antes de considerar as estáticas
  ...
}, [categoriasDB, selectedCategorias, user?.perfil, user?.categoria]);
Impacto: categoriaAtualMotorista fica null e o motorista não recebe corridas.
07

isOnline não persiste entre sessões 🔥 Grave

Pendente

Se o app fechar estando online, o Firebase mantém motoristas_online/{uid} ativo, mas a UI volta com isOnline = false. Estado divergente clássico.

Solução: ler o estado inicial via onValue(ref(db, 'motoristas_online/'+uid)) no mount, e/ou forçar goOffline() no boot quando o app inicia.

Impacto: motorista fantasma no mapa recebendo corridas que não vê.
08

persistirPreferencias dispara sem debounce 🟠 UX / Estado

Pendente

Cada toque em uma categoria, GPS ou biometria dispara um POST imediato. Trocar de categoria rapidamente gera N requisições desnecessárias.

useEffect(() => {
  if (!prefsLoaded) return;
  if (selectedCategorias.length === 0) return;
  persistirPreferencias({ gps: selectedGPS, biometria: biometriaEnabled, categorias_ids: ... });
}, [selectedGPS, biometriaEnabled, selectedCategorias, prefsLoaded, persistirPreferencias]);

Adicione um setTimeout de ~500ms no cleanup do efeito para agrupar mudanças.

09

Dois efeitos de som podem tocar juntos 🟠 UX / Estado

Pendente

Se corridaParaModal e mensagemParaModal aparecerem ao mesmo tempo, tocarSomNotificacao() toca 2x sobreposto.

useEffect(() => { if (corridaParaModal) tocarSomNotificacao(); }, [corridaParaModal]);
useEffect(() => { if (mensagemParaModal) tocarSomNotificacao(); }, [mensagemParaModal]);

Use um guard de tempo ("último som < 1s atrás, ignora") ou um único efeito combinado.

10

enviarLocalizacao roda mesmo offline 🟠 UX / Estado

Pendente

O backend recebe a localização em foreground mesmo quando o motorista está offline. Já o Firebase (updatePosition) só é atualizado se isOnline.

enviarLocalizacao(token, coords).catch(...); // ❌ sem if (isOnline)
if (isOnline) updatePosition(...);            // ✅ condicional

Se for heartbeat, documente. Se não, envolva no if (isOnline) para economizar banda.

11

handleSignOut engole erro de goOffline 🟠 UX / Estado

Pendente

Se goOffline falhar (rede ruim), o motorista fica "fantasma" online no Firebase permanentemente — e ainda por cima sem log.

try {
  if (user?.user_id) await goOffline(user.user_id);
} catch {} // ❌ engole erro silenciosamente
signOut();

Solução: logar, fazer retry, ou usar onDisconnect() do Firebase para limpar automaticamente.

12

toggleOnline engole erros sem feedback visual 🟠 UX / Estado

Pendente

Se o request expirar (8s de timeout), o usuário vê o botão voltar ao estado original sem nenhuma explicação. Nenhum Alert, nenhum toast.

catch (e: any) {
  console.log('[Home] toggleOnline erro:', e?.message ?? e); // só loga
}
Impacto: usuário acha que o app travou e fica clicando repetidamente.
13

Reset de status pode virar corrida com goOnline 🟠 UX / Estado

Pendente

Depois de goOnline, o código sobrescreve status: 'disponivel' via update direto. Se o backend já tiver setado outro status (ex.: aguardando_corrida), você o apaga.

await goOnline(...);
if (next) {
  await update(ref(db, `motoristas_online/${user.user_id}`), {
    status: 'disponivel', // ❌ sobrescreve qualquer status
    atualizadoEm: Date.now(),
  });
}

Prefira fazer esse reset dentro de goOnline ou via transação.

14

db pode ser nulo e não há guard 🟠 UX / Estado

Pendente

Se o Firebase não inicializou, ref(db, ...) estoura um erro obscuro.

import { db } from '@/services/firebase';
...
update(ref(db, `motoristas_online/${user.user_id}`), {...}) // ❌ sem guard

Adicione validação antes: if (!db) { Alert.alert('Firebase indisponível'); return; }

15

Diagnóstico do Firebase faz dynamic import desnecessário 🟡 Menor

Pendente

import('@/services/firebase') dentro de um useEffect é redundante — db já é importado estaticamente no topo do arquivo. Remova o efeito ou importe testarFirebase estaticamente.

16

accessibilityRole="switch" duplicado no Switch interno 🟡 Menor

Pendente

O Pressable externo já tem accessibilityRole="switch" e o Switch nativo dentro também anuncia como switch. VoiceOver/TalkBack lê duas vezes.

Escolha um: ou mantenha só o Pressable com role e use um View visual, ou remova o role do wrapper.

17

AnimatedPressable — hitSlop no wrapper, estilo no view interno 🟡 Menor

Pendente

Funciona, mas vale documentar: hitSlop e props de acessibilidade ficam no Pressable externo, enquanto style vai para o Animated.View. Pode confundir quem for dar manutenção.

18

Despacho de ícones por string é frágil 🟡 Menor

Pendente

getIconeCategoria retorna { lib: string, name, color } e o componente IconeCategoria faz uma cadeia de if (lib === ...). Mais simples: um mapa slug → componente direto.

19

focusable={false} em Switch nativo 🟡 Menor

Pendente

Switch do React Native não documenta focusable como prop. Algumas versões emitem warning. Remova ou use accessible={false}.

20

Recusa de categoria única é silenciosa 🟡 Menor

Pendente

Quando o usuário tenta desmarcar a última categoria, o código faz if (bloqueado) return; — nada visualmente acontece. Um toast do tipo "Mínimo de 1 categoria ativa" resolveria.

21

IDs negativos das estáticas não são persistidos 🟡 Menor

Pendente

CATEGORIAS_ESTATICAS usa IDs -1, -2, -3, mas o POST filtra id > 0. Se o usuário só tiver estáticas selecionadas, após reload ele fica sem categoria nenhuma.

categorias_ids: selectedCategorias.filter((id) => id > 0) // ❌ descarta estáticas

Solução: persistir também os slugs das estáticas num campo separado, ou usar IDs positivos fictícios reservados.

22

useOuvirCorridas declarado longe dos outros hooks 🟡 Menor

Pendente

Ele aparece depois de vários useEffect. Não quebra regras dos hooks, mas por convenção e para evitar surpresas futuras, agrupe todos os hooks de dados no topo do componente.

23

mensagemParaModal?.tipo || 'info' não valida strings vazias 🟡 Menor

Pendente

O fallback || 'info' cobre undefined, null e "", mas não valida se é um tipo reconhecido pelo modal (ex.: "erro" vs "error"). Considere um enum.

24

Handler de notificações sem cleanup 🟡 Menor

Pendente

configurarHandlerNotificacoes() é chamado uma vez sem retornar um unsubscribe. Em app-level geralmente é ok, mas se o handler registrar listeners globais, ideal retornar um cleanup para o caso de hot reload em dev.

useEffect(() => {
  try { configurarHandlerNotificacoes(); } catch (e) { ... }
}, []); // ❌ sem return de cleanup