Flutter Conecticube: unable to get call notification on IOS - Stack Overflow

I'm using ConnectyCube Flutter to implement video call functionality in my Flutter app.The call n

I'm using ConnectyCube Flutter to implement video call functionality in my Flutter app.

The call notifications work fine on Android, but on iOS, I don't receive any notification when a call is initiated from another device.

What I Have Done So Far:

  • Configured Push Notifications:
  • Set up Firebase Cloud Messaging (FCM) for push notifications.
  • Uploaded the APNs certificate to ConnectyCube.
  • Checked iOS Permissions: Enabled Push Notifications capability in Xcode. Ensured NSUserNotificationAlert is added in the Info.plist.

Tested with Background & Terminated State: On Android, the notification is received properly. On iOS, no notification appears in both foreground and background states. Debugging Logs: No errors related to push notifications appear in the logs. Verified that FCM token is correctly generated on iOS.

below is my AndroidManifest file

    xmlns:tools=";

    package="com.sambuq.care_first">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />


    
    <!-- End FlutterDownloader customization -->
    <!-- Provide required visibility configuration for API level 30 and above -->
    <queries>
    <!-- If your app checks for SMS support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="sms" />
    </intent>
    <!-- If your app checks for call support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="tel" />
    </intent>

    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
    </queries>

   <application
        android:label="360CareFirst"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <provider
            android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
            android:authorities="${applicationId}.flutter_downloader.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
        <!-- ... (other configurations) -->
        <!-- Begin FlutterDownloader customization -->
        <!-- disable default Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>

        <!-- declare customized Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
            android:authorities="${applicationId}.flutter-downloader-init"
            android:exported="false">
            <meta-data
                android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
                android:value="1" />
        </provider> 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
        <service
        android:name="de.julianassmann.flutter_background.IsolateHolderService"
        android:enabled="true"
        android:exported="false"
        android:foregroundServiceType="mediaProjection" />
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

**call manager class **

import 'package:care_first/bloc/auth_bloc/bloc/auth_bloc.dart';
import 'package:care_first/routing/route_names.dart';
import 'package:care_first/screens/settings/my_appointments/doctor_appointment/models/my_appointment.dart';
import 'package:care_first/shared/helper/helper_methods.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_io/io.dart';
import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';

import 'call_kit_manager.dart';
import '../utils/consts.dart';

class CallManager {
  static String TAG = "CallManager";

  static CallManager get instance => _getInstance();
  static CallManager? _instance;

  static CallManager _getInstance() {
    return _instance ??= CallManager._internal();
  }

  factory CallManager() => _getInstance();

  CallManager._internal();

  P2PClient? _callClient;
  P2PSession? _currentCall;
  BuildContext? context;
  MediaStream? localMediaStream;
  Map<int, MediaStream> remoteStreams = {};
  GlobalKey<NavigatorState>? navigatorKey;
  String? selfCubeId;
  late String resourceId;
  SystemMessagesManager? systemMessagesManager =
      CubeChatConnection.instance.systemMessagesManager;
  CubeMessage systemMessage = CubeMessage();
  final player = AudioPlayer();

  Function(bool, String)? onMicMuted;

  init(
    BuildContext context,
    GlobalKey<NavigatorState> navigatorKey,
    String selfCubeId,
    String resourceId,
  ) {
    this.context = context;
    this.navigatorKey = navigatorKey;
    this.selfCubeId = selfCubeId;
    this.resourceId = resourceId;

    _initCustomMediaConfigs();

    if (CubeChatConnection.instance.isAuthenticated()) {
      _initCalls(context);
    } else {
      _initChatConnectionStateListener(context);
    }

    _initCallKit();
  }

  destroy() {
    _callClient?.destroy();
    _callClient = null;
  }

  void _initCustomMediaConfigs() {
    RTCMediaConfig mediaConfig = RTCMediaConfig.instance;
    mediaConfig.minHeight = 340;
    mediaConfig.minWidth = 480;
    mediaConfig.minFrameRate = 25;

    RTCConfig.instance.statsReportsInterval = 200;
  }

