# 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 | sí | | `flutter_local_notifications` + flag `autoAnswer` | banner + auto answer | full-screen + auto answer | ~20 LOC | no | | `flutter_callkit_incoming` plugin | CallKit nativo | `CallStyle` nativo | ~3–4 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 (~1–3s) 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`](https://pub.dev/packages/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 | 1–2 días | | Android `CallStyle` (via plugin) | 1 día | | Backend APNs VoIP push | depende backend | | Flutter wiring + cleanup | 1 día | | QA cross-platform | 1–2 días | | **Total dev** | **~4–6 días** una vez desbloqueado el cert | ## Referencias - [flutter_callkit_incoming pub.dev](https://pub.dev/packages/flutter_callkit_incoming) - [Apple — Reporting Incoming Calls with CallKit](https://developer.apple.com/documentation/callkit) - [Android Notification.CallStyle](https://developer.android.com/reference/android/app/Notification.CallStyle) - TODO en código: `apps/mobile_app/lib/core/notifications_init.dart` — buscar `TODO(videocall-callkit-migration)`. - Limitación iOS ya documentada en `apps/mobile_app/lib/core/notifications_init.dart` con `TODO(videocall-ios-callkit)`.