Let us start with our second part for the implementation of the React Native Video Call App using Call Keep. If you have not checked the first part, it's 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 the previous article, we managed to get the video calling up and running on the Android Devices. You can get the complete code at our GitHub repository for the series.

In this part, we will focus on tweaking the implementation to provide support for iOS devices as well. So without any more delay, let's jump right into it.

Build a React Native Android Video Calling App with Callkeep using Firebase and Video SDK
In this tutorial, you’ll learn how to make a react native video calling app with callkeep using the firebase and video SDK.
Video SDK Image

Libraries

We will need one additional library that 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 - This library is used to send push notifications on iOS devices, as the Firebase notifications do not function well on iOS devices when the app is in a killed state.

Client Side Setup

Let's 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 access to the camera and microphone.

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

Firebase Setup

  1. Create a Firebase iOS App within the Firebase Project.
  2. Download and add GoogleService-info.plist files to the 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 to the iOS device for which, You must upload an APN Auth Key to implement push notifications. We need the following details about the app when sending push notifications via an APN Auth Key:

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

To create an APN 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 on 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 the 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  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 the following code changes to support call keep. These delegates will help 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 the following code changes to support call keep. These delegates will help us 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 it under functions directory which we generated from Apple Dev and uploaded to Firebase in the client setup. This will help us with VoIP push notifications.


Client Side Code

With our library all set, let's 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 while showing UI in the 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 let's 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 on an iOS basis.

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 devices should now be able to receive the call and join the video call. This is what the incoming call on an 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 call keep 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