swift - My Flutter Call Kit Implementation works for Android but not for iOS - Stack Overflow

I am able to send an incoming_call notification on both iOS and Android, I'm not actually using it

I am able to send an incoming_call notification on both iOS and Android, I'm not actually using it to start a call, it just brings them to a waiting room where I use Agora to

  void _listenForNotifications() {
    print("Setting up foreground message listener");

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {

      print('Received a foreground message:');
      print('Title: ${message.notification?.title}');
      print('Body: ${message.notification?.body}');
      print('Data: ${message.data}');
      print('Full message dump: $message');

      bool isCallNotification = false;

      if (message.data['type'] == 'MEETING_STARTED' &&
          message.data['notificationType'] == 'incoming_call') {
        isCallNotification = true;
        print(
            "Call detected via MEETING_STARTED type and incoming_call notificationType");
      } else if (message.data['notificationType'] == 'incoming_call') {
        isCallNotification = true;
        print("Call detected via incoming_call notificationType");
      } else if (message.data.containsKey('meetingId') &&
          message.data.containsKey('channelId') &&
          message.data.containsKey('token')) {
        isCallNotification = true;
        print("Call detected via presence of meeting parameters");
      }

      if (isCallNotification) {
        print("Detected incoming call notification in foreground");
        _handleIncomingCall(message);
      } else {
        // Handle other types of notifications
        _handleMessageWhenAppForeground(message);
      }
    }, onError: (error, stack) {
      NotificationLogger.logError("foreground_listener", error, stack);

      print("Error in foreground message listener: $error");
      print("Stack trace: $stack");
    });

    print("Foreground message listener set up complete");
  }

  void _handleIncomingCall(RemoteMessage message) async {
    try {
      print("[DEBUG] Handling incoming call");

      // Use the UUID from the message or generate a new one
      final String callId = message.data['uuid'] ?? const Uuid().v4();

      // Check for and end any active calls
      final List<dynamic> activeCalls =
          await FlutterCallkitIncoming.activeCalls();
      if (activeCalls.isNotEmpty) {
        await FlutterCallkitIncoming.endAllCalls();
      }

      // Create call parameters
      final CallKitParams callKitParams = CallKitParams(
        id: callId,
        nameCaller: message.data['callerName'] ?? 'Unknown',
        appName: 'Foundermatcha',
        avatar: message.data['callerImage'],
        handle: 'Video Call',
        type: 1, // Video call
        duration: 30000,
        textAccept: 'Accept',
        textDecline: 'Decline',
        missedCallNotification: const NotificationParams(
          showNotification: true,
          isShowCallback: true,
          subtitle: 'Missed call',
          callbackText: 'Call back',
        ),
        extra: {
          'meetingId': message.data['meetingId'],
          'channelId': message.data['channelId'],
          'token': message.data['token'],
          'imageRotation': message.data['imageRotation'] ?? '0',
        },
        android: const AndroidParams(
          isCustomNotification: true,
          isShowLogo: true,
          ringtonePath: 'system_ringtone_default',
          backgroundColor: '#78C452',
          backgroundUrl: 'assets/images/logo.png',
          actionColor: '#4CAF50',
          incomingCallNotificationChannelName: "Incoming Call",
          missedCallNotificationChannelName: "Missed Call",
        ),
        ios: const IOSParams(
          iconName: 'CallKitLogo',
          handleType: 'generic',
          supportsVideo: true,
          maximumCallGroups: 2,
          maximumCallsPerCallGroup: 1,
          audioSessionMode: 'default',
          audioSessionActive: true,
          audioSessionPreferredSampleRate: 44100.0,
          audioSessionPreferredIOBufferDuration: 0.005,
          supportsDTMF: true,
          supportsHolding: true,
          supportsGrouping: false,
          supportsUngrouping: false,
          ringtonePath: 'system_ringtone_default',
        ),
      );

      print("[DEBUG] Showing call UI");
      await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);
      print("[DEBUG] Call UI displayed");

      // Set up event listener for this specific call
      _setupCallEventListener(callId, message);
    } catch (e, stack) {
      print("[ERROR] Exception in _handleIncomingCall: $e");
      print("[ERROR] Stack trace: $stack");
    }
  }

And when it gets to .showCallkitIncoming it should... Show the call kit! but it always returns null. The system is working for Android but not iOS.

Here's how I start the call:

  Future<void> _initiateTestCall() async {
    final targetToken = _targetDeviceTokenController.text.trim();
    if (targetToken.isEmpty) {
      setState(() {
        _status = 'Please enter a target device token';
      });
      return;
    }

    setState(() {
      _isLoading = true;
      _status = 'Initiating test call...';
    });

    try {
      final currentUser = ref.read(userProvider);
      if (currentUser == null) {
        setState(() {
          _status = 'Error: Current user not found';
          _isLoading = false;
        });
        return;
      }

      // Get a sample token for testing
      final tokenResult =
          await FirebaseFunctions.instance.httpsCallable('generateToken').call({
        'channelName': 'test_channel_${DateTime.now().millisecondsSinceEpoch}',
        'expiryTime': 3600,
      });

      final token = tokenResult.data['token'] as String?;
      if (token == null) {
        setState(() {
          _status = 'Error: Failed to generate token';
          _isLoading = false;
        });
        return;
      }

      final meetingId = "test_meeting_${DateTime.now().millisecondsSinceEpoch}";
      final channelId = "test_channel_${DateTime.now().millisecondsSinceEpoch}";

      String? callerImage = currentUser.profileImageUrl;

      final firebaseMessagingServices =
          ref.read(firebaseMessagingServicesProvider);

      await firebaseMessagingServices.sendMeetingStartedNotification(
        users: [currentUser.uid],
        title: "Incoming Video Call",
        body: "${currentUser.firstName} is calling you",
        type: "MEETING_STARTED",
        starterId: currentUser.uid,
        //hardcoded
        recipientId: "mtIslN60KpWGt1jg1a6jESwSkvW2",
        recipientToken: targetToken,
        meetingId: meetingId,
        channelId: channelId,
        token: token,
        callerName: currentUser.firstName,
        callerImage: callerImage,
        notificationType: "incoming_call",
        imageNeedsRotation: true,
      );

      setState(() {
        _status = 'Test call notification sent!';
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _status = 'Error sending test call: $e';
        _isLoading = false;
      });
    }
  }


  Future<void> sendMeetingStartedNotification({
    required List<String> users,
    required String title,
    required String body,
    required String type,
    required String starterId,
    required String recipientId,
    String? recipientToken,
    String? meetingId,
    String? channelId,
    String? token,
    String? callerName,
    String? callerImage,
    String? notificationType,
    bool imageNeedsRotation = false,
  }) async {
    print('Sending meeting started notification...');
    print('getting token');

    print('Recipient token: $recipientToken');
    print('Recipient ID: $recipientId');

    // Check for missing token
    if (recipientToken == null || recipientToken.isEmpty) {
      print("Cannot send notification: recipient token is missing");
      try {
        final userDoc =
            await FirebaseConstants.usersCollection.doc(recipientId).get();
        recipientToken = userDoc.data()?['deviceToken'];
        if (recipientToken == null || recipientToken.isEmpty) {
          print("No valid token found for user: $recipientId");
          return;
        }
      } catch (e) {
        print("Error fetching token: $e");
        return;
      }
    }

    var serverAccessTokenKey = await getAccessToken();

    // Generate a UUID for the call that will be consistent between notification and handler
    final callUuid = const Uuid().v4();

    // Determine channel ID for Android
    String androidChannelId = 'high_importance_channel';
    if (notificationType == 'incoming_call') {
      androidChannelId = 'incoming_calls';
    } else if (type == 'MEETING_REMINDER') {
      androidChannelId = 'meeting_reminders';
    } else if (type.contains('chat') || type.contains('CHAT')) {
      androidChannelId = 'support_chat';
    }

    final Map<String, dynamic> notificationBody;
    if (notificationType == 'incoming_call') {
      // Data-only message format for calls
      notificationBody = {
        'message': {
          'token': recipientToken,
          'data': {
            'title': title,
            'body': body,
            'type': type,
            'senderId': starterId,
            'recipientId': recipientId,
            'meetingId': meetingId ?? 'unknown',
            'channelId': channelId ?? 'unknown',
            'token': token ?? 'unknown',
            'callerName': callerName ?? 'Unknown Caller',
            'callerImage': callerImage ?? '',
            'imageRotation': imageNeedsRotation ? '270' : '0',
            'notificationType': 'incoming_call',
            'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
            'uuid': callUuid,
          },
          'android': {
            'priority': 'high',
          },
          'apns': {
            'headers': {
              'apns-priority': '10',
              'apns-push-type': 'background',
            },
            'payload': {
              'aps': {
                'content-available': 1,
                'sound': 'default',
                'category': 'INCOMING_CALL',
              },
            },
          },
        }
      };
      print('Call notification body contains: $notificationBody');
    } else {
      // Regular notification format
      notificationBody = {
        'message': {
          'token': recipientToken,
          'notification': {
            'title': title,
            'body': body,
          },
          'data': {
            'type': type,
            'senderId': starterId,
            'recipientId': recipientId,
            'meetingId': meetingId ?? '',
            'channelId': channelId ?? '',
            'token': token ?? '',
            'callerName': callerName ?? '',
            'callerImage': callerImage ?? '',
            'imageRotation': imageNeedsRotation ? '270' : '0',
            'notificationType': notificationType ?? '',
            'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
          },
          'android': {
            'priority': 'high',
            'notification': {
              'channel_id': androidChannelId,
            },
          },
          'apns': {
            'headers': {
              'apns-priority': '10',
            },
            'payload': {
              'aps': {
                'category': notificationType == 'incoming_call'
                    ? 'INCOMING_CALL'
                    : 'DEFAULT',
                'sound': 'default',
                'badge': 1,
                'content-available': 1,
              },
            },
          },
        }
      };
    }

    print('Sending notification payload: ${jsonEncode(notificationBody)}');
    print('Notification type: $type, Channel: $androidChannelId');

    final headers = {
      'content-type': 'application/json',
      'Authorization': "Bearer $serverAccessTokenKey",
    };

    try {
      final response = await http.post(
        Uri.parse(AppConfig.firebaseNotificationsApi),
        headers: headers,
        body: jsonEncode(notificationBody),
      );

      if (response.statusCode == 200) {
        print('Notification sent successfully - Type: $type');
        return;
      } else {
        print(
            'Error sending notification. ${response.statusCode} : ${response.body}');

        // Handle token errors
        if (response.body.contains("UNREGISTERED") ||
            response.statusCode == 404) {
          try {
            // We already showed the notification for dev environment above
            print("Token appears to be invalid - may be a development token");
          } catch (e) {
            print("Error handling token status: $e");
          }
        }

        // Try alternative format for calls
        if (response.statusCode == 400 && notificationType == 'incoming_call') {
          print('Attempting alternative call notification format...');
          await _sendAlternativeCallNotification(
            recipientToken: recipientToken,
            title: title,
            body: body,
            type: type,
            starterId: starterId,
            recipientId: recipientId,
            meetingId: meetingId,
            channelId: channelId,
            token: token,
            callerName: callerName,
            callerImage: callerImage,
            imageRotation: imageNeedsRotation ? '270' : '0',
            serverAccessTokenKey: serverAccessTokenKey,
          );
        }
      }
    } catch (e) {
      print('Exception sending notification: $e');
    }
  }

