react-native-webrtc / react-native-webrtc/react-native-callkeep
Call failed screen appears after receiving a call
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.1k
- Forks
- 511
- Avg merge
- 9h 12m
- Merged PRs (30d)
- 2
Description
I am having an issue on IOS where it shows "Call failed" after receiving a call while the app is killed and phone is locked.
The steps causing this issue:
- I receive a call on IOS while the app is killed and phone is locked
- It shows the native incoming call screen
- I answer and immediately end the call before connecting to it
- The caller receives rejected status since the call ended before it started (correct)
- The caller calls again the IOS phone immediately
- The IOS phone receives the call for 1 second and then shows "Call failed"
- The call keeps ringing on the callers side
My AppDelegate.swift
import Expo
import FirebaseCore
import React
import ReactAppDependencyProvider
import Firebase
import PushKit
import CallKit
import Intents
import FirebaseMessaging
import UserNotifications
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
// CallKit properties
private var callObserver: CXCallObserver?
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
public override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Firebase configuration
FirebaseApp.configure()
// Firebase Messaging notification delegate setup
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications()
// RNCallKeep setup
RNCallKeep.setup([
"appName": "App Name",
"supportsVideo": true,
"imageName": "callLogo"
])
// Call observer setup
callObserver = CXCallObserver()
callObserver?.setDelegate(self, queue: nil)
// LiveKit setup (place this above any other RN related initialization)
LivekitReactNative.setup()
// React Native setup
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
bindReactNativeFactory(factory)
// VoIP registration
RNVoipPushNotificationManager.voipRegistration()
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - Linking API
public override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
// MARK: - Universal Links and Call Intents
public override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
// Handle Siri call intents
if userActivity.activityType == "INStartAudioCallIntent" ||
userActivity.activityType == "INStartVideoCallIntent" {
if let interaction = userActivity.interaction {
let intent = interaction.intent
let isVideo = userActivity.activityType == "INStartVideoCallIntent"
var contact: INPerson?
if let audioCallIntent = intent as? INStartCallIntent {
contact = audioCallIntent.contacts?.first
} else if let videoCallIntent = intent as? INStartCallIntent {
contact = videoCallIntent.contacts?.first
}
if let contact = contact,
let handle = contact.personHandle?.value {
let callUUID = UUID().uuidString
let videoParam = isVideo ? "true" : "false"
let callUrl = "appname://call?handle=\(handle)&video=\(videoParam)&fromContacts=true&uuid=\(callUUID)"
DispatchQueue.main.async {
if let url = URL(string: callUrl) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
return true
}
}
}
// Handle RNCallKeep
let handledCK = RNCallKeep.application(
application,
continue: userActivity,
restorationHandler: { activities in
let typedActivities = activities as? [UIUserActivityRestoring]
restorationHandler(typedActivities)
}
)
// Handle RCTLinkingManager
let handledLM = RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
let handledSuper = super.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
return handledCK || handledLM || handledSuper
}
// MARK: - Background Task Management
private func startBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
}
private func endBackgroundTask() {
if backgroundTask != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
private func handleCallEnded(with uuid: UUID) {
RNCallKeep.endCall(withUUID: uuid.uuidString, reason: 1)
endBackgroundTask()
}
// MARK: - Remote Notifications
public override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Messaging.messaging().apnsToken = deviceToken
}
public override func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Messaging.messaging().appDidReceiveMessage(userInfo)
completionHandler(.newData)
}
}
// MARK: - PKPushRegistryDelegate (VoIP)
extension AppDelegate: PKPushRegistryDelegate {
public func pushRegistry(
_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType
) {
RNVoipPushNotificationManager.didUpdate(
pushCredentials,
forType: type.rawValue
)
}
public func pushRegistry(
_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType
) {
print("VoIP Push Token invalidated for type: \(type.rawValue)")
}
public func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void
) {
let dictionaryPayload = payload.dictionaryPayload
// Extract call information with safe defaults
let uuid = (dictionaryPayload["callUUID"] as? String) ?? UUID().uuidString
let callerName = (dictionaryPayload["name"] as? String) ?? "Unknown Caller"
let handle = (dictionaryPayload["id"] as? String) ?? "Unknown"
let hasVideo = (dictionaryPayload["video"] as? NSNumber)?.boolValue ?? false
// Add completion handler
RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion)
// Notify about incoming push
RNVoipPushNotificationManager.didReceiveIncomingPush(
with: payload,
forType: type.rawValue
)
// Report new incoming call
RNCallKeep.reportNewIncomingCall(
uuid,
handle: handle,
handleType: "number",
hasVideo: hasVideo,
localizedCallerName: callerName,
supportsHolding: true,
supportsDTMF: true,
supportsGrouping: true,
supportsUngrouping: true,
fromPushKit: true,
payload: dictionaryPayload,
withCompletionHandler: completion
)
// End call if UUID is nil (should not happen with our logic above, but keeping for safety)
if dictionaryPayload["callUUID"] == nil {
RNCallKeep.endCall(withUUID: uuid, reason: 1)
}
completion()
}
}
// MARK: - CXCallObserverDelegate
extension AppDelegate: CXCallObserverDelegate {
public func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) {
print("[CallObserver] event from call with UUID: \(call.uuid)")
if call.hasEnded {
print("[CallObserver] call \(call.uuid) ended")
startBackgroundTask()
handleCallEnded(with: call.uuid)
}
}
}
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with AppDelegate.swift, especially the PKPushRegistryDelegate and CXCallObserverDelegate entry points, and reproduce the sequence with the app killed and the phone locked. Trace the first call's end and the immediate second push through CallKit and PushKit; done means the second call does not show "Call failed" and caller-side ringing ends correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ios, react-native, swift
- Domain
- audio-video-rtc, mobile
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100