  void _initCalls(BuildContext context) {
    if (_callClient == null) {
      _callClient = P2PClient.instance;

      _callClient!.init();
    }
    // _callClient is P2PClient
    _callClient!.onReceiveNewSession = (callSession) async {
      print("${'*' * 10} RECEIVED NEW CALL SESSION ${'*' * 10}");
      // if (_currentCall != null &&
      //     _currentCall!.sessionId != callSession.sessionId &&
      //     _currentCall!.opponentsIds.first.toString() != selfCubeId) {
      //   callSession.reject();
      //   return;
      // }
      if (navigatorKey!.currentContext!.read<AuthBloc>().user.persona ==
          "patient") {
        await player
            .setReleaseMode(ReleaseMode.loop)
            .then(
                (value) => player.setSource(AssetSource('audio/ringtone.mp3')))
            .then((value) => player.resume());
      }

      _currentCall = callSession;

      var callState = await _getCallState(_currentCall!.sessionId);
      await Future.delayed(Duration(seconds: 1));
      if (callState == CallState.REJECTED) {
        reject(_currentCall!.sessionId, false);
      } else if (callState == CallState.ACCEPTED) {
        acceptCall(_currentCall!.sessionId,
            _currentCall?.cubeSdp.userInfo ?? {}, false);
      } else if (callState == CallState.UNKNOWN ||
          callState == CallState.PENDING) {
        if (callState == CallState.UNKNOWN &&
            (Platform.isIOS || Platform.isAndroid)) {
          await ConnectycubeFlutterCallKit.setCallState(
              sessionId: _currentCall!.sessionId, callState: CallState.PENDING);
        }

        if (_currentCall!.cubeSdp.callerId.toString() != selfCubeId) {
          navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
              callSession;
          // await Future.delayed(Duration(seconds: 1));
          // await ConnectycubeFlutterCallKit.showCallNotification(CallEvent(
          //   sessionId: _currentCall!.sessionId,
          //   callType: CallType.VIDEO_CALL,
          //   callerId: _currentCall!.cubeSdp.callerId,
          //   callerName: "YASH",
          //   opponentsIds: _currentCall!.opponentsIds,
          //   userInfo: _currentCall?.cubeSdp.userInfo ?? {},
          // ));
          print("NOTIFICATION NOT CALLED");
          _showIncomingCallScreen(_currentCall!, navigatorKey!.currentContext!);
        }
      }

      _currentCall?.onLocalStreamReceived = (localStream) {
        localMediaStream = localStream;
      };

      _currentCall?.onRemoteStreamReceived = (session, userId, stream) {
        remoteStreams[userId] = stream;
      };

      _currentCall?.onRemoteStreamRemoved = (session, userId, stream) {
        remoteStreams.remove(userId);
      };

      _currentCall?.onReceiveHungUpFromUser =
          (session, userId, [userInfo]) async {
        print("CALL_MANAGER: onReceiveHungUpFromUser");
        /* systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_HUNGUP";
        print("-------------BROADCASTING CALL HUNGUP EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
        broadcastSystemMessage("CALL_HUNGUP");
        await player.release();
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      };
    };

    _callClient!.onSessionClosed = (callSession) async {
      if (_currentCall != null &&
          _currentCall!.sessionId == callSession.sessionId) {
        _currentCall = null;
        localMediaStream?.getTracks().forEach((track) async {
          await track.stop();
        });
        await localMediaStream?.dispose();
        localMediaStream = null;

        remoteStreams.forEach((key, value) async {
          await value.dispose();
        });

        remoteStreams.clear();
        CallKitManager.instance.processCallFinished(callSession.sessionId);
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      }
    };
  }

  void startNewCall(BuildContext context, int callType, int patientCubeId,
      MyAppointment appointment, String? businessId, String? moduleId) async {
    this.context = context;
    Set<int> opponents = {patientCubeId};
    if (opponents.isEmpty) return;
    if (!kIsWeb) {
      if (Platform.isIOS) {
        Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
            preferSpeakerOutput: true);
      }
    }
    P2PSession callSession =
        _callClient!.createCallSession(callType, opponents);
    _currentCall = callSession;
    // storing it in authbloc
    context.read<AuthBloc>().p2pSession = callSession;
    context.read<AuthBloc>().resourceId = resourceId;
    context.read<AuthBloc>().selfCubeId = selfCubeId;

    String callerName;
    if (appointment.doctorMap?["firstname"] != null) {
      callerName =
          "${appointment.doctorMap?["firstname"]} ${appointment.doctorMap?["lastname"]}";
    } else {
      callerName = "";
    }

    _sendStartCallSignalForOffliners(_currentCall!, callerName);
    GoRouter.of(context)
        .pushNamed(RoutesName.videoCallDoctorRoute, pathParameters: {
      'businessId': encrypt(text: businessId ?? '', context: context),
    }, queryParameters: {
      'moduleId': moduleId,
    }, extra: {
      // 'callSession': callSession,
      'appointment': appointment,
    });
  }

  void _showIncomingCallScreen(P2PSession callSession, BuildContext context) {
    if (navigatorKey!.currentContext != null) {
      GoRouter.of(navigatorKey!.currentContext!).pushNamed(
          RoutesName.incomingVideoCall,
          extra: Map<String, String>.from(
              {"resourceId": resourceId, "selfCubeId": selfCubeId}));
    }
  }

  void acceptCall(
    String sessionId,
    Map<String, String> userInfo,
    bool fromCallkit,
  ) async {
    await player.release();
    log('acceptCall, from callKit: $fromCallkit', TAG);
    //ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);
    if (_currentCall != null) {
      if (context != null) {
        // if (AppLifecycleState.resumed !=
        //     WidgetsBinding.instance.lifecycleState) {
        await _currentCall?.acceptCall();
        // }
        MyAppointment appointment = MyAppointment(
            id: userInfo['appointment_id'], doctor: userInfo['doctor_id']);
        log(userInfo.toString(), "User Info from AcceptCall");
        if (!fromCallkit) {
          await ConnectycubeFlutterCallKit.reportCallAccepted(
              sessionId: sessionId);
        }
        broadcastSystemMessage("CALL_ACCEPTED");
        navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
            _currentCall;
        GoRouter.of(navigatorKey!.currentContext!).pushReplacementNamed(
          RoutesName.videoCallPatientRoute,
          extra: Map<String, Object>.from({
            // 'currentCall': _currentCall!,
            'appointment': appointment,
          }),
        );

        /*  systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_ACCEPTED";
        print("-------------BROADCASTING ACCEPT CALL EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
      }
      if (!kIsWeb) {
        if (Platform.isIOS) {
          await Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
              preferSpeakerOutput: true);
        }
      }
    }
  }

  void reject(String sessionId, bool fromCallkit) {
    if (_currentCall != null) {
      player.release();
      broadcastSystemMessage("CALL_REJECTED");
      if (fromCallkit) {
        ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: false);
      } else {
        CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      }

      _currentCall!.reject();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  void hungUp() {
    if (_currentCall != null) {
      player.release();
      CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      _currentCall!.hungUp();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  CreateEventParams _getCallEventParameters(
      P2PSession currentCall, String? callerName) {
    /* String? callerName = users
        .where((cubeUser) => cubeUser.id == currentCall.callerId)
        .first
        .fullName; */
    CreateEventParams params = CreateEventParams();
    params.parameters = {
      'message':
          "Incoming ${currentCall.callType == CallType.VIDEO_CALL ? "Video" : "Audio"} call",
      PARAM_CALL_TYPE: currentCall.callType,
      PARAM_SESSION_ID: currentCall.sessionId,
      PARAM_CALLER_ID: currentCall.callerId,
      PARAM_CALL_OPPONENTS: currentCall.opponentsIds.join(','),
      PARAM_CALLER_NAME: callerName ?? "Doctor",
    };

    params.notificationType = NotificationType.PUSH;
    params.environment = CubeEnvironment.DEVELOPMENT;
    //kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
    params.usersIds = currentCall.opponentsIds.toList();

    return params;
  }

  Future<void> _sendStartCallSignalForOffliners(
      P2PSession currentCall, String callerName) async {
    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_START_CALL;
    params.parameters[PARAM_IOS_VOIP] = 1;
    params.parameters[PARAM_EXPIRATION] = 0;
    params.parameters['ios_push_type'] = 'background';

    await createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created: $cubeEvent");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _sendEndCallSignalForOffliners(
      P2PSession? currentCall, String? callerName) {
    if (currentCall == null) return;

    CubeUser? currentUser = CubeChatConnection.instance.currentUser;
    if (currentUser == null || currentUser.id != currentCall.callerId) return;

    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_END_CALL;

    createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _initCallKit() {
    CallKitManager.instance.init(
      onCallAccepted: (uuid) {
        acceptCall(uuid, _currentCall?.cubeSdp.userInfo ?? {}, true);
      },
      onCallEnded: (uuid) {
        reject(uuid, true);
      },
      onMuteCall: (mute, uuid) {
        onMicMuted?.call(mute, uuid);
      },
    );
  }

  void _initChatConnectionStateListener(BuildContext context) {
    CubeChatConnection.instance.connectionStateStream.listen((state) {
      if (CubeChatConnectionState.Ready == state) {
        _initCalls(context);
      }
    });
  }

  Future<String> _getCallState(String sessionId) async {
    if (Platform.isAndroid || Platform.isIOS) {
      var callState =
          await ConnectycubeFlutterCallKit.getCallState(sessionId: sessionId);

      log("CONECTICUBE CALL STATE: $callState");
      return callState;
    } else {
      return Future.value(CallState.UNKNOWN);
    }
  }

  void muteCall(String sessionId, bool mute) {
    CallKitManager.instance.muteCall(sessionId, mute);
  }

  Future<bool> _onBackPressed(BuildContext context) {
    return Future.value(false);
  }

  void broadcastSystemMessage(action) {
    systemMessage.recipientId = int.tryParse(selfCubeId!);
    systemMessage.properties["resourceId"] = resourceId;
    systemMessage.properties["action"] = action;
    print("BROADCASTING: $action EVENT");
    systemMessagesManager?.sendSystemMessage(systemMessage);
  }
}

FCM SETUP CLASS

import 'package:connectycube_sdk/connectycube_sdk.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:universal_io/io.dart';
import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';

class FcmSetup {
  static FcmSetup? _instance;
  FcmSetup._internal();

  factory FcmSetup() {
    return _instance ??= FcmSetup._internal();
  }
  FirebaseMessaging? firebaseMessaging;

  Future<void> init({
    required Function(RemoteMessage remoteMessage) onMessage,
  }) async {
    firebaseMessaging = FirebaseMessaging.instance;
    log("INT STARTED");
    await firebaseMessaging!.requestPermission(
      alert: false,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );
    String? token;
    if (Platform.isAndroid || kIsWeb) {
      token = await firebaseMessaging!.getToken();
    } else if (Platform.isIOS || Platform.isMacOS) {
      token = await firebaseMessaging!.getAPNSToken();
      log("APNS TOKEN $token");
    }

    if (token != null) {
      subscribe(token);
    }
    if (Platform.isIOS || Platform.isMacOS) {
      String? voipToken = await ConnectycubeFlutterCallKit.getToken();
      log("VOIP TOKEN $voipToken");
      if (voipToken != null) {
        subscribeVoIP(voipToken);
      }
    }

    firebaseMessaging!.onTokenRefresh.listen((newToken) async {
      subscribe(newToken);
    });

    // FirebaseMessaging.onMessage.listen(onMessage);
    // FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
  }

  subscribe(String token) async {
    log('[subscribe] token: $token');

    bool isProduction = bool.fromEnvironment('dart.vm.product');

    CreateSubscriptionParameters parameters = CreateSubscriptionParameters();
    parameters.environment =
        isProduction ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;

    if (Platform.isAndroid) {
      parameters.channel = NotificationsChannels.GCM;
      parameters.platform = CubePlatform.ANDROID;
      parameters.bundleIdentifier = "com.360carefirst.app";
    } else if (Platform.isIOS) {
      parameters.channel = NotificationsChannels.APNS;
      parameters.platform = CubePlatform.IOS;
      parameters.bundleIdentifier = Platform.isIOS
          ? "com.360carefirst.app"
          : "com.connectycube.flutter.chatSample.macOS";
    }
    String deviceId = await getDeviceId();
    parameters.udid = deviceId;
    parameters.pushToken = token;

    createSubscription(parameters.getRequestParameters())
        .then((cubeSubscription) {
      getSubscriptions().then((subscriptions) {
        log("Subscriptions: ${subscriptions.toString()}");
      });
      log("SUBSCRIPTION CREATED");
    }).catchError((error) {
      log("SUBSCRIPTION ERROR ${error}");
    });
  }

  Future<String> getDeviceId() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    if (Platform.isAndroid) {
      AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
      return androidInfo.id; // Unique ID for Android
    } else if (Platform.isIOS) {
      IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
      return iosInfo.identifierForVendor!;
    }
    return '';
  }

  void sendPushNotification(List<int> ids, String message, String title) {
    bool isProduction = bool.fromEnvironment('dart.vm.product');

    CreateEventParams params = CreateEventParams();
    params.parameters = {
      'message': message, // Required
      'title': title, // Required
      'ios_voip': 1, // Required for iOS VoIP push
      'push_badge': 1, // Updates app badge count
      'push_sound': 'default', // Plays default notification sound
      'custom_data': {
        'param1': 'value1', // Custom parameters (optional)
        'param2': 'value2',
      },
      'aps': {
        'alert': {'title': title, 'body': message},
        'sound': 'default',
        'content-available': 1 // Required for silent VoIP pushes
      }
    };

    params.notificationType = NotificationType.PUSH;
    params.environment =
        isProduction ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
    params.usersIds = ids;

    createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("SENT TO IDS ${ids.toString()}");
    }).catchError((error) {
      log("ERROR WHILE SEDING TO IDS");
    });
  }

