diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index e2f16042..ed5fa3dd 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -14,6 +14,7 @@ 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,
@@ -74,6 +75,37 @@ 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
@@ -108,6 +140,75 @@ 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 ""
@@ -345,6 +446,18 @@ 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 c35ead46..5bcdccbd 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -91,6 +91,14 @@
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 0bfff5a5..55f5821e 100644
--- a/lib/features/video_calls/services/video_call_callkit_coordinator.dart
+++ b/lib/features/video_calls/services/video_call_callkit_coordinator.dart
@@ -30,7 +30,6 @@ class VideoCallCallKitCoordinator {
final VideoCallSessionCubit? _videoCallSessionCubit;
StreamSubscription? _eventSubscription;
- AppLifecycleListener? _lifecycleListener;
final Map _callKitParamsById = {};
final Set _acceptHandledCallKitIds = {};
final Set _suppressedEndCallKitIds = {};
@@ -50,17 +49,16 @@ class VideoCallCallKitCoordinator {
if (Platform.isIOS) {
_voipDiagnosticsChannel.setMethodCallHandler((call) async {
- if (call.method == 'voipDiagnosticsUpdated') {
- _scheduleNativeVoipDiagnosticsDrain();
+ switch (call.method) {
+ case 'voipDiagnosticsUpdated':
+ _scheduleNativeVoipDiagnosticsDrain();
+ case 'acceptedVoipCall':
+ await _handleNativeAcceptedVoipCall(call.arguments);
}
});
await _firebaseApi.registerPushToken();
- _lifecycleListener = AppLifecycleListener(
- onResume: () => unawaited(_openActiveCallKitCallIfNeeded()),
- );
WidgetsBinding.instance.addPostFrameCallback((_) {
_scheduleNativeVoipDiagnosticsDrain();
- unawaited(_openActiveCallKitCallIfNeeded());
});
return;
}
@@ -83,9 +81,7 @@ class VideoCallCallKitCoordinator {
void dispose() {
unawaited(_eventSubscription?.cancel());
- _lifecycleListener?.dispose();
_eventSubscription = null;
- _lifecycleListener = null;
_callKitParamsById.clear();
_initialized = false;
}
@@ -289,53 +285,6 @@ 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);
@@ -368,6 +317,57 @@ 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,14 +387,6 @@ 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 7732b34a..3cbf6779 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
-version: 1.3.27+115
+version: 1.3.27+114
publish_to: 'none'
environment: