React Native CallKeep

npm version npm downloads

React Native CallKeep utilises a brand new iOS 10 framework CallKit and Android ConnectionService to make the life easier for VoIP developers using React Native.

For more information about CallKit on iOS, please see Official CallKit Framework Document or Introduction to CallKit by Xamarin

For more information about ConnectionService on Android, please see Android Documentation and Build a calling app

Demo

A demo of react-native-callkeep is available in the wazo-react-native-demo repository.

Android

Connection Service

iOS

Connection Service

Installation

npm install --save react-native-callkeep
# or
yarn add react-native-callkeep

Usage

Setup

import RNCallKeep from 'react-native-callkeep';

const options = {
  ios: {
    appName: 'My app name',
  },
  android: {
    alertTitle: 'Permissions required',
    alertDescription: 'This application needs to access your phone accounts',
    cancelButton: 'Cancel',
    okButton: 'ok',
    imageName: 'phone_account_icon',
    additionalPermissions: [PermissionsAndroid.PERMISSIONS.example]
  }
};

RNCallKeep.setup(options).then(accepted => {});

Constants

To make passing the right integer into methods easier, there are constants that are exported from the module.

const CONSTANTS = {
  END_CALL_REASONS: {
    FAILED: 1,
    REMOTE_ENDED: 2,
    UNANSWERED: 3,
    ANSWERED_ELSEWHERE: 4,
    DECLINED_ELSEWHERE: 5,
    MISSED: 6
  }
};

const { CONSTANTS as CK_CONSTANTS, RNCallKeep } from 'react-native-callkeep';

console.log(CK_CONSTANTS.END_CALL_REASONS.FAILED) // outputs 1

Methods

setAvailable

This feature is available only on Android.

Tell ConnectionService that the device is ready to make outgoing calls via the native Phone app. If not the user will be stuck in the build UI screen without any actions. Eg: Call it with false when disconnected from the sip client, when your token expires, when your user log out ... Eg: When your used log out (or the connection to your server is broken, etc..), you have to call setAvailable(false) so CallKeep will refuse the call and your user will not be stuck in the native UI.

RNCallKeep.setAvailable(true);

setCurrentCallActive

This feature is available only on Android.

Mark the current call as active (eg: when the callee has answered). Necessary to set the correct Android capabilities (hold, mute) once the call is set as active. Be sure to set this only after your call is ready for two way audio; used both incoming and outgoing calls.

RNCallKeep.setCurrentCallActive(uuid);

isCallActive

This feature is available only on IOS.

Returns true if the UUID passed matches an existing and answered call. This will return true ONLY if the call exists and the user has already answered the call. It will return false if the call does not exist or has not been answered. This is exposed to both React Native and Native sides. This was exposed so a call can be canceled if ringing and the user answered on a different device.

RNCallKeep.isCallActive(uuid);

displayIncomingCall

Display system UI for incoming calls

RNCallKeep.displayIncomingCall(uuid, handle, localizedCallerName);

answerIncomingCall

This feature is available only on Android.

Use this to tell the sdk a user answered a call from the app UI.

RNCallKeep.answerIncomingCall(uuid)

startCall

When you make an outgoing call, tell the device that a call is occurring. The argument list is slightly different on iOS and Android:

iOS:

RNCallKeep.startCall(uuid, handle, contactIdentifier, handleType, hasVideo);

Android:

RNCallKeep.startCall(uuid, handle, contactIdentifier);

updateDisplay

Use this to update the display after an outgoing call has started.

RNCallKeep.updateDisplay(uuid, displayName, handle)

endCall