  Future<void> unsubscribe() async {
    try {
      List<CubeSubscription> subscriptionsList = await getSubscriptions();
      log("SUBSCRIBED USER LIST ${subscriptionsList.length}");
      for (CubeSubscription subscription in subscriptionsList) {
        log("SUBSCRIPTION ID ${subscription.id}");
        await deleteSubscription(subscription.id!);
      }
      log("UNSUBSCRIBED SUC");
    } on Exception catch (e) {
      log("UNSUBSCRIBED ERROR ${e}");
    }
  }

  subscribeVoIP(String token) async {
    log('[subscribeVoIP] token: $token');

    CreateSubscriptionParameters parameters = CreateSubscriptionParameters();
    parameters.pushToken = token;

    if (Platform.isIOS) {
      parameters.channel = NotificationsChannels.APNS_VOIP;
      parameters.platform = CubePlatform.IOS;
    }

    String deviceId = await getDeviceId();
    parameters.udid = deviceId;
    parameters.environment =
        kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;

    var packageInfo = await PackageInfo.fromPlatform();
    parameters.bundleIdentifier = packageInfo.packageName;

    createSubscription(parameters.getRequestParameters())
        .then((cubeSubscriptions) {
      log('[subscribeVoIP] subscription SUCCESS');
    }).catchError((error) {
      log('[subscribeVoIP] subscription ERROR: $error');
    });
  }
}

