Let us start with our second part for the implementation of React Native Video Call App using Call Keep. If you have not checked the first part, its recommended to start from there.

Just to give a quick recap, we started out by understating what is Call Keep and what it does and then understanding the flow and functioning of the app. Next, we moved on to installing and setting up the libraries for Android devices. By the end of previous article, we managed to get the video calling up and running in the Android Devices. You can get the complete code at our github repository for these series.

In these part, we will focus on tweaking the implementation to provide the support for iOS devices as well. So without anymore delay, lets jump right into it.

Libraries

We will need one additional library which will handle the push notifications on the iOS devices, since there are a few cases where the firebase notification fails.

React Native VoIP Push Notification - These library is used to send push notification on the iOS devices, as the Firebase notifications do not function well on iOS device when the app is in killed state.

Client Side Setup

Lets install the library we discussed above by using the command:

npm install react-native-voip-push-notification

iOS Setup

VideoSDK Setup

  1. Perform the manual linking of the react-native-incall-manager.
    Select Your_Xcode_Project/TARGETS/BuildSettings, in Header Search Paths, add "$(SRCROOT)/../node_modules/@videosdk.live/react-native-incall-manager/ios/RNInCallManager"
  2. Update the Podfile in the ios directory to include the react-native-webrtc
pod 'react-native-webrtc', :path => '../node_modules/@videosdk.live/react-native-webrtc'

3. Update the platform field to 12.0 as react-native-webrtc does not support iOS < 12.

platform: ios, '12.0'

4. Declare the permissions in the Info.plist to allow the access of camera and microphone.

<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>

Firebase Setup

  1. Create Firebase iOS App within Firebase Project.
  2. Download and add GoogleService-info.plist file to project
Video SDK Image

3. Update the Podfile in the ios directory to include the Firebase.

pod 'Firebase', :modular_headers => true
pod 'FirebaseCoreInternal', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true
#import <Firebase.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	 //...
     
    // Add these line in the start
	[FIRApp configure];
    
    //...
}

PushKit Setup

PushKit will allow us to send the notifications in the iOS device for which, You must upload an APNs Auth Key in order to implement push notifications. We need the following details about the app when sending push notifications via an APNs Auth Key:

  • Auth Key file
  • Team ID
  • Key ID
  • Your app’s bundle ID

To create an APNs auth key, follow the steps below.

  1. Visit the Apple Developer Member Center
Video SDK Image

2. Click on Certificates, Identifiers & Profiles. Go to Keys from the left side. Create a new Auth Key by clicking on the plus button in the top right side.

Video SDK Image

3. On the following page, add a Key Name, and select APNs.

Video SDK Image

4. Click on the Register button.

Video SDK Image

5. You can download your auth key file from this page and upload this file to Firebase dashboard without changing its name.

Video SDK Image

6. In your firebase project, go to Settings and select the Cloud Messaging tab. Scroll down to iOS app configurationand click upload under APNs Authentication Key

Video SDK Image

7. Enter Key ID and Team ID. Key ID is in the file name, AuthKey_{Key ID}.p8 and is 10 characters. Your Team ID is in the Apple Member Center under the membership tab or displayed always under your account name in the top right corner.

Video SDK Image

8. Enable Push Notifications in Capabilities

Video SDK Image
Video SDK Image

9. Enable selected permission in Background Modes

Video SDK Image

CallKeep Setup

  1. Update the Podfile with the Call Keep library.
pod 'RNCallKeep', :path => '../node_modules/react-native-callkeep'

2.  Update the ios/YourProject/Info.plist file to allow deep linking.

<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>videocalling</string>
        <key>CFBundleURLSchemes</key>
        <array>
        	<string>videocalling</string>
        </array>
    </dict>
</array>

3.  Update the ios/YourProject/AppDelegate.m file with following code changes to support call keep. These delegates will help use invoke the React Native CallKeep.

#import "RNCallKeep.h"
#import <React/RCTLinkingManager.h>

//Update the tehse deleegate with the CallKeep setup
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

  [FIRApp configure];
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  
  //Add these Lines
  [RNCallKeep setup:@{
      @"appName": @"VideoSDK Call Trigger",
      @"maximumCallGroups": @3,
      @"maximumCallsPerCallGroup": @1,
      @"supportsVideo": @YES,
    }];
  
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"ReactNativeCallTrigger"
                                            initialProperties:nil];

  if (@available(iOS 13.0, *)) {
      rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
      rootView.backgroundColor = [UIColor whiteColor];
  }

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

//Add below delegate to handle invocking of call
 - (BOOL)application:(UIApplication *)application
 continueUserActivity:(NSUserActivity *)userActivity
   restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler
 {
   return [RNCallKeep application:application
            continueUserActivity:userActivity
              restorationHandler:restorationHandler];
 }