When finish an incoming/outgoing call.
(When user actively chooses to end the call from your app's UI.)

RNCallKeep.endCall(uuid);

endAllCalls

End all ongoing calls.

RNCallKeep.endAllCalls();

rejectCall

When you reject an incoming call.

RNCallKeep.rejectCall(uuid);

reportEndCallWithUUID

Report that the call ended without the user initiating.
(Not ended by user, is usually due to the following reasons)

RNCallKeep.reportEndCallWithUUID(uuid, reason);

setMutedCall

Switch the mic on/off.

RNCallKeep.setMutedCall(uuid, true);

setOnHold

Set a call on/off hold.

RNCallKeep.setOnHold(uuid, true)

checkIfBusy

Checks if there are any active calls on the device and returns a promise with a boolean value (true if there're active calls, false otherwise). This feature is available only on iOS.

RNCallKeep.checkIfBusy();

checkSpeaker

Checks if the device speaker is on and returns a promise with a boolean value (true if speaker is on, false otherwise). This feature is available only on iOS.

RNCallKeep.checkSpeaker();

supportConnectionService (async)

Tells if ConnectionService is available on the device (returns a boolean).

This feature is available only on Android.

RNCallKeep.supportConnectionService();

hasPhoneAccount (async)

Checks if the user has enabled the phone account for your application. A phone account must be enable to be able to display UI screen on incoming call and make outgoing calls from native Contact application.

Returns a promise of a boolean.

This feature is available only on Android.

await RNCallKeep.hasPhoneAccount();

hasOutgoingCall (async)

This feature is available only on Android, useful when waking up the application for an outgoing call.

When waking up the Android application in background mode (eg: when the application is killed and the user make a call from the native Phone application). The user can hang up the call before your application has been started in background mode, and you can lost the RNCallKeepPerformEndCallAction event.

To be sure that the outgoing call is still here, you can call hasOutgoingCall when you app waken up.

const hasOutgoingCall = await RNCallKeep.hasOutgoingCall();

hasDefaultPhoneAccount

Checks if the user has set a default phone account. If the user has not set a default they will be prompted to do so with an alert.

This is a workaround for an issue affecting some Samsung devices.

This feature is available only on Android.

const options = {
  alertTitle: 'Default not set',
  alertDescription: 'Please set the default phone account'
};

RNCallKeep.hasDefaultPhoneAccount(options);

backToForeground

This feature is available only on Android.

Use this to display the application in foreground if the application was in background state. This method will open the application if it was closed.

RNCallKeep.backToForeground();

Events

didReceiveStartCallAction

Device sends this event once it decides the app is allowed to start a call, either from the built-in phone screens (iOS/Recents, Android/Contact), or by the app calling RNCallKeep.startCall.

Try to start your app call action from here (e.g. get credentials of the user by data.handle and/or send INVITE to your SIP server)

Note: on iOS callUUID is not defined as the call is not yet managed by CallKit. You have to generate your own and call startCall.

RNCallKeep.addEventListener('didReceiveStartCallAction', ({ handle, callUUID, name }) => {

});

- answerCall

User answer the incoming call

RNCallKeep.addEventListener('answerCall', ({ callUUID }) => {
  // Do your normal `Answering` actions here.
});

- endCall

User finish the call.

RNCallKeep.addEventListener('endCall', ({ callUUID }) => {
  // Do your normal `Hang Up` actions here
});

- didActivateAudioSession

The AudioSession has been activated by RNCallKeep.

RNCallKeep.addEventListener('didActivateAudioSession', () => {
  // you might want to do following things when receiving this event:
  // - Start playing ringback if it is an outgoing call
});

- didDisplayIncomingCall

Callback for RNCallKeep.displayIncomingCall

RNCallKeep.addEventListener('didDisplayIncomingCall', ({ error, callUUID, handle, localizedCallerName, hasVideo, fromPushKit, payload }) => {
  // you might want to do following things when receiving this event:
  // - Start playing ringback if it is an outgoing call
});

- didPerformSetMutedCallAction

A call was muted by the system or the user:

RNCallKeep.addEventListener('didPerformSetMutedCallAction', ({ muted, callUUID }) => {

});

- didToggleHoldCallAction

A call was held or unheld by the current user

RNCallKeep.addEventListener('didToggleHoldCallAction', ({ hold, callUUID }) => {

});

- didPerformDTMFAction

Used type a number on his dialer

RNCallKeep.addEventListener('didPerformDTMFAction', ({ digits, callUUID }) => {

});

- checkReachability

On Android when the application is in background, after a certain delay the OS will close every connection with informing about it. So we have to check if the application is reachable before making a call from the native phone application.

RNCallKeep.addEventListener('checkReachability', () => {
  RNCallKeep.setReachable();
});

removeEventListener

Allows to remove the listener on an event.

RNCallKeep.removeEventListener('checkReachability');

Example

A full example is available in the example folder.

import React from 'react';
import RNCallKeep from 'react-native-callkeep';
import uuid from 'uuid';

class RNCallKeepExample extends React.Component {
  constructor(props) {
    super(props);

    this.currentCallId = null;

    // Add RNCallKeep Events
    RNCallKeep.addEventListener('didReceiveStartCallAction', this.didReceiveStartCallAction);
    RNCallKeep.addEventListener('answerCall', this.onAnswerCallAction);
    RNCallKeep.addEventListener('endCall', this.onEndCallAction);
    RNCallKeep.addEventListener('didDisplayIncomingCall', this.onIncomingCallDisplayed);
    RNCallKeep.addEventListener('didPerformSetMutedCallAction', this.onToggleMute);
    RNCallKeep.addEventListener('didToggleHoldCallAction', this.onToggleHold);
    RNCallKeep.addEventListener('didPerformDTMFAction', this.onDTMFAction);
    RNCallKeep.addEventListener('didActivateAudioSession', this.audioSessionActivated);
  }

  // Initialise RNCallKeep
  setup = () => {
    const options = {
      ios: {
        appName: 'ReactNativeWazoDemo',
        imageName: 'sim_icon',
        supportsVideo: false,
        maximumCallGroups: '1',
        maximumCallsPerCallGroup: '1'
      },
      android: {
        alertTitle: 'Permissions Required',
        alertDescription:
          'This application needs to access your phone calling accounts to make calls',
        cancelButton: 'Cancel',
        okButton: 'ok',
        imageName: 'sim_icon',
        additionalPermissions: [PermissionsAndroid.PERMISSIONS.READ_CONTACTS]
      }
    };

    try {
      RNCallKeep.setup(options);
      RNCallKeep.setAvailable(true); // Only used for Android, see doc above.
    } catch (err) {
      console.error('initializeCallKeep error:', err.message);
    }
  }

  // Use startCall to ask the system to start a call - Initiate an outgoing call from this point
  startCall = ({ handle, localizedCallerName }) => {
    // Your normal start call action
    RNCallKeep.startCall(this.getCurrentCallId(), handle, localizedCallerName);
  };

  reportEndCallWithUUID = (callUUID, reason) => {
    RNCallKeep.reportEndCallWithUUID(callUUID, reason);
  }

  // Event Listener Callbacks

  didReceiveStartCallAction(data) => {
    let { handle, callUUID, name } = data;
    // Get this event after the system decides you can start a call
    // You can now start a call from within your app
  };

  onAnswerCallAction = (data) => {
    let { callUUID } = data;
    // Called when the user answers an incoming call
  };

  onEndCallAction = (data) => {
    let { callUUID } = data;
    RNCallKeep.endCall(this.getCurrentCallId());

    this.currentCallId = null;
  };

  // Currently iOS only
  onIncomingCallDisplayed = (data) => {
    let { error } = data;
    // You will get this event after RNCallKeep finishes showing incoming call UI
    // You can check if there was an error while displaying
  };

  onToggleMute = (data) => {
    let { muted, callUUID } = data;
    // Called when the system or user mutes a call
  };

  onToggleHold = (data) => {
    let { hold, callUUID } = data;
    // Called when the system or user holds a call
  };

  onDTMFAction = (data) => {
    let { digits, callUUID } = data;
    // Called when the system or user performs a DTMF action
  };

  audioSessionActivated = (data) => {
    // you might want to do following things when receiving this event:
    // - Start playing ringback if it is an outgoing call
  };

  getCurrentCallId = () => {
    if (!this.currentCallId) {
      this.currentCallId = uuid.v4();
    }

    return this.currentCallId;
  };

  render() {
  }
}

Receiving a call when the application is not reachable.

In some case your application can be unreachable :

To be able to wake up your application to display the incoming call, you can use https://github.com/ianlin/react-native-voip-push-notification on iOS or BackgroundMessaging from react-native-firebase-(Optional)(Android-only)-Listen-for-FCM-messages-in-the-background).

You have to send a push to your application, like with Firebase for Android and with a library supporting PushKit pushes for iOS.

PushKit

Since iOS 13, you'll have to report the incoming calls that wakes up your application with a VoIP push. Add this in your AppDelegate.m if you're using VoIP pushes to wake up your application :

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  // Process the received push
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];

  // Retrieve information like handle and callerName here
  // NSString *uuid = /* fetch for payload or ... */ [[[NSUUID UUID] UUIDString] lowercaseString];
  // NSString *callerName = @"caller name here";
  // NSString *handle = @"caller number here";
  // NSDictionary *extra = [payload.dictionaryPayload valueForKeyPath:@"custom.path.to.data"]; /* use this to pass any special data (ie. from your notification) down to RN. Can also be `nil` */

  [RNCallKeep reportNewIncomingCall:uuid handle:handle handleType:@"generic" hasVideo:false localizedCallerName:callerName fromPushKit: YES payload:extra withCompletionHandler:completion];
}

Debug

Android

adb logcat *:S RNCallKeepModule:V

Troubleshooting

Contributing

Any pull request, issue report and suggestion are highly welcome!

License

This work is dual-licensed under ISC and MIT. Previous work done by @ianlin on iOS is on ISC Licence. We choose MIT for the rest of the project.

SPDX-License-Identifier: ISC OR MIT