I'm unfamiliar with the the call kit but I can't figure out from the docs what is causing the issue. Here's the terminal out put from the caller:

flutter: Sending meeting started notification...
flutter: getting token
flutter: Recipient token: fwzrcggr90A6jFmUUb9M_2:APA91bHL50Ai2WHbppnuw_4LmE50YlyDETfrY2X081A7_QW_-9Aoz8xIQMLyTyKACeeo31VueJvQDttU3Z1PGmmGggre5PdJKkid5B-L0tzY7G7XfkbK3Tg
flutter: Recipient ID: mtIslN60KpWGt1jg1a6jESwSkvW2
flutter: Call notification body contains: {message: {token: fwzrcggr90A6jFmUUb9M_2:APA91bHL50Ai2WHbppnuw_4LmE50YlyDETfrY2X081A7_QW_-9Aoz8xIQMLyTyKACeeo31VueJvQDttU3Z1PGmmGggre5PdJKkid5B-L0tzY7G7XfkbK3Tg, data: {title: Incoming Video Call, body: 8 is calling you, type: MEETING_STARTED, senderId: 3DaYgDkMtJQo5OmY6cLasygINvh2, recipientId: mtIslN60KpWGt1jg1a6jESwSkvW2, meetingId: test_meeting_1741275230450, channelId: test_channel_1741275230450, token: 007eJxTYFAsYD2/tuzmp2trmbyXLJzu55Kx4Mq5zZ6CG0SNxO80/DRXYEg1tUw0tEg2SDVNTDKxSEu1NEtKMkwxsTQwMzZOMTdMjjtwMj3v3Mn0s8VRrIwMjAwsDIwMIMAEJpnBJAuYlGIoSS0uiU/OSMzLS82JNzQ3MTQyNzUysjAyt2RgAAAYCCea, callerName: 8, callerImage: .appspot/o/userImages%2F3DaYgDkMtJQo5OmY6cLasygINvh2%2Fprofilepicture?alt=media&token=9810d869-c004-483b-b2a9-d76870c0501a, imageRotation: 270, notificationType: incoming_call, timestamp: 1741275230707, uuid: e7cca826-29db-480c-9220-ac62f5c71721}, android: {priority: high}, ap<…>
flutter: Sending notification payload: {"message":{"token":"fwzrcggr90A6jFmUUb9M_2:APA91bHL50Ai2WHbppnuw_4LmE50YlyDETfrY2X081A7_QW_-9Aoz8xIQMLyTyKACeeo31VueJvQDttU3Z1PGmmGggre5PdJKkid5B-L0tzY7G7XfkbK3Tg","data":{"title":"Incoming Video Call","body":"8 is calling you","type":"MEETING_STARTED","senderId":"3DaYgDkMtJQo5OmY6cLasygINvh2","recipientId":"mtIslN60KpWGt1jg1a6jESwSkvW2","meetingId":"test_meeting_1741275230450","channelId":"test_channel_1741275230450","token":"007eJxTYFAsYD2/tuzmp2trmbyXLJzu55Kx4Mq5zZ6CG0SNxO80/DRXYEg1tUw0tEg2SDVNTDKxSEu1NEtKMkwxsTQwMzZOMTdMjjtwMj3v3Mn0s8VRrIwMjAwsDIwMIMAEJpnBJAuYlGIoSS0uiU/OSMzLS82JNzQ3MTQyNzUysjAyt2RgAAAYCCea","callerName":"8","callerImage":".appspot/o/userImages%2F3DaYgDkMtJQo5OmY6cLasygINvh2%2Fprofilepicture?alt=media&token=9810d869-c004-483b-b2a9-d76870c0501a","imageRotation":"270","notificationType":"incoming_call","timestamp":"1741275230707","uuid":"e7cca826-29db-480c-9220-ac62f5c71721"},<…>
flutter: Notification type: MEETING_STARTED, Channel: incoming_calls
flutter: Notification sent successfully - Type: MEETING_STARTED

And here it is for the receiver(from when the call is received):

flutter: Received a foreground message:
flutter: Title: null
flutter: Body: null
flutter: Data: {body: 8 is calling you, callerImage: .appspot/o/userImages%2F3DaYgDkMtJQo5OmY6cLasygINvh2%2Fprofilepicture?alt=media&token=9810d869-c004-483b-b2a9-d76870c0501a, meetingId: test_meeting_1741275230450, senderId: 3DaYgDkMtJQo5OmY6cLasygINvh2, channelId: test_channel_1741275230450, uuid: e7cca826-29db-480c-9220-ac62f5c71721, timestamp: 1741275230707, imageRotation: 270, callerName: 8, notificationType: incoming_call, type: MEETING_STARTED, title: Incoming Video Call, token: 007eJxTYFAsYD2/tuzmp2trmbyXLJzu55Kx4Mq5zZ6CG0SNxO80/DRXYEg1tUw0tEg2SDVNTDKxSEu1NEtKMkwxsTQwMzZOMTdMjjtwMj3v3Mn0s8VRrIwMjAwsDIwMIMAEJpnBJAuYlGIoSS0uiU/OSMzLS82JNzQ3MTQyNzUysjAyt2RgAAAYCCea, recipientId: mtIslN60KpWGt1jg1a6jESwSkvW2}
flutter: Full message dump: Instance of 'RemoteMessage'
flutter: Call detected via MEETING_STARTED type and incoming_call notificationType
flutter: ✅ Detected incoming call notification in foreground
flutter: [DEBUG] Handling incoming call
flutter: [DEBUG] Showing call UI
flutter: [DEBUG] Call UI displayed

Also I have these background modes in info.plist:

    <key>UIBackgroundModes</key>
    <array>
        <string>fetch</string>
        <string>remote-notification</string>
        <string>processing</string> 
    </array>

As I'm not actually starting a call I don't believe I need voip And this is my AppDelegate & CallKitBridge

import Flutter
import UIKit
import FirebaseCore

@main
@objc class AppDelegate: FlutterAppDelegate {
  private var callKitBridge: SwiftCallKitBridge?
  private var methodChannel: FlutterMethodChannel?
  
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    FirebaseApp.configure()
    GeneratedPluginRegistrant.register(with: self)
    
    // Initialize CallKit bridge
    callKitBridge = SwiftCallKitBridge()
    
    // Setup method channel
    if let controller = window?.rootViewController as? FlutterViewController {
      methodChannel = FlutterMethodChannel(name: "com.foundermatcha.callkit", binaryMessenger: controller.binaryMessenger)
      
      methodChannel?.setMethodCallHandler({ [weak self] (call, result) in
        guard let self = self else { return }
        
        switch call.method {
        case "endCall":
          self.callKitBridge?.endCall()
          result(true)
        case "showIncomingCall":
          if let args = call.arguments as? [String: Any],
             let callerName = args["callerName"] as? String,
             let meetingId = args["meetingId"] as? String,
             let channelId = args["channelId"] as? String,
             let token = args["token"] as? String {
            self.callKitBridge?.reportIncomingCall(from: callerName, meetingId: meetingId, channelId: channelId, token: token)
            result(true)
          } else {
            result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments for showIncomingCall", details: nil))
          }
        default:
          result(FlutterMethodNotImplemented)
        }
      })
      
      // Set method channel on bridge
      callKitBridge?.setMethodChannel(methodChannel!)
    }
    
    // Register for standard push notifications
    UNUserNotificationCenter.current().delegate = self
    let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
    UNUserNotificationCenter.current().requestAuthorization(
      options: authOptions,
      completionHandler: { _, _ in }
    )
    application.registerForRemoteNotifications()
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
    let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
    let token = tokenParts.joined()
    print("Push token: \(token)")
    methodChannel?.invokeMethod("updatePushToken", arguments: ["token": token])
  }
  
  // Handle push notifications when app is in background
  override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    if let data = userInfo["data"] as? [String: Any], 
       let type = data["type"] as? String,
       type == "incoming_call" {
      // Extract call data
      let callerName = data["callerName"] as? String ?? "Unknown"
      let meetingId = data["meetingId"] as? String ?? ""
      let channelId = data["channelId"] as? String ?? ""
      let token = data["token"] as? String ?? ""
      
      // Show CallKit UI
      callKitBridge?.reportIncomingCall(from: callerName, meetingId: meetingId, channelId: channelId, token: token)
    }
    
    completionHandler(.newData)
  }
}
import Flutter
import UIKit
import CallKit
import AVFoundation

@objc public class SwiftCallKitBridge: NSObject, CXProviderDelegate {
    private let provider: CXProvider
    private let callController = CXCallController()
    private var callId: UUID?
    private var callInfo: [String: Any]?
    private var methodChannel: FlutterMethodChannel?
    
    @objc public override init() {
        let providerConfiguration = CXProviderConfiguration(localizedName: "Founder Matcha")
        providerConfiguration.supportsVideo = true
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.supportedHandleTypes = [.generic]
        providerConfiguration.ringtoneSound = "ringtone_default.caf"
        
        if let iconImage = UIImage(named: "CallKitLogo") {
            providerConfiguration.iconTemplateImageData = iconImage.pngData()
        }
        
        provider = CXProvider(configuration: providerConfiguration)
        
        super.init()
        provider.setDelegate(self, queue: nil)
    }
    
    @objc public func setMethodChannel(_ channel: FlutterMethodChannel) {
        self.methodChannel = channel
    }
    
    // Report incoming call to the system
    @objc public func reportIncomingCall(from caller: String, meetingId: String, channelId: String, token: String) {
        let callUpdate = CXCallUpdate()
        callUpdate.remoteHandle = CXHandle(type: .generic, value: caller)
        callUpdate.hasVideo = true
        callUpdate.supportsDTMF = false
        callUpdate.supportsHolding = false
        callUpdate.supportsGrouping = false
        callUpdate.supportsUngrouping = false
        
        self.callId = UUID()
        
        // Store call information for later use
        self.callInfo = [
            "meetingId": meetingId,
            "channelId": channelId,
            "token": token,
            "callerName": caller
        ]
        
        if let callId = self.callId {
            provider.reportNewIncomingCall(with: callId, update: callUpdate) { error in
                if let error = error {
                    print("Failed to report incoming call: \(error.localizedDescription)")
                } else {
                    print("Incoming call reported successfully")
                    
                    // Enable audio session for the call
                    let audioSession = AVAudioSession.sharedInstance()
                    do {
                        try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
                        try audioSession.setActive(true)
                    } catch {
                        print("Error setting up audio session: \(error)")
                    }
                }
            }
        }
    }
    
    // MARK: - CXProviderDelegate methods
    
    public func providerDidReset(_ provider: CXProvider) {
        print("Provider did reset")
        callId = nil
        callInfo = nil
        
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(false)
        } catch {
            print("Error deactivating audio session: \(error)")
        }
    }
    
    public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        print("Call answered")
        
        if let callInfo = self.callInfo {
            methodChannel?.invokeMethod("callAccepted", arguments: callInfo)
        }
        
        action.fulfill()
    }
    
    public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        print("Call ended")
        
        if let callId = self.callId {
            methodChannel?.invokeMethod("callEnded", arguments: ["callId": callId.uuidString])
        }
        
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setActive(false)
        } catch {
            print("Error deactivating audio session: \(error)")
        }
        
        callId = nil
        callInfo = nil
        
        action.fulfill()
    }
    
    // Method to programmatically end the call
    @objc public func endCall() {
        guard let callId = self.callId else { return }
        
        let endCallAction = CXEndCallAction(call: callId)
        let transaction = CXTransaction(action: endCallAction)
        
        callController.request(transaction) { error in
            if let error = error {
                print("Error ending call: \(error.localizedDescription)")
            } else {
                print("Call ended successfully")
            }
        }
    }
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744964121a4603569.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信