//Add below delegate to allow deep linking
- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

VoIP Push Notification Setup

  1. Update the ios/YourProject/AppDelegate.m file with following code changes to support call keep. These delegates will help use with to receive the VoIP Push Notification
#import <PushKit/PushKit.h>
#import "RNVoipPushNotificationManager.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  //...
    
  // Add these line to regiser voip
  [RNVoipPushNotificationManager voipRegistration];
  
  //...
}


- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
  // Register VoIP push token (a property of PKPushCredentials) with server
  [RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];
}

- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type
{
  // --- The system calls this method when a previously provided push token is no longer valid for use. No action is necessary on your part to reregister the push type. Instead, use this method to notify your server not to send push notifications using the matching push token.
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  

  // --- NOTE: apple forced us to invoke callkit ASAP when we receive voip push
  // --- see: react-native-callkeep

  // --- Retrieve information from your voip push payload
  NSString *uuid = payload.dictionaryPayload[@"uuid"];
  NSString *callerName = [NSString stringWithFormat:@"%@ Calling from VideoSDK", payload.dictionaryPayload[@"callerName"]];
  NSString *handle = payload.dictionaryPayload[@"handle"];

  // --- this is optional, only required if you want to call `completion()` on the js side
  [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];

  // --- Process the received push
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
//  NSDictionary *extra = [payload.dictionaryPayload valueForKeyPath:@"custom.path.to.data"];

  [RNCallKeep reportNewIncomingCall: uuid
                               handle: handle
                           handleType: @"generic"
                             hasVideo: YES
                  localizedCallerName: callerName
                      supportsHolding: YES
                         supportsDTMF: YES
                     supportsGrouping: YES
                   supportsUngrouping: YES
                          fromPushKit: YES
                              payload: nil
                withCompletionHandler: completion];
  
  // --- You don't need to call it if you stored `completion()` and will call it on the js side.
  completion();
}

Server Side Setup

You have to add AuthKey_{Key ID}.p8 under functions directory which we generated from Apple Dev and upload it to Firebase in client setup. This will helps us in VoIP push notification.


Client Side Code

With our library all set, lets make the required changes on the app side.

  1. Let us start by storing the APN token in the firestore. To do that update the getFCMToken() and declare the state for the APN.
const [APN, setAPN] = useState(null);
APNRef.current = APN;
const APNRef = useRef();


//replace the getFCMToken() with below.
async function getFCMtoken() {
    const authStatus = await messaging().requestPermission();
    const enabled =
          authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
          authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    //Register the APN Token.
    Platform.OS === "ios" && VoipPushNotification.registerVoipToken();

    if (enabled) {
        const token = await messaging().getToken();
        const querySnapshot = await firestore()
        .collection("users")
        .where("token", "==", token)
        .get();

        const uids = querySnapshot.docs.map((doc) => {
            if (doc && doc?.data()?.callerId) {

                //We added the APN to the Data and firebaseUserConfig.
                const { token, platform, APN, callerId } = doc?.data();
                setfirebaseUserConfig({
                    callerId,
                    token,
                    platform,
                    APN,
                });
            }
            return doc;
        });

        if (uids && uids.length == 0) {
            addUser({ token });
        } else {
            console.log("Token Found");
        }
    }
}

2. Update the addUser() to set the generated APN token in the firestore database.

const addUser = ({ token }) => {
    const platform = Platform.OS === "android" ? "ANDROID" : "iOS";
    const obj = {
      callerId: Math.floor(10000000 + Math.random() * 90000000).toString(),
      token,
      platform,
    };
    
    //We will add the APN to firestore
    if (platform == "iOS") {
      obj.APN = APNRef.current;
    }
    firestore()
      .collection("users")
      .add(obj)
      .then(() => {
        setfirebaseUserConfig(obj);
        console.log("User added!");
      });
  };

3. Now we will listen to the VoipPushNotification for the notification event and initiate the call.

useEffect(() => {
    VoipPushNotification.addEventListener("register", (token) => {
      setAPN(token);
    });

    VoipPushNotification.addEventListener("notification", (notification) => {
      const { callerInfo, videoSDKInfo, type } = notification;
      if (type === "CALL_INITIATED") {
        const incomingCallAnswer = ({ callUUID }) => {
          updateCallStatus({
            callerInfo,
            type: "ACCEPTED",
          });
          navigation.navigate(SCREEN_NAMES.Meeting, {
            name: "Person B",
            token: videoSDKInfo.token,
            meetingId: videoSDKInfo.meetingId,
          });
        };
        const endIncomingCall = () => {
          Incomingvideocall.endAllCall();
          updateCallStatus({ callerInfo, type: "REJECTED" });
        };
        Incomingvideocall.configure(incomingCallAnswer, endIncomingCall);
      } else if (type === "DISCONNECT") {
        Incomingvideocall.endAllCall();
      }
      VoipPushNotification.onVoipNotificationCompleted(notification.uuid);
    });

    VoipPushNotification.addEventListener("didLoadWithEvents", (events) => {
      const { callerInfo, videoSDKInfo, type } =
        events.length > 1 && events[1].data;
      if (type === "CALL_INITIATED") {
        const incomingCallAnswer = ({ callUUID }) => {
          updateCallStatus({
            callerInfo,
            type: "ACCEPTED",
          });
          navigation.navigate(SCREEN_NAMES.Meeting, {
            name: "Person B",
            token: videoSDKInfo.token,
            meetingId: videoSDKInfo.meetingId,
          });
        };

        const endIncomingCall = () => {
          Incomingvideocall.endAllCall();
          updateCallStatus({ callerInfo, type: "REJECTED" });
        };

        Incomingvideocall.configure(incomingCallAnswer, endIncomingCall);
      }
    });

    return () => {
      VoipPushNotification.removeEventListener("didLoadWithEvents");
      VoipPushNotification.removeEventListener("register");
      VoipPushNotification.removeEventListener("notification");
    };
  }, []);

4. Inside the index.js file, update the AppRegistry with a HeadlessCheck so that iOS will be able to launch the app with showing UI in background.

function HeadlessCheck({ isHeadless }) {
  if (isHeadless) {
    // App has been launched in the background by iOS, ignore
    return null;
  }

  return <App />;
}

AppRegistry.registerComponent(appName, () => HeadlessCheck);

With these the client side code is all set. But we need our firebase function to send the APN notification instead of the simple FCM notification. So lets update the firbase function to do the same.

Server Side Code

  1. Add the required node-apn library by running:
npm install https://github.com/node-apn/node-apn.git

2.  Add the imports for the AuthKey and apn.

var apn = require("apn");
var Key = "./AuthKey_{KEY ID}.p8";

3.  Inside our initiate-call API we will check for the platform and send the notification for iOS basis that.

app.post("/initiate-call", (req, res) => {
  const { calleeInfo, callerInfo, videoSDKInfo } = req.body;

  //Check for the platform and send the notification accordingly.
  if (calleeInfo.platform === "iOS") {
    let deviceToken = calleeInfo.APN;
    var options = {
      token: {
        key: Key,
        keyId: "YOUR_KEY_ID",
        teamId: "YOUR_TEAM_ID",
      },
      production: true,
    };

    var apnProvider = new apn.Provider(options);

    var note = new apn.Notification();

    note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
    note.badge = 1;
    note.sound = "ping.aiff";
    note.alert = "You have a new message";
    note.rawPayload = {
      callerName: callerInfo.name,
      aps: {
        "content-available": 1,
      },
      handle: callerInfo.name,
      callerInfo,
      videoSDKInfo,
      type: "CALL_INITIATED",
      uuid: uuidv4(),
    };
    note.pushType = "voip";
    note.topic = "org.reactjs.ReactNativeCallTrigger.voip";
    apnProvider.send(note, deviceToken).then((result) => {
      if (result.failed && result.failed.length > 0) {
        console.log("RESULT", result.failed[0].response);
        res.status(400).send(result.failed[0].response);
      } else {
        res.status(200).send(result);
      }
    });
  } else if (calleeInfo.platform === "ANDROID") {
    var FCMtoken = calleeInfo.token;
    const info = JSON.stringify({
      callerInfo,
      videoSDKInfo,
      type: "CALL_INITIATED",
    });
    var message = {
      data: {
        info,
      },
      android: {
        priority: "high",
      },
      token: FCMtoken,
    };
    FCM.send(message, function (err, response) {
      if (err) {
        res.status(200).send(response);
      } else {
        res.status(400).send(response);
      }
    });
  } else {
    res.status(400).send("Not supported platform");
  }
});

4. In the above API, you will have to update TEAM_ID and KEY_ID in the above code which you can get from the Firebase Project Setting > Cloud Messaging

Video SDK Image

With these, iOS device should now be able to receive the call and join the video call. These is how the incoming call on iOS device looks like.

Video SDK Image
Congratulations!!! You made the complete video calling app which works both on Android and iOS devices.

Here is the video showing the incoming call and initiating a video session

Conclusion

With this, we successfully built the react native video calling app with callkeep using the video SDK and firebase. You can always refer to our documentation if you want to add features like chat messaging and screen sharing. If you have any problems with the implementation, please contact us via our Discord community.

Video SDK Image