Skip to main content
Implement VoIP push notifications to receive incoming calls even when your app is in the background or terminated. This requires platform-specific configuration for iOS CallKit and Android ConnectionService.

iOS VoIP Configuration

Enable VoIP Push Notifications

  1. In Xcode, select your target
  2. Go to Signing & Capabilities
  3. Add Push Notifications capability
  4. Add Background Modes capability
  5. Enable Voice over IP

Create VoIP Certificate

  1. Go to Apple Developer Portal
  2. Navigate to Certificates, Identifiers & Profiles
  3. Create a new VoIP Services Certificate
  4. Download and install the certificate
  5. Export the .p12 file for your server

Configure CometChat Dashboard

  1. Go to your CometChat Dashboard
  2. Navigate to Notifications > Push Notifications
  3. Upload your VoIP certificate (.p12 file)
  4. Configure the certificate password

Implement CallKit

Create a native module to handle CallKit:
// ios/CallKitManager.swift
import CallKit
import PushKit

@objc(CallKitManager)
class CallKitManager: NSObject, CXProviderDelegate, PKPushRegistryDelegate {
    
    static let shared = CallKitManager()
    
    private let provider: CXProvider
    private let callController = CXCallController()
    private var voipRegistry: PKPushRegistry?
    
    override init() {
        let config = CXProviderConfiguration()
        config.supportsVideo = true
        config.maximumCallsPerCallGroup = 1
        config.supportedHandleTypes = [.generic]
        
        provider = CXProvider(configuration: config)
        super.init()
        provider.setDelegate(self, queue: nil)
    }
    
    @objc func registerForVoIPPushes() {
        voipRegistry = PKPushRegistry(queue: .main)
        voipRegistry?.delegate = self
        voipRegistry?.desiredPushTypes = [.voIP]
    }
    
    // PKPushRegistryDelegate
    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined()
        // Send token to CometChat
        NotificationCenter.default.post(
            name: NSNotification.Name("VoIPTokenReceived"),
            object: nil,
            userInfo: ["token": token]
        )
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        guard type == .voIP else { return }
        
        let callerId = payload.dictionaryPayload["callerId"] as? String ?? "Unknown"
        let callerName = payload.dictionaryPayload["callerName"] as? String ?? "Unknown"
        let sessionId = payload.dictionaryPayload["sessionId"] as? String ?? ""
        let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool ?? false
        
        reportIncomingCall(
            uuid: UUID(),
            handle: callerId,
            callerName: callerName,
            hasVideo: hasVideo
        ) { error in
            completion()
        }
    }
    
    func reportIncomingCall(uuid: UUID, handle: String, callerName: String, hasVideo: Bool, completion: @escaping (Error?) -> Void) {
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: handle)
        update.localizedCallerName = callerName
        update.hasVideo = hasVideo
        
        provider.reportNewIncomingCall(with: uuid, update: update) { error in
            completion(error)
        }
    }
    
    // CXProviderDelegate
    func providerDidReset(_ provider: CXProvider) {}
    
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        // Notify React Native to accept the call
        NotificationCenter.default.post(
            name: NSNotification.Name("CallKitAnswerCall"),
            object: nil,
            userInfo: ["callUUID": action.callUUID.uuidString]
        )
        action.fulfill()
    }
    
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        // Notify React Native to end the call
        NotificationCenter.default.post(
            name: NSNotification.Name("CallKitEndCall"),
            object: nil,
            userInfo: ["callUUID": action.callUUID.uuidString]
        )
        action.fulfill()
    }
}

Android VoIP Configuration

Add Firebase Cloud Messaging

  1. Add Firebase to your Android project
  2. Add the FCM dependency to android/app/build.gradle:
dependencies {
    implementation 'com.google.firebase:firebase-messaging:23.0.0'
}

Configure CometChat Dashboard

  1. Go to your CometChat Dashboard
  2. Navigate to Notifications > Push Notifications
  3. Upload your Firebase Server Key

Implement ConnectionService

Create a ConnectionService for incoming calls:
// android/app/src/main/java/com/yourapp/CallConnectionService.java
package com.yourapp;

import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;

public class CallConnectionService extends ConnectionService {
    
    @Override
    public Connection onCreateIncomingConnection(
            PhoneAccountHandle connectionManagerPhoneAccount,
            ConnectionRequest request) {
        
        CallConnection connection = new CallConnection();
        connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
        connection.setCallerDisplayName(
            request.getExtras().getString("callerName"),
            TelecomManager.PRESENTATION_ALLOWED
        );
        connection.setRinging();
        
        return connection;
    }
    
    @Override
    public Connection onCreateOutgoingConnection(
            PhoneAccountHandle connectionManagerPhoneAccount,
            ConnectionRequest request) {
        
        CallConnection connection = new CallConnection();
        connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
        connection.setDialing();
        
        return connection;
    }
}

Register ConnectionService

Add to AndroidManifest.xml:
<service
    android:name=".CallConnectionService"
    android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
    android:exported="true">
    <intent-filter>
        <action android:name="android.telecom.ConnectionService" />
    </intent-filter>
</service>

Add Permissions

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

Register Push Token

Register the VoIP/FCM token with CometChat:
import { CometChat } from '@cometchat/chat-sdk-react-native';

async function registerPushToken(token: string, platform: 'ios' | 'android') {
  try {
    if (platform === 'ios') {
      await CometChat.registerTokenForPushNotification(token, {
        voip: true,
      });
    } else {
      await CometChat.registerTokenForPushNotification(token);
    }
    console.log('Push token registered');
  } catch (error) {
    console.error('Error registering push token:', error);
  }
}

Handle Incoming VoIP Push

import { useEffect } from 'react';
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import { CometChat } from '@cometchat/chat-sdk-react-native';
import { CometChatCalls } from '@cometchat/calls-sdk-react-native';

function useVoIPPush() {
  useEffect(() => {
    if (Platform.OS === 'ios') {
      const eventEmitter = new NativeEventEmitter(NativeModules.CallKitManager);
      
      const answerSubscription = eventEmitter.addListener(
        'CallKitAnswerCall',
        async (data) => {
          // Accept the call via Chat SDK
          const sessionId = data.sessionId;
          await CometChat.acceptCall(sessionId);
          
          // Start the call session
          const { token } = await CometChatCalls.generateToken(sessionId);
          // Navigate to call screen with token
        }
      );
      
      const endSubscription = eventEmitter.addListener(
        'CallKitEndCall',
        async (data) => {
          CometChatCalls.leaveSession();
        }
      );
      
      return () => {
        answerSubscription.remove();
        endSubscription.remove();
      };
    }
  }, []);
}

export default useVoIPPush;