Files
sf-app-platform/docs/videocall-callkit-migration.md
JulianAlcala abdbc2bf2e release: 1.0.0+6 — chat notifications + videocall refactor
Chat notifications (production-ready)
- ChatDeeplinkService: resolves client chat ID from incoming push/WS payloads using the (senderId, chatId) matrix and switches selectedDeviceProvider when the payload references a different watch (multi-device deeplink)
- IncomingChatResolver in domain layer with full unit coverage of the 4-case matrix
- ChatContext provider (sealed: outsideChat / list / conversation) wired into ChatListScreen and ChatConversationScreen via initState/dispose to enable WhatsApp-style suppression
- notifications_init refactor: foreground CHAT_MESSAGE notifications are suppressed on chat list and matching conversation; tap navigation goes through ChatDeeplinkService
- ChatSyncService.subscribeToReconnect: reconciles from REST when the WebSocket comes back from a disconnect (recovers messages missed in background)
- 5s message-id dedup window in the WS listener (mitigates server-side duplicate chat-message-received events)
- Reconcile from remote on conversation mount (covers cached controller from background)
- AppBar refresh button + inverted pull-to-refresh in the conversation (overscroll either edge of the reverse list)

WS event parser fix
- chat-message-received normalises to chat_message_received; parser now accepts both that and chat_message so the conversation reactively refreshes when a watch sends a message

Chat application layer
- Split the conversation controller into services: chat_send_service, chat_sync_service, chat_participants_service, chat_permission_flow_service, chat_media_cleanup_service
- chat_conversation_config centralises page size, polling interval, dedup window
- chat_bubble_shell extracted; input bar split into smaller widgets
- emoji picker sheet + emoji blocking input formatter + watch_emoji_catalog
- Multipart upload header race fixed via synchronized.Lock around the shared Dio instance

Videocall (carryover from earlier work in this branch)
- Application services: incoming, outgoing, session
- Domain entities: VideocallIncomingArgs, VideocallRoom, VideocallUserId, parseDeviceIdFromRoom helper
- Views split: idle, incoming, active call, group call
- Widgets: picture_in_picture_video, remote_or_fallback_video, video_call_header
- videocall_config centralises timeouts, ringing duration, battery threshold
- Incoming via push (channel mode) with full-screen notification + ringtone
- Hangup-on-remote-left moved to controller; redirect on participant update documented
- treezor_token_interceptor: distinguish session expiry from operation-denied 401s

Localization & misc
- New keys for chat conversation, refresh, errors across en/es/de/fr/it/pt
- Location map: dispose ref-after-unmount fix; route history layer cleanup
- Legacy device view model: position update event handling
- AndroidManifest: notification channel + permissions for incoming-call full-screen intent
2026-05-08 07:20:40 -05:00

5.3 KiB
Raw Blame History

Videocall — Migración a CallKit / Notification.CallStyle nativo

Contexto

Hoy (2026-05-08) las videollamadas entrantes con la app killed se manejan via flutter_local_notifications con fullScreenIntent: true y AndroidNotificationCategory.call. El usuario ve dos botones en la notificación (Aceptar / Rechazar) construidos como AndroidNotificationAction. Ver apps/mobile_app/lib/core/notifications_init.dart.

Problema conocido

Doble tap para aceptar.

Cuando el usuario toca "Aceptar" en la notificación, el flujo es:

notif "Aceptar"  →  app abre  →  IncomingView (otro Aceptar)  →  llamada
   tap 1                                       tap 2

WhatsApp / Telegram / Zoom entran directo a la llamada activa con un solo tap. Eso es lo esperado por el usuario.

La causa estructural: flutter_local_notifications solo nos pasa el actionId al handler. No hay integración con la API de telefonía del sistema operativo. Para tener tap único nativo necesitamos:

  • iOS: CallKit + PushKit + VoIP cert (cert es el bloqueador hoy).
  • Android: Notification.CallStyle (API 31+) + Telecom/ConnectionService.

Opciones evaluadas

Solución iOS Android Costo Doble-tap
flutter_local_notifications (actual) banner básico full-screen intent OK hecho
flutter_local_notifications + flag autoAnswer banner + auto answer full-screen + auto answer ~20 LOC no
flutter_callkit_incoming plugin CallKit nativo CallStyle nativo ~34 días + VoIP cert no
Bare metal (Swift+CallKit / Kotlin+ConnectionService) máximo control máximo control varios sprints no

Bloqueador actual

VoIP cert de Apple no obtenido. Sin él, no hay PushKit / CallKit en iOS, y por lo tanto la migración a flutter_callkit_incoming no entrega valor en iOS. Movernos solo en Android dejaría una asimetría en la UX entre plataformas.

Account Holder Apple Developer: Jorge Alvarez (Team KQ73NP6L4Q). El cert se gestiona desde su cuenta.

Plan recomendado

Fase 1 — mientras no haya VoIP cert (hoy)

No implementar ahora. Conscientemente decidimos vivir con el doble-tap hasta tener el cert. Si la presión de UX aumenta antes de obtener el cert, implementar el flag autoAnswer como mitigación temporal:

  • Añadir campo autoAnswer: bool a VideocallIncomingArgs.
  • En notifications_init._onLocalNotificationTapped, distinguir actionId == actionAccept (autoAnswer=true) vs body tap (autoAnswer=false).
  • En videocall_controller._consumePendingIncoming, si autoAnswer, llamar answerCall() automáticamente después de presentIncomingChannelCall(...).

Caveat: durante el SDK login (~13s) el usuario verá brevemente la IncomingView con el ringtone antes de entrar a la llamada activa. Para una v1 está OK. Reemplazable por un loading "Conectando…".

Fase 2 — cuando llegue VoIP cert

Migrar a flutter_callkit_incoming.

iOS:

  1. Generar VoIP cert en Apple Developer (Account Holder).
  2. Subir cert al backend (FCM no envía VoIP pushes; necesitamos APNs VoIP directo o un proxy en el backend).
  3. Configurar PushKit en AppDelegate.swift.
  4. Implementar CXProviderDelegate callbacks que disparan joinChannel (Juphoon) cuando CallKit reporta accept.

Android:

  1. Permiso MANAGE_OWN_CALLS en AndroidManifest.xml.
  2. Foreground service para mantener la llamada en background.
  3. flutter_callkit_incoming ya wrappea Notification.CallStyle internamente — solo configurar.

Flutter:

  1. Reemplazar _showIncomingCallNotification(data) en notifications_init.dart por FlutterCallkitIncoming.showCallkitIncoming(params).
  2. Suscribirse a FlutterCallkitIncoming.onEvent para manejar actionCallAccept / actionCallDecline / actionCallEnded.
  3. En actionCallAccept callback, leer roomNumber/sessionId del payload y llamar al videocallController.presentIncomingChannelCall(...) + answerCall() directo.
  4. Eliminar el flag autoAnswer si se hubiera implementado en Fase 1.

Backend:

  • La push payload de VIDEO_CALL_FROM puede mantenerse igual (los campos roomNumber, appAccount, sessionId, chatType siguen siendo los que usa el handler).
  • Para iOS: el backend necesita enviar push VoIP via APNs (canal aparte de FCM). Esto es trabajo coordinado con backend.

Estimación

Item Esfuerzo
Obtener VoIP cert (gestión Apple) semanas (no nuestro)
iOS CallKit + PushKit nativo 12 días
Android CallStyle (via plugin) 1 día
Backend APNs VoIP push depende backend
Flutter wiring + cleanup 1 día
QA cross-platform 12 días
Total dev ~46 días una vez desbloqueado el cert

Referencias