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
- 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 the access of camera and microphone.
<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>
Firebase Setup
- Create Firebase iOS App within Firebase Project.
- Download and add
GoogleService-info.plist
file to 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 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.
- 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 in 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 Firebase dashboard without changing its name.
6. In your firebase project, go to Settings
and select the Cloud Messaging
tab. Scroll down to 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 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
- 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.
- 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
- 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
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.
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.