react-native-call-keeper
Version:
A React Native module for handling VoIP calls with CallKit (iOS) and ConnectionService (Android) support. Compatible with the New Architecture and Expo.
456 lines (326 loc) • 10.8 kB
Markdown
# react-native-call-keeper
A modern React Native module for handling VoIP calls with CallKit (iOS) and ConnectionService (Android) support. Built with the New Architecture (TurboModules) and fully compatible with Expo.
## Features
- ✅ **CallKit** integration for iOS
- ✅ **ConnectionService** integration for Android
- ✅ **New Architecture** (TurboModules) support
- ✅ **Expo** compatible with config plugin
- ✅ Full **TypeScript** support
- ✅ Display incoming call UI
- ✅ Start outgoing calls
- ✅ Answer/Reject calls
- ✅ Mute/Hold functionality
- ✅ DTMF support
- ✅ Video call support
- ✅ Event-driven architecture
## Installation
### For bare React Native projects:
```bash
npm install react-native-call-keeper
# or
yarn add react-native-call-keeper
```
For iOS, install pods:
```bash
cd ios && pod install
```
### For Expo projects:
```bash
npx expo install react-native-call-keeper
```
Add the plugin to your `app.json` or `app.config.js`:
```json
{
"expo": {
"plugins": ["react-native-call-keeper"]
}
}
```
Then rebuild your development client:
```bash
npx expo prebuild
npx expo run:ios
# or
npx expo run:android
```
## Platform-specific Setup
### iOS Setup
Add the following to your `Info.plist`:
```xml
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
<string>audio</string>
</array>
```
> Note: If using the Expo config plugin, this is done automatically.
### Android Setup
The required permissions and service declarations are automatically added by the library. However, you need to request runtime permissions in your app:
```typescript
import { PermissionsAndroid, Platform } from 'react-native';
if (Platform.OS === 'android') {
await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE,
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
PermissionsAndroid.PERMISSIONS.READ_CALL_LOG,
PermissionsAndroid.PERMISSIONS.WRITE_CALL_LOG,
]);
}
```
## Usage
### Setup
First, initialize the module with your app configuration:
```typescript
import CallKeeper from 'react-native-call-keeper';
await CallKeeper.setup({
appName: 'MyApp',
imageName: 'logo', // iOS only: image name in your asset catalog
ringtoneSound: 'ringtone.mp3', // iOS only
includesCallsInRecents: true,
supportsVideo: true,
maximumCallGroups: 1,
maximumCallsPerCallGroup: 1,
});
```
### Display Incoming Call
```typescript
import CallKeeper from 'react-native-call-keeper';
const callUUID = 'unique-call-id'; // Generate with uuid library
await CallKeeper.displayIncomingCall(
callUUID,
'+1234567890', // caller handle
'John Doe', // caller name
'number', // handleType: 'generic' | 'number' | 'email'
false // hasVideo
);
```
### Start Outgoing Call
```typescript
await CallKeeper.startCall(
callUUID,
'+1234567890',
'John Doe',
'number',
false
);
```
### Answer Call
```typescript
await CallKeeper.answerIncomingCall(callUUID);
```
### End Call
```typescript
await CallKeeper.endCall(callUUID);
```
### Mute/Unmute
```typescript
await CallKeeper.setMutedCall(callUUID, true); // mute
await CallKeeper.setMutedCall(callUUID, false); // unmute
```
### Hold/Unhold
```typescript
await CallKeeper.setOnHold(callUUID, true); // hold
await CallKeeper.setOnHold(callUUID, false); // unhold
```
### Report Call Status
```typescript
// Report outgoing call connected
await CallKeeper.reportConnectedOutgoingCall(callUUID);
// Report call ended with reason
// Reasons: 1=failed, 2=remote ended, 3=unanswered, 4=answered elsewhere, 5=declined elsewhere
await CallKeeper.reportEndCallWithUUID(callUUID, 2);
```
## Events
Listen to call events:
```typescript
import CallKeeper from 'react-native-call-keeper';
// Answer call event
CallKeeper.addEventListener('answerCall', ({ callUUID }) => {
console.log('Call answered:', callUUID);
// Start your VoIP session here
});
// End call event
CallKeeper.addEventListener('endCall', ({ callUUID }) => {
console.log('Call ended:', callUUID);
// Clean up your VoIP session
});
// Start call event (outgoing)
CallKeeper.addEventListener('didReceiveStartCallAction', ({ callUUID, handle }) => {
console.log('Starting call to:', handle);
// Initiate your VoIP call
});
// Mute event
CallKeeper.addEventListener('didPerformSetMutedCallAction', ({ callUUID, muted }) => {
console.log('Mute changed:', muted);
});
// Hold event
CallKeeper.addEventListener('didToggleHoldAction', ({ callUUID, hold }) => {
console.log('Hold changed:', hold);
});
// Audio session activated (iOS)
CallKeeper.addEventListener('didActivateAudioSession', () => {
console.log('Audio session activated');
});
// DTMF event
CallKeeper.addEventListener('didPerformDTMFAction', ({ callUUID, digits }) => {
console.log('DTMF digits:', digits);
});
```
### Cleanup
Remember to remove event listeners when your component unmounts:
```typescript
// Remove specific listener
CallKeeper.removeEventListener('answerCall');
// Remove all listeners
CallKeeper.removeAllListeners();
```
## Complete Example
```typescript
import React, { useEffect } from 'react';
import { View, Button } from 'react-native';
import CallKeeper from 'react-native-call-keeper';
import { v4 as uuidv4 } from 'uuid';
function App() {
const [callUUID, setCallUUID] = React.useState<string | null>(null);
useEffect(() => {
// Setup CallKeeper
CallKeeper.setup({
appName: 'MyApp',
supportsVideo: false,
includesCallsInRecents: true,
});
// Setup event listeners
CallKeeper.addEventListener('answerCall', handleAnswerCall);
CallKeeper.addEventListener('endCall', handleEndCall);
CallKeeper.addEventListener('didReceiveStartCallAction', handleStartCall);
return () => {
CallKeeper.removeAllListeners();
};
}, []);
const handleAnswerCall = ({ callUUID }: { callUUID: string }) => {
console.log('User answered call:', callUUID);
// Connect your VoIP session
};
const handleEndCall = ({ callUUID }: { callUUID: string }) => {
console.log('Call ended:', callUUID);
setCallUUID(null);
// Disconnect your VoIP session
};
const handleStartCall = ({ callUUID, handle }: { callUUID: string; handle: string }) => {
console.log('Starting call to:', handle);
// Initiate your VoIP connection
};
const displayIncomingCall = async () => {
const uuid = uuidv4();
setCallUUID(uuid);
await CallKeeper.displayIncomingCall(
uuid,
'+1234567890',
'John Doe',
'number',
false
);
};
const startOutgoingCall = async () => {
const uuid = uuidv4();
setCallUUID(uuid);
await CallKeeper.startCall(
uuid,
'+1234567890',
'John Doe',
'number',
false
);
};
const endCurrentCall = async () => {
if (callUUID) {
await CallKeeper.endCall(callUUID);
setCallUUID(null);
}
};
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
<Button title="Display Incoming Call" onPress={displayIncomingCall} />
<Button title="Start Outgoing Call" onPress={startOutgoingCall} />
<Button title="End Call" onPress={endCurrentCall} disabled={!callUUID} />
</View>
);
}
export default App;
```
## API Reference
### Methods
#### `setup(options: CallKeeperOptions): Promise<void>`
Initialize the CallKeeper module.
**Options:**
- `appName` (string, required): Your app name
- `imageName` (string, optional): iOS only - icon name from asset catalog
- `ringtoneSound` (string, optional): iOS only - ringtone filename
- `includesCallsInRecents` (boolean, optional): Add calls to recents
- `supportsVideo` (boolean, optional): Enable video call support
- `maximumCallGroups` (number, optional): Max concurrent call groups
- `maximumCallsPerCallGroup` (number, optional): Max calls per group
#### `displayIncomingCall(callUUID, handle, localizedCallerName?, handleType?, hasVideo?): Promise<void>`
Display an incoming call notification.
#### `startCall(callUUID, handle, contactIdentifier?, handleType?, hasVideo?): Promise<void>`
Start an outgoing call.
#### `endCall(callUUID): Promise<void>`
End a specific call.
#### `endAllCalls(): Promise<void>`
End all active calls.
#### `answerIncomingCall(callUUID): Promise<void>`
Answer an incoming call programmatically.
#### `rejectCall(callUUID): Promise<void>`
Reject an incoming call.
#### `setMutedCall(callUUID, muted): Promise<void>`
Set mute status for a call.
#### `setOnHold(callUUID, onHold): Promise<void>`
Put a call on hold or resume.
#### `reportConnectedOutgoingCall(callUUID): Promise<void>`
Report that an outgoing call has connected.
#### `reportEndCallWithUUID(callUUID, reason): Promise<void>`
Report that a call has ended with a specific reason.
#### `updateDisplay(callUUID, displayName, handle): Promise<void>`
Update the display information for an active call.
#### `checkPermissions(): Promise<boolean>`
Check if the app has necessary permissions.
#### `checkIsInManagedCall(): Promise<boolean>`
Check if there's an active managed call.
#### `backToForeground(): Promise<void>`
Bring the app to foreground.
### Events
- `didReceiveStartCallAction` - Outgoing call started
- `answerCall` - Incoming call answered
- `endCall` - Call ended
- `didActivateAudioSession` - Audio session activated (iOS)
- `didDisplayIncomingCall` - Incoming call displayed
- `didPerformSetMutedCallAction` - Mute status changed
- `didToggleHoldAction` - Hold status changed
- `didPerformDTMFAction` - DTMF tone played
- `didResetProvider` - Provider reset (iOS)
## New Architecture Support
This library is built with full support for React Native's New Architecture (TurboModules). The module will automatically use the new architecture if your app has it enabled.
### Enable New Architecture
For React Native 0.70+, set in your `android/gradle.properties`:
```properties
newArchEnabled=true
```
And in your Podfile:
```ruby
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
```
## Troubleshooting
### iOS: Calls not showing up
Make sure you have added the required background modes to your Info.plist and have configured VoIP push notifications properly.
### Android: Missing permissions
Request runtime permissions before using the module. Check the Android Setup section for required permissions.
### Expo: Module not found
Make sure you have run `npx expo prebuild` after installing the package and adding the plugin to your app.json.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
## License
MIT
## Credits
Built with ❤️ for the React Native community
# react-native-call-keeper