I'm using ConnectyCube Flutter to implement video call functionality in my Flutter app.

The call notifications work fine on Android, but on iOS, I don't receive any notification when a call is initiated from another device.

What I Have Done So Far:

  • Configured Push Notifications:
  • Set up Firebase Cloud Messaging (FCM) for push notifications.
  • Uploaded the APNs certificate to ConnectyCube.
  • Checked iOS Permissions: Enabled Push Notifications capability in Xcode. Ensured NSUserNotificationAlert is added in the Info.plist.

Tested with Background & Terminated State: On Android, the notification is received properly. On iOS, no notification appears in both foreground and background states. Debugging Logs: No errors related to push notifications appear in the logs. Verified that FCM token is correctly generated on iOS.

below is my AndroidManifest file

    xmlns:tools="http://schemas.android/tools"

    package="com.sambuq.care_first">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />


    
    <!-- End FlutterDownloader customization -->
    <!-- Provide required visibility configuration for API level 30 and above -->
    <queries>
    <!-- If your app checks for SMS support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="sms" />
    </intent>
    <!-- If your app checks for call support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="tel" />
    </intent>

    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
    </queries>

   <application
        android:label="360CareFirst"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <provider
            android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
            android:authorities="${applicationId}.flutter_downloader.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
        <!-- ... (other configurations) -->
        <!-- Begin FlutterDownloader customization -->
        <!-- disable default Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>

        <!-- declare customized Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
            android:authorities="${applicationId}.flutter-downloader-init"
            android:exported="false">
            <meta-data
                android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
                android:value="1" />
        </provider> 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
        <service
        android:name="de.julianassmann.flutter_background.IsolateHolderService"
        android:enabled="true"
        android:exported="false"
        android:foregroundServiceType="mediaProjection" />
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

