Skip to content

Call failed screen appears after receiving a call #870

@malekmajzoubFC

Description

@malekmajzoubFC

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions