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.
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
- Perform the manual linking of the
react-native-incall-manager
.
SelectYour_Xcode_Project/TARGETS/BuildSettings
, in Header Search Paths, add"$(SRCROOT)/../node_modules/@videosdk.live/react-native-incall-manager/ios/RNInCallManager"
- Update the
Podfile
in theios
directory to include thereact-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
- Create a Firebase iOS App within the Firebase Project.
- Download and add
GoogleService-info.plist
files to the project
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.
- Visit the Apple Developer Member Center
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.
3. On the following page, add a Key Name, and select APNs.
4. Click on the Register button.
5. You can download your auth key file from this page and upload this file to the Firebase dashboard without changing its name.
6. In your Firebase project, go to Settings
and select the Cloud Messaging
tab. Scroll down iOS app configuration
and click Upload under APNs Authentication Key
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.
8. Enable Push Notifications in Capabilities
9. Enable selected permission in Background Modes
CallKeep Setup
- 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
- 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.
- 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
- 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
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.
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.