**call manager class **

import 'package:care_first/bloc/auth_bloc/bloc/auth_bloc.dart';
import 'package:care_first/routing/route_names.dart';
import 'package:care_first/screens/settings/my_appointments/doctor_appointment/models/my_appointment.dart';
import 'package:care_first/shared/helper/helper_methods.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_io/io.dart';
import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';

import 'call_kit_manager.dart';
import '../utils/consts.dart';

class CallManager {
  static String TAG = "CallManager";

  static CallManager get instance => _getInstance();
  static CallManager? _instance;

  static CallManager _getInstance() {
    return _instance ??= CallManager._internal();
  }

  factory CallManager() => _getInstance();

  CallManager._internal();

  P2PClient? _callClient;
  P2PSession? _currentCall;
  BuildContext? context;
  MediaStream? localMediaStream;
  Map<int, MediaStream> remoteStreams = {};
  GlobalKey<NavigatorState>? navigatorKey;
  String? selfCubeId;
  late String resourceId;
  SystemMessagesManager? systemMessagesManager =
      CubeChatConnection.instance.systemMessagesManager;
  CubeMessage systemMessage = CubeMessage();
  final player = AudioPlayer();

  Function(bool, String)? onMicMuted;

  init(
    BuildContext context,
    GlobalKey<NavigatorState> navigatorKey,
    String selfCubeId,
    String resourceId,
  ) {
    this.context = context;
    this.navigatorKey = navigatorKey;
    this.selfCubeId = selfCubeId;
    this.resourceId = resourceId;

    _initCustomMediaConfigs();

    if (CubeChatConnection.instance.isAuthenticated()) {
      _initCalls(context);
    } else {
      _initChatConnectionStateListener(context);
    }

    _initCallKit();
  }

  destroy() {
    _callClient?.destroy();
    _callClient = null;
  }

  void _initCustomMediaConfigs() {
    RTCMediaConfig mediaConfig = RTCMediaConfig.instance;
    mediaConfig.minHeight = 340;
    mediaConfig.minWidth = 480;
    mediaConfig.minFrameRate = 25;

    RTCConfig.instance.statsReportsInterval = 200;
  }

