-
Notifications
You must be signed in to change notification settings - Fork 493
Open
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)
}
}
}
Metadata
Metadata
Assignees
Labels
No labels