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

131 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | ~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`](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 | 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
- [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)`.