# FASE 5 — App do morador ## Objetivo App Compose Multiplatform (Android + iOS) que **toca de verdade** com o app fechado, permite resolver entrega em um toque na notificação, e atende videochamada. ## Pré-requisitos FASES 0–4 concluídas. Ler `docs/04-DESIGN-SYSTEM-UX.md` §4. ## O ponto que define esta fase **No iOS, notificação comum não faz o telefone tocar como chamada.** Só PushKit + CallKit fazem. Sem isso o app não funciona como portaria — o morador vê a chamada quando abrir o celular, dez minutos depois, e o visitante já foi embora. E a Apple **obriga**: ao receber um push do PushKit, o app precisa reportar a chamada ao CallKit imediatamente, na mesma execução. Não reportar faz o sistema matar o app e, com reincidência, revogar o direito de receber VoIP push. Não há meio-termo aqui. ## Tarefas ### 1. Estrutura `apps/composeApp` com Compose Multiplatform, dependendo de `:shared`. Arquitetura MVVM com `ViewModel` compartilhado. Ktor Client para HTTP e WebSocket. Material 3 Expressive com o `Theme.kt` gerado dos tokens. Um único app com dois perfis (morador e operador), decididos por `GET /me`. ### 2. Notificação de chamada — Android FCM com prioridade `high`. Notificação `CallStyle` com full-screen intent para visita; notificação com ações para entrega. Permissões necessárias: `POST_NOTIFICATIONS` (13+), `USE_FULL_SCREEN_INTENT` (14+), `FOREGROUND_SERVICE_MICROPHONE` e `FOREGROUND_SERVICE_CAMERA`. **Otimização de bateria é o inimigo silencioso.** Xiaomi, Samsung e Huawei matam apps agressivamente e o push nunca chega. O app precisa detectar e orientar o usuário a isentá-lo — e o painel admin precisa mostrar quem está com push falhando (`05-INFRA-DOCKER.md` §6). ### 3. Notificação de chamada — iOS **Visita** → PushKit VoIP push → `CXProvider.reportNewIncomingCall()` **imediatamente**, antes de qualquer chamada de rede. Só depois busque os detalhes da visita. **Entrega** → notificação comum com `UNNotificationCategory` e ações. CallKit para uma entrega seria intrusivo e é risco de reprovação na App Review. Entitlement de VoIP configurado. Chave APNs `.p8` no backend. Áudio configurado via `AVAudioSession` com categoria `playAndRecord`. **Nota de App Review:** documente na submissão que o app é VoIP legítimo de controle de acesso. Descreva o fluxo. Apps que usam CallKit para notificação genérica são reprovados — este não é o caso, mas o revisor precisa entender por quê. ### 4. Ações de um toque O morador resolve **sem abrir o app**: - Android: `Notification.Action` com `PendingIntent` para um `BroadcastReceiver` - iOS: `UNNotificationAction` tratada na extensão Ambas chamam `POST /visits/{id}/resolve` com `Idempotency-Key` gerada no dispositivo. **Trate a falha de rede com retry**: a ação disparada em rede ruim que falha em silêncio é a pior falha possível deste app — o morador acha que autorizou e o entregador continua parado. ### 5. Telas **Home** — pendências (recados não resolvidos) no topo, histórico abaixo, estado de conexão visível. **Chamada** — vídeo do visitante em tela cheia, auto-preview no canto. Sobreposto: nome informado, unidade de destino, e o **selo de geofence** (`✓ Na entrada` verde / `⚠ A 340m da portaria` âmbar). Esse selo é a informação de segurança mais útil da tela — alguém acionando de longe é sinal claro. Ações: `Autorizar` (verde, primária), `Negar`, mudo, encerrar. **Detalhe da visita** — foto, mapa do ponto, timeline de `visit_attempts`, player do recado. **Configurações** — regra de entrega por unidade, ordem de toque, dispositivos ativos, notificações. ### 6. Registro de dispositivo `POST /devices` no login e a cada renovação de token, enviando `push_token` e — no iOS — também `voip_token`. Renove sempre que o sistema emitir novo token; token expirado é a causa número um de "não recebi a chamada". ### 7. Concorrência entre moradores Ao receber `409 VISIT_ALREADY_RESOLVED`, mostre **"Maria já autorizou"** com horário. Nunca um erro técnico — dois moradores respondendo juntos é situação normal, não falha. ### 8. Tempo real WebSocket enquanto o app está em foreground, com fallback para push. Ao voltar do background, reconcilia estado via REST antes de confiar no WS. ## Critérios de aceite - [ ] **iOS: chamada toca em tela cheia com o app fechado e o telefone bloqueado** - [ ] **Android: full-screen intent aparece com o app fechado e a tela apagada** - [ ] Entrega resolvida pela notificação, sem abrir o app, em ambas as plataformas - [ ] Teste com o app morto por otimização de bateria (Xiaomi/Samsung) - [ ] Videochamada funciona com o app vindo do background - [ ] `409` mostra quem já resolveu - [ ] Ação em rede ruim tem retry e não falha em silêncio - [ ] Registro de dispositivo renova o token corretamente - [ ] Testado em dispositivos físicos — simulador não recebe push - [ ] Acessibilidade: TalkBack e VoiceOver navegam a tela de chamada ## Não faça nesta fase - App do operador completo (FASE 7) — perfil de operador fica oculto - WhatsApp (FASE 8) - Autorizações recorrentes (v2)