From ea5fced456cce5871fabf2021c8de633fc667a27 Mon Sep 17 00:00:00 2001 From: misha Date: Wed, 17 Jun 2026 16:47:51 +0300 Subject: [PATCH] feat(video_calls): added system ui for call and navigation to the app --- ios/Runner/AppDelegate.swift | 113 ----------------- ios/Runner/Info.plist | 8 -- .../video_call_callkit_coordinator.dart | 120 ++++++++++-------- pubspec.yaml | 2 +- 4 files changed, 65 insertions(+), 178 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index ed5fa3dd..e2f16042 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -14,7 +14,6 @@ import flutter_foreground_task private var voipDiagnosticsChannel: FlutterMethodChannel? private let voipDiagnosticsStorageKey = "EatvioVoipDiagnostics" private let voipDiagnosticsChannelName = "com.eatvioapp/voip_diagnostics" - private let appUrlScheme = "eatvio" override func application( _ application: UIApplication, @@ -75,37 +74,6 @@ import flutter_foreground_task open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { - if url.scheme == appUrlScheme { - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - let queryItems = components?.queryItems ?? [] - let conversationUuid = queryItems.first { $0.name == "conversationUuid" }?.value? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let callKitId = queryItems.first { $0.name == "callKitId" }?.value? - .trimmingCharacters(in: .whitespacesAndNewlines) - recordVoipDiagnostic( - "accepted_call_url_opened", - data: [ - "callKitId": callKitId ?? "", - "hasCallKitId": callKitId != nil, - "conversationUuid": conversationUuid, - "url": url.absoluteString, - ] - ) - if let callKitId, !callKitId.isEmpty, !conversationUuid.isEmpty { - voipDiagnosticsChannel?.invokeMethod( - "acceptedVoipCall", - arguments: [ - "id": callKitId, - "handle": conversationUuid, - "extra": [ - "conversationUuid": conversationUuid - ], - ] - ) - } - return true - } - return YXLSdk.shared.handleOpen( url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String @@ -140,75 +108,6 @@ import flutter_foreground_task return nil } - private func conversationUuid(fromCall call: Call) -> String? { - let extra = call.data.extra as? [String: Any] ?? [:] - for key in ["conversationUuid", "conversation_uuid", "conversation_id", "chat_uuid", "uuid"] { - if let value = extra[key] as? String { - let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty { - return trimmed - } - } - } - - let handle = call.data.handle.trimmingCharacters(in: .whitespacesAndNewlines) - if !handle.isEmpty { - return handle - } - - return nil - } - - private func openAppForAcceptedCall(_ call: Call) { - guard let conversationUuid = conversationUuid(fromCall: call), !conversationUuid.isEmpty else { - recordVoipDiagnostic( - "accepted_call_missing_conversation_uuid", - data: ["callKitId": call.data.uuid] - ) - return - } - - var components = URLComponents() - components.scheme = appUrlScheme - components.host = "video-call" - components.queryItems = [ - URLQueryItem(name: "callKitId", value: call.data.uuid), - URLQueryItem(name: "conversationUuid", value: conversationUuid) - ] - - guard let url = components.url else { - recordVoipDiagnostic( - "accepted_call_url_build_failed", - data: [ - "callKitId": call.data.uuid, - "conversationUuid": conversationUuid, - ] - ) - return - } - - recordVoipDiagnostic( - "accepted_call_open_app_requested", - data: [ - "callKitId": call.data.uuid, - "conversationUuid": conversationUuid, - "url": url.absoluteString, - "appState": UIApplication.shared.applicationState.rawValue, - ] - ) - - UIApplication.shared.open(url, options: [:]) { [weak self] success in - self?.recordVoipDiagnostic( - success ? "accepted_call_open_app_succeeded" : "accepted_call_open_app_failed", - data: [ - "callKitId": call.data.uuid, - "conversationUuid": conversationUuid, - "url": url.absoluteString, - ] - ) - } - } - private func sanitizedDiagnosticValue(_ value: Any?) -> Any { guard let value = value else { return "" @@ -446,18 +345,6 @@ import flutter_foreground_task func onAccept(_ call: Call, _ action: CXAnswerCallAction) { - recordVoipDiagnostic( - "callkit_accept_native", - data: [ - "callKitId": call.data.uuid, - "conversationUuid": conversationUuid(fromCall: call) ?? "", - "data": call.data.toJSON(), - ] - ) - voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON()) - DispatchQueue.main.async { [weak self] in - self?.openAppForAcceptedCall(call) - } action.fulfill() } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5bcdccbd..c35ead46 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -91,14 +91,6 @@ yx5399b03636104c6d920b87c1ad329254 - - CFBundleURLName - Eatvio - CFBundleURLSchemes - - eatvio - - diff --git a/lib/features/video_calls/services/video_call_callkit_coordinator.dart b/lib/features/video_calls/services/video_call_callkit_coordinator.dart index 55f5821e..0bfff5a5 100644 --- a/lib/features/video_calls/services/video_call_callkit_coordinator.dart +++ b/lib/features/video_calls/services/video_call_callkit_coordinator.dart @@ -30,6 +30,7 @@ class VideoCallCallKitCoordinator { final VideoCallSessionCubit? _videoCallSessionCubit; StreamSubscription? _eventSubscription; + AppLifecycleListener? _lifecycleListener; final Map _callKitParamsById = {}; final Set _acceptHandledCallKitIds = {}; final Set _suppressedEndCallKitIds = {}; @@ -49,16 +50,17 @@ class VideoCallCallKitCoordinator { if (Platform.isIOS) { _voipDiagnosticsChannel.setMethodCallHandler((call) async { - switch (call.method) { - case 'voipDiagnosticsUpdated': - _scheduleNativeVoipDiagnosticsDrain(); - case 'acceptedVoipCall': - await _handleNativeAcceptedVoipCall(call.arguments); + if (call.method == 'voipDiagnosticsUpdated') { + _scheduleNativeVoipDiagnosticsDrain(); } }); await _firebaseApi.registerPushToken(); + _lifecycleListener = AppLifecycleListener( + onResume: () => unawaited(_openActiveCallKitCallIfNeeded()), + ); WidgetsBinding.instance.addPostFrameCallback((_) { _scheduleNativeVoipDiagnosticsDrain(); + unawaited(_openActiveCallKitCallIfNeeded()); }); return; } @@ -81,7 +83,9 @@ class VideoCallCallKitCoordinator { void dispose() { unawaited(_eventSubscription?.cancel()); + _lifecycleListener?.dispose(); _eventSubscription = null; + _lifecycleListener = null; _callKitParamsById.clear(); _initialized = false; } @@ -285,6 +289,53 @@ class VideoCallCallKitCoordinator { return _paramsForCallId(id); } + /// When the user opens the app from the system CallKit UI (e.g. taps "Итвио"), + /// navigate to the in-app call screen for an already-accepted active call. + Future _openActiveCallKitCallIfNeeded() async { + if (!Platform.isIOS) return; + if (_cubit.state.connectPhase != VideoCallConnectPhase.idle) return; + + final activeCalls = await FlutterCallkitIncoming.activeCalls(); + if (activeCalls.isEmpty) return; + + for (final params in activeCalls) { + _cacheCallKitParams(params); + if (!params.isAccepted) continue; + + final extra = params.extra ?? {}; + final conversationUuid = _parseConversationUuid( + extra, + handle: params.handle, + ); + if (conversationUuid.isEmpty) continue; + + await _deliverIncomingAccept( + callKitId: params.id, + conversationUuid: conversationUuid, + counterpartName: + _parseString(extra, const [ + 'counterpartName', + 'caller_name', + 'callerName', + ]) ?? + params.nameCaller, + counterpartAvatarUrl: + _parseString(extra, const [ + 'counterpartAvatarUrl', + 'avatar', + 'avatarUrl', + 'caller_avatar', + ]) ?? + params.avatar, + localParticipantName: _parseString(extra, const [ + 'localParticipantName', + 'local_participant_name', + ]), + ); + return; + } + } + Future _onNativeAcceptCallHandle(Map body) async { final extra = Map.from(body); final conversationUuid = _parseConversationUuid(extra); @@ -317,57 +368,6 @@ class VideoCallCallKitCoordinator { ); } - Future _handleNativeAcceptedVoipCall(Object? arguments) async { - if (!Platform.isIOS) return; - - final data = _stringKeyedMap(arguments); - final extra = _stringKeyedMap(data['extra']); - final callKitId = _parseString(data, const ['id', 'uuid']); - if (callKitId == null) { - log( - 'VideoCallCallKitCoordinator: native acceptedVoipCall without callKitId, data=$data', - name: 'VideoCallCallKit', - ); - return; - } - - final conversationUuid = _parseConversationUuid( - extra, - handle: _parseString(data, const ['handle']), - ); - if (conversationUuid.isEmpty) { - log( - 'VideoCallCallKitCoordinator: native acceptedVoipCall without conversationUuid, data=$data', - name: 'VideoCallCallKit', - ); - return; - } - - await _deliverIncomingAccept( - callKitId: callKitId, - conversationUuid: conversationUuid, - counterpartName: - _parseString(extra, const [ - 'counterpartName', - 'caller_name', - 'callerName', - ]) ?? - _parseString(data, const ['nameCaller']), - counterpartAvatarUrl: - _parseString(extra, const [ - 'counterpartAvatarUrl', - 'avatar', - 'avatarUrl', - 'caller_avatar', - ]) ?? - _parseString(data, const ['avatar']), - localParticipantName: _parseString(extra, const [ - 'localParticipantName', - 'local_participant_name', - ]), - ); - } - Future _handleAccept(String callKitId) async { if (_acceptHandledCallKitIds.contains(callKitId)) return; @@ -387,6 +387,14 @@ class VideoCallCallKitCoordinator { return; } + if (Platform.isIOS) { + log( + 'VideoCallCallKitCoordinator: iOS accept received (keeping CallKit UI), callKitId=$callKitId, conversationUuid=$conversationUuid', + name: 'VideoCallCallKit', + ); + return; + } + await _deliverIncomingAccept( callKitId: callKitId, conversationUuid: conversationUuid, diff --git a/pubspec.yaml b/pubspec.yaml index 3cbf6779..7732b34a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.27+114 +version: 1.3.27+115 publish_to: 'none' environment: