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: