Skip to content

Call failed screen appears after receiving a call #870

@malekmajzoubHT

Description

@malekmajzoubHT

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions