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
131 lines
5.3 KiB
Markdown
131 lines
5.3 KiB
Markdown
# 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)`.
|