  void _initCalls(BuildContext context) {
    if (_callClient == null) {
      _callClient = P2PClient.instance;

      _callClient!.init();
    }
    // _callClient is P2PClient
    _callClient!.onReceiveNewSession = (callSession) async {
      print("${'*' * 10} RECEIVED NEW CALL SESSION ${'*' * 10}");
      // if (_currentCall != null &&
      //     _currentCall!.sessionId != callSession.sessionId &&
      //     _currentCall!.opponentsIds.first.toString() != selfCubeId) {
      //   callSession.reject();
      //   return;
      // }
      if (navigatorKey!.currentContext!.read<AuthBloc>().user.persona ==
          "patient") {
        await player
            .setReleaseMode(ReleaseMode.loop)
            .then(
                (value) => player.setSource(AssetSource('audio/ringtone.mp3')))
            .then((value) => player.resume());
      }

      _currentCall = callSession;

      var callState = await _getCallState(_currentCall!.sessionId);
      await Future.delayed(Duration(seconds: 1));
      if (callState == CallState.REJECTED) {
        reject(_currentCall!.sessionId, false);
      } else if (callState == CallState.ACCEPTED) {
        acceptCall(_currentCall!.sessionId,
            _currentCall?.cubeSdp.userInfo ?? {}, false);
      } else if (callState == CallState.UNKNOWN ||
          callState == CallState.PENDING) {
        if (callState == CallState.UNKNOWN &&
            (Platform.isIOS || Platform.isAndroid)) {
          await ConnectycubeFlutterCallKit.setCallState(
              sessionId: _currentCall!.sessionId, callState: CallState.PENDING);
        }

        if (_currentCall!.cubeSdp.callerId.toString() != selfCubeId) {
          navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
              callSession;
          // await Future.delayed(Duration(seconds: 1));
          // await ConnectycubeFlutterCallKit.showCallNotification(CallEvent(
          //   sessionId: _currentCall!.sessionId,
          //   callType: CallType.VIDEO_CALL,
          //   callerId: _currentCall!.cubeSdp.callerId,
          //   callerName: "YASH",
          //   opponentsIds: _currentCall!.opponentsIds,
          //   userInfo: _currentCall?.cubeSdp.userInfo ?? {},
          // ));
          print("NOTIFICATION NOT CALLED");
          _showIncomingCallScreen(_currentCall!, navigatorKey!.currentContext!);
        }
      }

      _currentCall?.onLocalStreamReceived = (localStream) {
        localMediaStream = localStream;
      };

      _currentCall?.onRemoteStreamReceived = (session, userId, stream) {
        remoteStreams[userId] = stream;
      };

      _currentCall?.onRemoteStreamRemoved = (session, userId, stream) {
        remoteStreams.remove(userId);
      };

      _currentCall?.onReceiveHungUpFromUser =
          (session, userId, [userInfo]) async {
        print("CALL_MANAGER: onReceiveHungUpFromUser");
        /* systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_HUNGUP";
        print("-------------BROADCASTING CALL HUNGUP EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
        broadcastSystemMessage("CALL_HUNGUP");
        await player.release();
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      };
    };

    _callClient!.onSessionClosed = (callSession) async {
      if (_currentCall != null &&
          _currentCall!.sessionId == callSession.sessionId) {
        _currentCall = null;
        localMediaStream?.getTracks().forEach((track) async {
          await track.stop();
        });
        await localMediaStream?.dispose();
        localMediaStream = null;

        remoteStreams.forEach((key, value) async {
          await value.dispose();
        });

        remoteStreams.clear();
        CallKitManager.instance.processCallFinished(callSession.sessionId);
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      }
    };
  }

  void startNewCall(BuildContext context, int callType, int patientCubeId,
      MyAppointment appointment, String? businessId, String? moduleId) async {
    this.context = context;
    Set<int> opponents = {patientCubeId};
    if (opponents.isEmpty) return;
    if (!kIsWeb) {
      if (Platform.isIOS) {
        Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
            preferSpeakerOutput: true);
      }
    }
    P2PSession callSession =
        _callClient!.createCallSession(callType, opponents);
    _currentCall = callSession;
    // storing it in authbloc
    context.read<AuthBloc>().p2pSession = callSession;
    context.read<AuthBloc>().resourceId = resourceId;
    context.read<AuthBloc>().selfCubeId = selfCubeId;

    String callerName;
    if (appointment.doctorMap?["firstname"] != null) {
      callerName =
          "${appointment.doctorMap?["firstname"]} ${appointment.doctorMap?["lastname"]}";
    } else {
      callerName = "";
    }

    _sendStartCallSignalForOffliners(_currentCall!, callerName);
    GoRouter.of(context)
        .pushNamed(RoutesName.videoCallDoctorRoute, pathParameters: {
      'businessId': encrypt(text: businessId ?? '', context: context),
    }, queryParameters: {
      'moduleId': moduleId,
    }, extra: {
      // 'callSession': callSession,
      'appointment': appointment,
    });
  }

  void _showIncomingCallScreen(P2PSession callSession, BuildContext context) {
    if (navigatorKey!.currentContext != null) {
      GoRouter.of(navigatorKey!.currentContext!).pushNamed(
          RoutesName.incomingVideoCall,
          extra: Map<String, String>.from(
              {"resourceId": resourceId, "selfCubeId": selfCubeId}));
    }
  }

  void acceptCall(
    String sessionId,
    Map<String, String> userInfo,
    bool fromCallkit,
  ) async {
    await player.release();
    log('acceptCall, from callKit: $fromCallkit', TAG);
    //ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);
    if (_currentCall != null) {
      if (context != null) {
        // if (AppLifecycleState.resumed !=
        //     WidgetsBinding.instance.lifecycleState) {
        await _currentCall?.acceptCall();
        // }
        MyAppointment appointment = MyAppointment(
            id: userInfo['appointment_id'], doctor: userInfo['doctor_id']);
        log(userInfo.toString(), "User Info from AcceptCall");
        if (!fromCallkit) {
          await ConnectycubeFlutterCallKit.reportCallAccepted(
              sessionId: sessionId);
        }
        broadcastSystemMessage("CALL_ACCEPTED");
        navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
            _currentCall;
        GoRouter.of(navigatorKey!.currentContext!).pushReplacementNamed(
          RoutesName.videoCallPatientRoute,
          extra: Map<String, Object>.from({
            // 'currentCall': _currentCall!,
            'appointment': appointment,
          }),
        );

        /*  systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_ACCEPTED";
        print("-------------BROADCASTING ACCEPT CALL EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
      }
      if (!kIsWeb) {
        if (Platform.isIOS) {
          await Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
              preferSpeakerOutput: true);
        }
      }
    }
  }

  void reject(String sessionId, bool fromCallkit) {
    if (_currentCall != null) {
      player.release();
      broadcastSystemMessage("CALL_REJECTED");
      if (fromCallkit) {
        ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: false);
      } else {
        CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      }

      _currentCall!.reject();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  void hungUp() {
    if (_currentCall != null) {
      player.release();
      CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      _currentCall!.hungUp();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  CreateEventParams _getCallEventParameters(
      P2PSession currentCall, String? callerName) {
    /* String? callerName = users
        .where((cubeUser) => cubeUser.id == currentCall.callerId)
        .first
        .fullName; */
    CreateEventParams params = CreateEventParams();
    params.parameters = {
      'message':
          "Incoming ${currentCall.callType == CallType.VIDEO_CALL ? "Video" : "Audio"} call",
      PARAM_CALL_TYPE: currentCall.callType,
      PARAM_SESSION_ID: currentCall.sessionId,
      PARAM_CALLER_ID: currentCall.callerId,
      PARAM_CALL_OPPONENTS: currentCall.opponentsIds.join(','),
      PARAM_CALLER_NAME: callerName ?? "Doctor",
    };

    params.notificationType = NotificationType.PUSH;
    params.environment = CubeEnvironment.DEVELOPMENT;
    //kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
    params.usersIds = currentCall.opponentsIds.toList();

    return params;
  }

  Future<void> _sendStartCallSignalForOffliners(
      P2PSession currentCall, String callerName) async {
    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_START_CALL;
    params.parameters[PARAM_IOS_VOIP] = 1;
    params.parameters[PARAM_EXPIRATION] = 0;
    params.parameters['ios_push_type'] = 'background';

    await createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created: $cubeEvent");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _sendEndCallSignalForOffliners(
      P2PSession? currentCall, String? callerName) {
    if (currentCall == null) return;

    CubeUser? currentUser = CubeChatConnection.instance.currentUser;
    if (currentUser == null || currentUser.id != currentCall.callerId) return;

    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_END_CALL;

    createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _initCallKit() {
    CallKitManager.instance.init(
      onCallAccepted: (uuid) {
        acceptCall(uuid, _currentCall?.cubeSdp.userInfo ?? {}, true);
      },
      onCallEnded: (uuid) {
        reject(uuid, true);
      },
      onMuteCall: (mute, uuid) {
        onMicMuted?.call(mute, uuid);
      },
    );
  }

  void _initChatConnectionStateListener(BuildContext context) {
    CubeChatConnection.instance.connectionStateStream.listen((state) {
      if (CubeChatConnectionState.Ready == state) {
        _initCalls(context);
      }
    });
  }

  Future<String> _getCallState(String sessionId) async {
    if (Platform.isAndroid || Platform.isIOS) {
      var callState =
          await ConnectycubeFlutterCallKit.getCallState(sessionId: sessionId);

      log("CONECTICUBE CALL STATE: $callState");
      return callState;
    } else {
      return Future.value(CallState.UNKNOWN);
    }
  }

  void muteCall(String sessionId, bool mute) {
    CallKitManager.instance.muteCall(sessionId, mute);
  }

  Future<bool> _onBackPressed(BuildContext context) {
    return Future.value(false);
  }

  void broadcastSystemMessage(action) {
    systemMessage.recipientId = int.tryParse(selfCubeId!);
    systemMessage.properties["resourceId"] = resourceId;
    systemMessage.properties["action"] = action;
    print("BROADCASTING: $action EVENT");
    systemMessagesManager?.sendSystemMessage(systemMessage);
  }
}

FCM SETUP CLASS

import 'package:connectycube_sdk/connectycube_sdk.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:universal_io/io.dart';
import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';

class FcmSetup {
  static FcmSetup? _instance;
  FcmSetup._internal();

  factory FcmSetup() {
    return _instance ??= FcmSetup._internal();
  }
  FirebaseMessaging? firebaseMessaging;

  Future<void> init({
    required Function(RemoteMessage remoteMessage) onMessage,
  }) async {
    firebaseMessaging = FirebaseMessaging.instance;
    log("INT STARTED");
    await firebaseMessaging!.requestPermission(
      alert: false,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );
    String? token;
    if (Platform.isAndroid || kIsWeb) {
      token = await firebaseMessaging!.getToken();
    } else if (Platform.isIOS || Platform.isMacOS) {
      token = await firebaseMessaging!.getAPNSToken();
      log("APNS TOKEN $token");
    }

    if (token != null) {
      subscribe(token);
    }
    if (Platform.isIOS || Platform.isMacOS) {
      String? voipToken = await ConnectycubeFlutterCallKit.getToken();
      log("VOIP TOKEN $voipToken");
      if (voipToken != null) {
        subscribeVoIP(voipToken);
      }
    }

    firebaseMessaging!.onTokenRefresh.listen((newToken) async {
      subscribe(newToken);
    });

    // FirebaseMessaging.onMessage.listen(onMessage);
    // FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
  }

  subscribe(String token) async {
    log('[subscribe] token: $token');

    bool isProduction = bool.fromEnvironment('dart.vm.product');

    CreateSubscriptionParameters parameters = CreateSubscriptionParameters();
    parameters.environment =
        isProduction ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;

    if (Platform.isAndroid) {
      parameters.channel = NotificationsChannels.GCM;
      parameters.platform = CubePlatform.ANDROID;
      parameters.bundleIdentifier = "com.360carefirst.app";
    } else if (Platform.isIOS) {
      parameters.channel = NotificationsChannels.APNS;
      parameters.platform = CubePlatform.IOS;
      parameters.bundleIdentifier = Platform.isIOS
          ? "com.360carefirst.app"
          : "com.connectycube.flutter.chatSample.macOS";
    }
    String deviceId = await getDeviceId();
    parameters.udid = deviceId;
    parameters.pushToken = token;

    createSubscription(parameters.getRequestParameters())
        .then((cubeSubscription) {
      getSubscriptions().then((subscriptions) {
        log("Subscriptions: ${subscriptions.toString()}");
      });
      log("SUBSCRIPTION CREATED");
    }).catchError((error) {
      log("SUBSCRIPTION ERROR ${error}");
    });
  }

  Future<String> getDeviceId() async {
    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    if (Platform.isAndroid) {
      AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
      return androidInfo.id; // Unique ID for Android
    } else if (Platform.isIOS) {
      IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
      return iosInfo.identifierForVendor!;
    }
    return '';
  }

  void sendPushNotification(List<int> ids, String message, String title) {
    bool isProduction = bool.fromEnvironment('dart.vm.product');

    CreateEventParams params = CreateEventParams();
    params.parameters = {
      'message': message, // Required
      'title': title, // Required
      'ios_voip': 1, // Required for iOS VoIP push
      'push_badge': 1, // Updates app badge count
      'push_sound': 'default', // Plays default notification sound
      'custom_data': {
        'param1': 'value1', // Custom parameters (optional)
        'param2': 'value2',
      },
      'aps': {
        'alert': {'title': title, 'body': message},
        'sound': 'default',
        'content-available': 1 // Required for silent VoIP pushes
      }
    };

    params.notificationType = NotificationType.PUSH;
    params.environment =
        isProduction ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
    params.usersIds = ids;

    createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("SENT TO IDS ${ids.toString()}");
    }).catchError((error) {
      log("ERROR WHILE SEDING TO IDS");
    });
  }

  Future<void> unsubscribe() async {
    try {
      List<CubeSubscription> subscriptionsList = await getSubscriptions();
      log("SUBSCRIBED USER LIST ${subscriptionsList.length}");
      for (CubeSubscription subscription in subscriptionsList) {
        log("SUBSCRIPTION ID ${subscription.id}");
        await deleteSubscription(subscription.id!);
      }
      log("UNSUBSCRIBED SUC");
    } on Exception catch (e) {
      log("UNSUBSCRIBED ERROR ${e}");
    }
  }

  subscribeVoIP(String token) async {
    log('[subscribeVoIP] token: $token');

    CreateSubscriptionParameters parameters = CreateSubscriptionParameters();
    parameters.pushToken = token;

    if (Platform.isIOS) {
      parameters.channel = NotificationsChannels.APNS_VOIP;
      parameters.platform = CubePlatform.IOS;
    }

    String deviceId = await getDeviceId();
    parameters.udid = deviceId;
    parameters.environment =
        kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;

    var packageInfo = await PackageInfo.fromPlatform();
    parameters.bundleIdentifier = packageInfo.packageName;

    createSubscription(parameters.getRequestParameters())
        .then((cubeSubscriptions) {
      log('[subscribeVoIP] subscription SUCCESS');
    }).catchError((error) {
      log('[subscribeVoIP] subscription ERROR: $error');
    });
  }
}

Share Improve this question asked Mar 7 at 8:47 yash guptayash gupta 871 silver badge8 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

At first need to check the guide how to configure Push Notifications. After all setup check with some of call samples. Also take a look at similar issue at github issue

But again, it's important to look at official guide and double check all needed configuration settings.

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

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

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

关注微信