> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS APNs Push Notifications

> Implement APNs push notifications with CometChat UIKit for iOS, including CallKit integration for VoIP calls.

<Card title="iOS UI Kit Sample App" icon="github" href="https://github.com/cometchat/cometchat-uikit-ios/tree/v5/SampleAppPushNotificationAPNs/Push%20Notification%20%2B%20VoIP">
  Reference implementation of iOS UIKit, APNs and Push Notification Setup.
</Card>

## What this guide covers

* CometChat dashboard setup (enable push, add APNs Device + APNs VoIP providers) with screenshots.
* APNs + PushKit/CallKit wiring (tokens, delegates, CallKit).
* Incoming message/call handling and deep links.
* Badge count and grouped notifications.
* Payload customization and testing.

## How APNs + CometChat work together

* **APNs is the transport:** Apple issues the APNs device/VoIP tokens and delivers the payloads. No FCM bridge is involved.
* **CometChat providers:** The APNs Device and APNs VoIP providers you add in the CometChat dashboard hold your APNs key/cert. When you call `CometChatNotifications.registerPushToken(..., .APNS_IOS_DEVICE / .APNS_IOS_VOIP, providerId)` after login, CometChat binds those tokens to the logged-in user and sends to APNs for you.
* **Flow:** Permission prompt → APNs returns device + VoIP tokens → after `CometChat.login`, register both tokens with the matching provider IDs → CometChat sends to APNs → APNs delivers → `UNUserNotificationCenterDelegate` (and `PushKit`/`CallKit` for VoIP) surface the notification/tap.

## 1. Enable push and add providers (CometChat Dashboard)

1. Go to **Notifications → Settings** and enable **Push Notifications**.

<Frame>
  <img src="https://mintcdn.com/cometchat-22654f5b/NuY3hD_g_g_X-fwH/images/80a520bb-pushnotification-enable-e64632d479a2ebba111453b95bd522c6.png?fit=max&auto=format&n=NuY3hD_g_g_X-fwH&q=85&s=6c50d7c706ee0833ad673d81a0f972b8" alt="Enable Push Notifications" width="1202" height="607" data-path="images/80a520bb-pushnotification-enable-e64632d479a2ebba111453b95bd522c6.png" />
</Frame>

2. Click **Add Credentials**:
   * Add an **APNs Device** provider (alerts) using your `.p8` key, Team ID, Key ID, and Bundle ID; copy the Provider ID.
   * Add an **APNs VoIP** provider (calls) with the same `.p8` (recommended for CallKit reliability); copy the Provider ID.

<Warning>
  **`.p12` certificates are deprecated.** Apple recommends using `.p8` Auth Keys for push notifications. `.p8` keys are simpler to manage (one key works for all your apps), never expire, and are the only format actively supported going forward. If you are still using `.p12`, migrate to `.p8` at your earliest convenience.
</Warning>

<Frame>
  <img src="https://mintcdn.com/cometchat-22654f5b/F0pRX00dbgkztAbW/images/1133421a-add-apns-credentials-f8c2fd747ab4cf3905c6d13cf215b4c1.png?fit=max&auto=format&n=F0pRX00dbgkztAbW&q=85&s=85ab021e5605537cd85badaae0322e52" alt="Add APNs credentials" width="1800" height="1200" data-path="images/1133421a-add-apns-credentials-f8c2fd747ab4cf3905c6d13cf215b4c1.png" />
</Frame>

Keep the provider IDs—you’ll paste them into your app constants.

## 2. Apple setup

1. Capabilities: Push Notifications, Background Modes → Remote notifications & Voice over IP, CallKit usage descriptions in `Info.plist` (mic/camera).
2. APNs Auth Key: generate `.p8` (or use cert), note **Key ID**, **Team ID**, and **Bundle ID**; upload to CometChat providers.

<Frame>
  <img src="https://mintcdn.com/cometchat-22654f5b/HxM9nuCOhaEOdKpp/images/notification-capabilities-apns.png?fit=max&auto=format&n=HxM9nuCOhaEOdKpp&q=85&s=cacdc1a5e718b8ece82db876b58e9108" alt="Enable Push Notifications and Background Modes for APNs" width="2034" height="760" data-path="images/notification-capabilities-apns.png" />
</Frame>

## 3. Wiring APNs + PushKit/CallKit

* From below code, copy `CometChatAPNsHelper.swift`, `CometChatPNHelper.swift`, and the two `AppDelegate` extensions (`AppDelegate+PN.swift` and `AppDelegate+VoIP.swift`) into your project.
* These files implement APNs + PushKit/CallKit handling, notification presentation, tap and quick-reply actions, and call management.
* Update bundle ID, team ID, and provider IDs (`AppConstants.PROVIDER_ID` etc.). Keep the `voip` push type.

<Tabs>
  <Tab title="AppDelegate + PN.swift">
    ```swift lines theme={null}
    import Foundation
    import UIKit
    import CometChatSDK
    import CometChatUIKitSwift

    extension AppDelegate: UNUserNotificationCenterDelegate {

        // MARK: - Foreground Notifications
        func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
            print("willPresent notification: \(notification.request.content.userInfo)")
            let userInfo = notification.request.content.userInfo

            if CometChatPNHelper.shouldPresentNotification(userInfo: userInfo) == false {
                print("Suppressing notification (user is in active chat)")
                completionHandler([])
                return
            }

            completionHandler([.banner, .badge, .sound])
        }

        // MARK: - Notification Tap/Interaction
        func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

            let userInfo = response.notification.request.content.userInfo
            print("User tapped notification: \(userInfo)")

            if response.actionIdentifier == "REPLY_ACTION" {
                if let textResponse = response as? UNTextInputNotificationResponse {
                    let userReply = textResponse.userText
                    print("Quick reply: \(userReply)")
                    CometChatPNHelper.handleQuickReplyActionOnNotification(userInfo: userInfo, text: userReply, completionHandler: completionHandler)
                }
                completionHandler()
                return
            }

            CometChatPNHelper.handleTapActionOnNotification(userInfo: userInfo, completionHandler: completionHandler)
        }

    }

    ```
  </Tab>

  <Tab title="AppDelegate + VoIP.swift">
    ```swift lines theme={null}
    #if canImport(CometChatCallsSDK)

    import Foundation
    import PushKit
    import CallKit
    import AVFoundation
    import CometChatSDK
    import CometChatCallsSDK

    extension AppDelegate: PKPushRegistryDelegate, CXProviderDelegate {

      // MARK: - VoIP Push Token Updates

      func pushRegistry(
          _ registry: PKPushRegistry,
          didUpdate pushCredentials: PKPushCredentials,
          for type: PKPushType
      ) {
          print("VoIP token updated for type: \(type.rawValue)")
          cometchatAPNsHelper.registerForVoIPCalls(pushCredentials: pushCredentials)
      }

      func pushRegistry(
          _ registry: PKPushRegistry,
          didInvalidatePushTokenFor type: PKPushType
      ) {
          print("VoIP push token invalidated for type: \(type.rawValue)")
          initializePushKit()
          refreshPushCredentials()
      }

      // MARK: - PushKit Setup

      func initializePushKit() {
          if pushRegistry == nil {
              let registry = PKPushRegistry(queue: DispatchQueue.main)
              registry.delegate = self
              registry.desiredPushTypes = [.voIP]
              pushRegistry = registry
              print("Push registry initialized")
          } else {
              print("Push registry already initialized")
          }
      }

      func refreshPushCredentials() {
          guard let registry = pushRegistry else {
              print("Push registry is nil")
              return
          }

          registry.desiredPushTypes = []
          registry.desiredPushTypes = [.voIP]
          print("VoIP token refreshed")
      }

      // MARK: - Incoming VoIP Push

      func pushRegistry(
          _ registry: PKPushRegistry,
          didReceiveIncomingPushWith payload: PKPushPayload,
          for type: PKPushType,
          completion: @escaping () -> Void
      ) {
          print("Incoming VoIP push received")
          let provider = cometchatAPNsHelper.didReceiveIncomingPushWith(payload: payload)
          provider?.setDelegate(self, queue: nil)
          completion()
      }

      // MARK: - CallKit Delegates

      func providerDidReset(_ provider: CXProvider) {
          print("CallKit provider did reset")
          cometchatAPNsHelper.onProviderDidReset(provider: provider)
      }

      func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
          print("User answered call")

          // CRITICAL: Configure audio session BEFORE answering
          configureAudioSession()

          cometchatAPNsHelper.onAnswerCallAction(action: action)
      }

      func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
          print("User ended call")
          cometchatAPNsHelper.onEndCallAction(action: action)
          action.fulfill()
      }

      func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
          print("User toggled mute: \(action.isMuted)")
          CometChatCalls.audioMuted(action.isMuted)
          action.fulfill()
      }

      // MARK: - CRITICAL: Audio Session Delegates (MISSING IN YOUR CODE)

      /// Called when CallKit activates the audio session
      func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
          print("Audio session activated")

          // Configure audio session for VoIP
          configureAudioSession()

          // Removed CometChatCalls.startAudioSession() as per instructions
      }

      /// Called when CallKit deactivates the audio session
      func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
          print("Audio session deactivated")

          // Removed CometChatCalls.stopAudioSession() as per instructions
      }

      // MARK: - Audio Session Configuration

      /// Configure AVAudioSession for VoIP calls
      private func configureAudioSession() {
          let audioSession = AVAudioSession.sharedInstance()

          do {
              // Set category for VoIP with speaker and bluetooth support
              try audioSession.setCategory(
                  .playAndRecord,
                  mode: .voiceChat,
                  options: [.allowBluetooth, .allowBluetoothA2DP]
              )

              // Activate the session
              try audioSession.setActive(true)

              print("Audio session configured successfully")

          } catch {
              print("Failed to configure audio session: \(error.localizedDescription)")
          }
      }
    }

    #endif
    ```
  </Tab>

  <Tab title="CometChatAPNsHelper.swift">
    ```swift lines theme={null}
    import Foundation
    import UIKit
    import CometChatSDK
    import CometChatUIKitSwift
    import PushKit
    import CallKit
    import AVFAudio

    #if canImport(CometChatCallsSDK)
    import CometChatCallsSDK
    #endif

    class CometChatAPNsHelper {

        var uuid: UUID?
        var activeCall: Call?
        var cancelCall: Bool = true
        var onCall = true
        var callController = CXCallController()
        let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
        var provider: CXProvider? = nil

        // MARK: - Configure Push Notifications
        public func configurePushNotification(application: UIApplication, delegate: AppDelegate) {

            print("Configuring Push Notifications...")

            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: { granted, error in
                    print("Push notification authorization granted: \(granted)")
                    if let error = error {
                        print("Authorization error: \(error.localizedDescription)")
                    } else if granted {
                        print("User granted notification permissions")
                        // Register for remote notifications on main thread
                        DispatchQueue.main.async {
                            UIApplication.shared.registerForRemoteNotifications()
                        }
                    } else {
                        print("User denied notification permissions")
                    }
                })

            // Define the reply action
            let replyAction = UNTextInputNotificationAction(
                identifier: "REPLY_ACTION",
                title: "Reply",
                options: [],
                textInputButtonTitle: "Send",
                textInputPlaceholder: "Type your reply here"
            )

            // Define the notification category
            let messageCategory = UNNotificationCategory(
                identifier: "MESSAGE_CATEGORY",
                actions: [replyAction],
                intentIdentifiers: [],
                options: []
            )

            // Register the category
            UNUserNotificationCenter.current().setNotificationCategories([messageCategory])

            // Add login listener
            CometChat.addLoginListener("loginlistener-pnToken-register-login", self)

            #if canImport(CometChatCallsSDK)
            let voipRegistry: PKPushRegistry = PKPushRegistry(queue: DispatchQueue.main)
            voipRegistry.delegate = (delegate as? PKPushRegistryDelegate)
            voipRegistry.desiredPushTypes = [PKPushType.voIP]
            CometChatCallEvents.addListener("loginlistener-pnToken-register-login", self)
            #endif
        }

        // MARK: - Register APNs Token
        public func registerTokenForPushNotification(deviceToken: Data) {
            guard CometChat.getLoggedInUser() != nil else {
                print("Cannot register token: User not logged in")
                return
            }

            let hexString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
            UserDefaults.standard.set(hexString, forKey: "apnspuToken")
            print("APNs token: \(hexString)")

            CometChatNotifications.registerPushToken(
                pushToken: hexString,
                platform: CometChatNotifications.PushPlatforms.APNS_IOS_DEVICE,
                providerId: AppConstants.PROVIDER_ID,
                onSuccess: { success in
                    print("APNs token registered successfully: \(success)")
                },
                onError: { error in
                    print("APNs token registration failed: \(error.errorCode) - \(error.errorDescription)")
                }
            )
        }

        // MARK: - Register Pending Token (After Login)
        private func registerPendingTokenIfNeeded() {
            if let pendingToken = UserDefaults.standard.string(forKey: "pendingAPNsToken") {
                print("Registering pending APNs token after login...")
                if let tokenData = hexStringToData(pendingToken) {
                    registerTokenForPushNotification(deviceToken: tokenData)
                    UserDefaults.standard.removeObject(forKey: "pendingAPNsToken")
                }
            }
        }

        private func hexStringToData(_ string: String) -> Data? {
            let len = string.count / 2
            var data = Data(capacity: len)
            for i in 0..<len {
                let j = string.index(string.startIndex, offsetBy: i*2)
                let k = string.index(j, offsetBy: 2)
                let bytes = string[j..<k]
                if var num = UInt8(bytes, radix: 16) {
                    data.append(&num, count: 1)
                } else {
                    return nil
                }
            }
            return data
        }

        // MARK: - VoIP Registration
        public func registerForVoIPCalls(pushCredentials: PKPushCredentials) {
            guard CometChat.getLoggedInUser() != nil else {
                print("Cannot register VoIP token: User not logged in")
                return
            }

            let deviceToken = pushCredentials.token.reduce("", {$0 + String(format: "%02X", $1) })
            UserDefaults.standard.set(deviceToken, forKey: "voipToken")
            print("VoIP token: \(deviceToken)")

            CometChatNotifications.registerPushToken(
                pushToken: deviceToken,
                platform: CometChatNotifications.PushPlatforms.APNS_IOS_VOIP,
                providerId: AppConstants.PROVIDER_ID,
                onSuccess: { success in
                    print("VoIP token registered successfully: \(success)")
                },
                onError: { error in
                    print("VoIP token registration failed: \(error.errorCode) - \(error.errorDescription)")
                }
            )
        }

        // MARK: - Missed Call Notification
        public func handleMissedCallNotification(payload: [AnyHashable: Any]) {
            guard let senderName = payload["senderName"] as? String,
                  let senderAvatar = payload["senderAvatar"] as? String else {
                print("Missing required payload fields for missed call")
                return
            }

            let content = UNMutableNotificationContent()
            if let avatarURL = URL(string: senderAvatar), let imageData = try? Data(contentsOf: avatarURL) {
                do {
                    let fileManager = FileManager.default
                    let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
                    let fileURL = temporaryDirectory.appendingPathComponent("avatar.png")
                    try imageData.write(to: fileURL)
                    let attachment = try UNNotificationAttachment(identifier: "avatar", url: fileURL, options: nil)
                    content.attachments = [attachment]
                } catch {
                    print("Error creating notification attachment: \(error.localizedDescription)")
                }
            }

            content.title = "\(senderName)"
            content.body = "Missed call"
            content.sound = UNNotificationSound.default

            let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
            UNUserNotificationCenter.current().add(request) { error in
                if let error = error {
                    print("Error displaying missed call notification: \(error.localizedDescription)")
                } else {
                    print("Missed call notification displayed")
                }
            }
        }

        func reloadViewController(_ rootViewController : UIViewController) {
            if let navigationController = rootViewController as? UINavigationController {
                if let visibleViewController = navigationController.visibleViewController {
                    visibleViewController.viewWillAppear(true)
                    visibleViewController.viewDidAppear(true)
                }
            } else {
                rootViewController.viewWillAppear(true)
                rootViewController.viewDidAppear(true)
            }
        }

        // MARK: - Present Message From Payload
        public func presentMessageFromPayload(response: UNNotificationResponse) {
            let notification = response.notification.request.content.userInfo as? [String: Any]

            if let userInfo = notification, let messageObject =
                userInfo["message"], let dict = messageObject as? [String: Any] {

                let message = CometChat.processMessage(dict).0
                let cometChatMessages = MessagesVC()

                if message?.receiverType == .user {
                    guard let uid = message?.senderUid, let userName = message?.sender?.name else { return }
                    let user = User(uid: uid, name: userName)
                    cometChatMessages.user = user
                } else {
                    guard let group = (message?.receiver as? Group) else { return }
                    cometChatMessages.group = group
                }

                cometChatMessages.modalPresentationStyle = .fullScreen

                let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate

                if let window = sceneDelegate?.window, let rootViewController = window.rootViewController {
                    var currentController = rootViewController
                    while let presentedController = currentController.presentedViewController {
                        currentController = presentedController
                    }
                    currentController.present(cometChatMessages, animated: true)
                }
            }
        }

    }

    // MARK: - Login Token Handling
    extension CometChatAPNsHelper: CometChatLoginDelegate {

        func onLoginSuccess(user: CometChatSDK.User) {
            print("User logged in successfully: \(user.uid ?? "unknown")")

            // Register for APNs after login
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
                print("Requesting APNs token after login...")
            }

            // Check for pending token
            registerPendingTokenIfNeeded()

            #if canImport(CometChatCallsSDK)
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
                    let registry = PKPushRegistry(queue: DispatchQueue.main)
                    registry.delegate = appDelegate
                    appDelegate.pushRegistry(registry, didInvalidatePushTokenFor: .voIP)
                }
            }
            #endif
        }

        func onLoginFailed(error: CometChatSDK.CometChatException?) {
            print("Login failed: \(error?.errorDescription ?? "unknown error")")
        }

        func onLogoutSuccess() {
            print("User logged out successfully")
        }

        func onLogoutFailed(error: CometChatSDK.CometChatException?) {
            print("Logout failed: \(error?.errorDescription ?? "unknown error")")
        }

    }

    // MARK: - VoIP & CallKit Functions
    #if canImport(CometChatCallsSDK)

    extension CometChatAPNsHelper {

        public func didReceiveIncomingPushWith(payload: PKPushPayload) -> CXProvider? {
            guard let sender = payload.dictionaryPayload["sender"] as? String,
                  let senderName = payload.dictionaryPayload["senderName"] as? String,
                  let body = payload.dictionaryPayload["body"] as? String,
                  let callAction = payload.dictionaryPayload["callAction"] as? String,
                  let receiver = payload.dictionaryPayload["receiver"] as? String,
                  let type = payload.dictionaryPayload["type"] as? String,
                  let callType = payload.dictionaryPayload["callType"] as? String,
                  let sessionId = payload.dictionaryPayload["sessionId"] as? String,
                  let conversationId = payload.dictionaryPayload["conversationId"] as? String else {
                print("Incomplete VoIP payload")
                return nil
            }

            let applicationState = UIApplication.shared.applicationState
            print("VoIP push received - Action: \(callAction), State: \(applicationState.rawValue)")

            if type == "call" {
                switch callAction {
                case "initiated":
                    switch applicationState {
                    case .active:
                        if CometChat.getActiveCall() != nil {
                            print("User already on a call, rejecting with busy...")
                            CometChat.rejectCall(sessionID: sessionId, status: .busy, onSuccess: { rejectedCall in
                                print("Rejected incoming call with busy status")
                            }, onError: { error in
                                print("Failed to reject with busy: \(error?.errorDescription ?? "")")
                            })
                            return nil
                        } else {
                            return updatedInitiateCall(sender: sender, senderName: senderName, body: body, callAction: callAction, receiver: receiver, callType: callType, sessionId: sessionId, conversationId: conversationId)
                        }
                    case .inactive, .background:
                        return updatedInitiateCall(sender: sender, senderName: senderName, body: body, callAction: callAction, receiver: receiver, callType: callType, sessionId: sessionId, conversationId: conversationId)
                    @unknown default:
                        break
                    }

                case "ongoing":
                    print("Call ongoing")
                    break

                case "unanswered":
                    provider?.reportCall(with: uuid!, endedAt: Date(), reason: .unanswered)
                    handleMissedCallNotification(payload: payload.dictionaryPayload)

                case "rejected":
                    provider?.reportCall(with: uuid!, endedAt: Date(), reason: .unanswered)

                case "busy":
                    if let uuid = uuid {
                        provider?.reportCall(with: uuid, endedAt: Date(), reason: .unanswered)
                        self.uuid = nil
                    }

                case "cancelled":
                    provider?.reportCall(with: uuid!, endedAt: Date(), reason: .failed)
                    handleMissedCallNotification(payload: payload.dictionaryPayload)

                case "ended":
                    provider?.reportCall(with: uuid!, endedAt: Date(), reason: .remoteEnded)

                default:
                    provider?.reportCall(with: uuid!, endedAt: Date(), reason: .remoteEnded)
                }
            }

            return nil
        }

        public func onAnswerCallAction(action: CXAnswerCallAction) {
            if activeCall != nil {
                startCall()
            }
            action.fulfill()
        }

        private func updatedInitiateCall(sender: String, senderName: String, body: String, callAction: String, receiver: String, callType: String, sessionId: String, conversationId: String) -> CXProvider? {

            let callTypeValue: CometChat.CallType = callType == "audio" ? .audio : .video
            let receiverType: CometChat.ReceiverType = conversationId.contains("group") ? .group : .user
            let call = Call(receiverId: receiver, callType: callTypeValue, receiverType: receiverType)
            call.sessionID = sessionId
            call.callStatus = .initiated
            call.initiatedAt = Date().timeIntervalSince1970
            call.callInitiator = User(uid: sender, name: senderName)
            call.callType = callTypeValue
            call.callReceiver = User(uid: receiver, name: receiver)

            activeCall = call
            uuid = UUID()

            let callerName = senderName
            let config = CXProviderConfiguration(localizedName: "APNS + Callkit")
            config.iconTemplateImageData = UIImage(named: "AppIcon")?.pngData()
            config.includesCallsInRecents = true
            config.ringtoneSound = "ringtone.caf"
            config.supportsVideo = true

            provider = CXProvider(configuration: config)

            let update = CXCallUpdate()
            update.remoteHandle = CXHandle(type: .generic, value: callerName.capitalized)
            update.hasVideo = callType == "video"

            provider?.reportNewIncomingCall(with: uuid!, update: update, completion: { error in
                if error == nil {
                    self.configureAudioSession()
                }
            })

            return provider!
        }

        private func configureAudioSession() {
            do {
                try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, options: [.mixWithOthers, .allowBluetooth, .defaultToSpeaker])
                try AVAudioSession.sharedInstance().setActive(true)
            } catch let error as NSError {
                print("Audio session error: \(error)")
            }
        }

        private func startCall() {
            let cometChatOngoingCall = CometChatOngoingCall()

            CometChat.acceptCall(sessionID: activeCall?.sessionID ?? "") { call in
                DispatchQueue.main.async {
                    let isAudioCall = (self.activeCall?.callType == .audio)
                    var callSettingsBuilder = CometChatCallsSDK.CallSettingsBuilder()
                    callSettingsBuilder = callSettingsBuilder.setIsAudioOnly(isAudioCall)
                    cometChatOngoingCall.set(callSettingsBuilder: callSettingsBuilder)
                    cometChatOngoingCall.set(callWorkFlow: .defaultCalling)
                    cometChatOngoingCall.set(sessionId: call?.sessionID ?? "")
                    cometChatOngoingCall.modalPresentationStyle = .fullScreen

                    if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate,
                       let window = sceneDelegate.window,
                       let rootViewController = window.rootViewController {
                        var currentController = rootViewController
                        while let presentedController = currentController.presentedViewController {
                            currentController = presentedController
                        }
                        currentController.present(cometChatOngoingCall, animated: true)
                    }
                }

                cometChatOngoingCall.setOnCallEnded { [weak self] call in
                    DispatchQueue.main.async {
                        if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
                            if let rootViewController = scene.windows.first?.rootViewController {
                                self?.dismissCometChatIncomingCall(from: rootViewController)
                                self?.reloadViewController(rootViewController)
                            }
                        }
                    }
                    self?.provider?.reportCall(with: self?.uuid ?? UUID(), endedAt: Date(), reason: .remoteEnded)
                }
            } onError: { error in
                print("Error accepting call: \(error?.errorDescription ?? "")")
            }
        }

        func onCallEnded(call: CometChatSDK.Call) {
            guard let uuid = uuid else { return }

            if activeCall != nil {
                let transaction = CXTransaction(action: CXEndCallAction(call: uuid))
                callController.request(transaction, completion: { error in })
                activeCall = nil
            }

            DispatchQueue.main.sync { [self] in
                if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
                    if let rootViewController = scene.windows.first?.rootViewController {
                        dismissCometChatIncomingCall(from: rootViewController)
                        self.reloadViewController(rootViewController)
                    }
                }
            }
        }

        func onCallInitiated(call: CometChatSDK.Call) {
            let callerName = (call.callReceiver as? User)?.name
            callController = CXCallController()
            uuid = UUID()

            let transactionCallStart = CXTransaction(action: CXStartCallAction(call: uuid!, handle: CXHandle(type: .generic, value: callerName ?? "")))
            callController.request(transactionCallStart, completion: { error in })
        }

        private func dismissCometChatIncomingCall(from viewController: UIViewController) {
            if let presentedViewController = viewController.presentedViewController {
                if presentedViewController is CometChatIncomingCall {
                    presentedViewController.dismiss(animated: false, completion: nil)
                } else {
                    dismissCometChatIncomingCall(from: presentedViewController)
                }
            }
        }

        public func onProviderDidReset(provider: CXProvider) {
            if let uuid = self.uuid {
                onCall = true
                provider.reportCall(with: uuid, endedAt: Date(), reason: .unanswered)
            }
        }

        public func onEndCallAction(action: CXEndCallAction) {
            let endCallAction = CXEndCallAction(call: uuid!)
            let transaction = CXTransaction()
            transaction.addAction(endCallAction)

            callController.request(transaction) { error in
                if let error = error {
                    print("Error requesting transaction: \(error)")
                } else {
                    print("Requested transaction successfully")
                }
            }

            if let activeCall = activeCall {
                if CometChat.getActiveCall() == nil || (CometChat.getActiveCall()?.callStatus == .initiated && CometChat.getActiveCall()?.callInitiator != CometChat.getLoggedInUser()) {
                    CometChat.rejectCall(sessionID: activeCall.sessionID ?? "", status: .rejected, onSuccess: { [self] (rejectedCall) in
                        action.fulfill()
                        print("CallKit: Reject call success")
                        DispatchQueue.main.async { [self] in
                            if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
                                if let rootViewController = scene.windows.first?.rootViewController {
                                    self.dismissCometChatIncomingCall(from: rootViewController)
                                    self.reloadViewController(rootViewController)
                                }
                            }
                            if let uuid = uuid {
                                provider?.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded)
                                self.uuid = nil
                            }
                        }
                    }) { (error) in
                        print("CallKit: Reject call failed: \(error?.errorDescription ?? "")")
                    }
                } else {
                    CometChat.endCall(sessionID: CometChat.getActiveCall()?.sessionID ?? "") { call in
                        CometChatCalls.endSession()
                        action.fulfill()
                        print("CallKit: End call success")
                        DispatchQueue.main.async { [self] in
                            if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
                                if let rootViewController = scene.windows.first?.rootViewController {
                                    self.dismissCometChatIncomingCall(from: rootViewController)
                                    self.reloadViewController(rootViewController)
                                }
                            }
                        }
                    } onError: { error in
                        print("CallKit: End call failed: \(error?.errorDescription ?? "")")
                    }
                }
            }
        }

    }

    extension CometChatAPNsHelper: CometChatCallEventListener {
    func ccCallEnded(call: Call) {
    guard let uuid = uuid else { return }

            if activeCall != nil {
                let transactionCallAccepted = CXTransaction(action: CXEndCallAction(call: uuid))
                callController.request(transactionCallAccepted, completion: { error in })
                activeCall = nil
            }
        }

    }

    #endif

    ```
  </Tab>

  <Tab title="CometChatPNHelper.swift">
    ```swift lines theme={null}
    import Foundation
    import UIKit
    import CometChatSDK
    import CometChatUIKitSwift

    class CometChatPNHelper {

    let cometchatAPNsHelper = CometChatAPNsHelper()
    static var currentActiveUser: CometChatSDK.User?
    static var currentActiveGroup: CometChatSDK.Group?

    static func handleTapActionOnNotification(userInfo: [AnyHashable: Any], completionHandler: @escaping () -> Void) {
      guard let notificationType = userInfo["type"] as? String,
         let receiverType = userInfo["receiverType"] as? String else {
        print("Notification type or receiver type not found in payload")
        completionHandler()
        return
      }

      switch notificationType {
      case "chat":
        if receiverType == "user" {
          handleChatNotification(userInfo: userInfo)
        } else if receiverType == "group" {
          handleGroupChatNotification(userInfo: userInfo)
        } else {
          print("Invalid receiver type for chat notification")
        }

      case "call":
        if receiverType == "user" {
          handleChatNotification(userInfo: userInfo)
        } else if receiverType == "group" {
          handleGroupChatNotification(userInfo: userInfo)
        } else {
          print("Invalid receiver type for call notification")
        }

      default:
        navigateToDefaultScreen()
      }

      completionHandler()
    }

    static func handleQuickReplyActionOnNotification(userInfo: [AnyHashable: Any], text: String, completionHandler: @escaping () -> Void) {
      guard let notificationType = userInfo["type"] as? String,
         let receiverType = userInfo["receiverType"] as? String else {
        print("Notification type or receiver type not found in payload")
        completionHandler()
        return
      }

      switch notificationType {
      case "chat":
        if receiverType == "user" {
          replyToUserWith(message: text, userInfo: userInfo)
        } else if receiverType == "group" {
          replyToGroupWith(message: text, userInfo: userInfo)
        } else {
          print("Invalid receiver type for chat notification")
        }
      default:
        break
      }

      completionHandler()
    }

    static func navigateToViewController(_ viewController: UIViewController) {

      guard let window = UIApplication.shared.windows.first else {
        print("Window not found")
        return
      }

      if let navigationController = window.rootViewController as? UINavigationController {
        if let currentViewController = navigationController.viewControllers.last,
          currentViewController.description == viewController.description {
          print("Already in same view")
          return

        }
        navigationController.popViewController(animated: false)
        navigationController.pushViewController(viewController, animated: false)
      } else {
        print("Root view controller is not a UINavigationController")
      }

    }

    static func replyToUserWith(message text: String, userInfo: [AnyHashable: Any], withParentId: Int? = nil) {
      guard let sender = userInfo["sender"] as? String,
         let senderName = userInfo["senderName"] as? String else {
        print("Sender information missing in payload")
        return
      }

      let textMessage = TextMessage(receiverUid: sender, text: text, receiverType: .user)
      if let parentID = withParentId {
        textMessage.parentMessageId = parentID
      }
      CometChatUIKit.sendTextMessage(message: textMessage)
    }

    static func replyToGroupWith(message text: String, userInfo: [AnyHashable: Any], withParentId: Int? = nil) {
      guard let groupID = userInfo["receiver"] as? String,
         let groupName = userInfo["receiverName"] as? String else {
        print("Group information missing in payload")
        return
      }

      let textMessage = TextMessage(receiverUid: groupID, text: text, receiverType: .group)
      if let parentID = withParentId {
        textMessage.parentMessageId = parentID
      }
      CometChatUIKit.sendTextMessage(message: textMessage)
    }

    static func handleChatNotification(userInfo: [AnyHashable: Any]) {
      guard let sender = userInfo["sender"] as? String,
         let senderName = userInfo["senderName"] as? String else {
        print("Sender information missing in payload")
        return
      }

      let senderUser = User(uid: sender, name: senderName)
      senderUser.avatar = userInfo["senderAvatar"] as? String

      getUser(forUID: sender) { retrievedUser in
        DispatchQueue.main.async {
          if let user = retrievedUser {
            senderUser.status = user.status
          } else {
            print("Failed to retrieve user status")
          }

          let chatViewController = MessagesVC()
          chatViewController.user = retrievedUser
          self.navigateToViewController(chatViewController)
        }

      }
    }


    static func handleGroupChatNotification(userInfo: [AnyHashable: Any]) {
      guard let groupID = userInfo["receiver"] as? String,
         let groupName = userInfo["receiverName"] as? String else {
        print("Group information missing in payload")
        return
      }

      let groupUser = Group(guid: groupID, name: groupName, groupType: .private, password: nil)

      self.getGroup(for: groupUser, guid: groupID) { fetchedGroup in
        DispatchQueue.main.async {
          if let group = fetchedGroup {
            groupUser.membersCount = group.membersCount
            groupUser.icon = group.icon
          } else {
            print("Failed to fetch group members count")
          }
          let chatViewController = MessagesVC()
          chatViewController.group = fetchedGroup
          self.navigateToViewController(chatViewController)
        }
      }
    }

    static func handleCallNotification(userInfo: [AnyHashable: Any]) {
      guard let sender = userInfo["sender"] as? String,
         let senderName = userInfo["senderName"] as? String else {
        print("Sender information missing in payload")
        return
      }

      let user = User(uid: sender, name: senderName)
      user.avatar = userInfo["senderAvatar"] as? String
      DispatchQueue.main.async {
        let callViewController = MessagesVC()
        callViewController.user = user
        CometChatPNHelper.navigateToViewController(callViewController)
      }
    }

    static func handleGroupCallNotification(userInfo: [AnyHashable: Any]) {
      guard let groupID = userInfo["receiver"] as? String,
         let groupName = userInfo["receiverName"] as? String else {
        print("Group information missing in payload")
        return
      }

      let groupUser = Group(guid: groupID, name: groupName, groupType: .private, password: nil)
      groupUser.icon = userInfo["receiverAvatar"] as? String
      DispatchQueue.main.async {

        let callViewController = MessagesVC()
        callViewController.group = groupUser
        CometChatPNHelper.navigateToViewController(callViewController)
      }
    }

    static func navigateToDefaultScreen() {
      DispatchQueue.main.async {
        let defaultViewController = MessagesVC()

        guard let window = UIApplication.shared.windows.first else {
          print("Window not found")
          return
        }

        if let navigationController = window.rootViewController as? UINavigationController {
          navigationController.pushViewController(defaultViewController, animated: true)
        } else {
          print("Root view controller is not a UINavigationController")
        }
      }
    }
    static func getUser(forUID uid: String, completionHandler: @escaping (CometChatSDK.User?) -> Void) {
      CometChat.getUser(UID: uid, onSuccess: { user in
        let user = user
        completionHandler(user)
      }) { error in
        print("User fetching failed with error: \(error?.errorDescription ?? "Unknown error")")
        completionHandler(nil)
      }
    }

    static func getGroup(for group: Group, guid: String, completionHandler: @escaping (Group?) -> Void) {
      CometChat.getGroup(GUID: guid, onSuccess: { fetchedGroup in
        completionHandler(fetchedGroup)
      }) { error in
        print("Group details fetching failed with error: \(error?.errorDescription ?? "Unknown error")")
        completionHandler(nil)
      }
    }

    static func shouldPresentNotification(userInfo: [AnyHashable: Any]) -> Bool {
      guard let notificationType = userInfo["type"] as? String,
         let receiverType = userInfo["receiverType"] as? String else {
        return true
      }

      if notificationType == "chat" {
        if receiverType == "user" {
          let sender = userInfo["sender"] as? String
          if sender == CometChatPNHelper.currentActiveUser?.uid {
            return false
          }
        } else if receiverType == "group" {
          let receiver = userInfo["receiver"] as? String
          if receiver == CometChatPNHelper.currentActiveGroup?.guid {
            return false
          }
        }
      }

      return true
    }
    }
    ```
  </Tab>

  <Tab title="AppConstants.swift">
    ```swift lines highlight={5-8} theme={null}
    import Foundation
    import UIKit

    class AppConstants {
    static var APP_ID: String = ""
    static var AUTH_KEY: String = ""
    static var REGION: String = ""
    static var PROVIDER_ID: String = ""
    }

    extension AppConstants{
    static func saveAppConstants(){
    UserDefaults.standard.set(APP_ID, forKey: "appID")
    UserDefaults.standard.set(AUTH_KEY, forKey: "authKey")
    UserDefaults.standard.set(REGION, forKey: "region")
    }

        static func retrieveAppConstants(){
            APP_ID = UserDefaults.standard.string(forKey: "appID") ?? AppConstants.APP_ID
            AUTH_KEY = UserDefaults.standard.string(forKey: "authKey") ?? AppConstants.AUTH_KEY
            REGION = UserDefaults.standard.string(forKey: "region") ?? AppConstants.REGION
        }

    }

    ```
  </Tab>
</Tabs>

## 4. Register APNs device + VoIP tokens with CometChat

* In your `AppDelegate.swift`, implement the following methods to handle APNs registration success and failure, and to register the device token with CometChat.
* Make sure to import the necessary modules at the top of the file.
* Complete your `AppDelegate.swift` as shown below:

```Swift lines highlight={17, 19, 22} theme={null}
import UIKit
import PushKit
import CometChatSDK
import CometChatUIKitSwift

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var pushRegistry: PKPushRegistry?
    let cometchatAPNsHelper = CometChatAPNsHelper()
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        UNUserNotificationCenter.current().delegate = self

        cometchatAPNsHelper.configurePushNotification(application: application, delegate: self)

        // Initialize PushKit
        initializePushKit()

        return true
    }

    // MARK: - APNs Registration Success
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        print("APNs Device token received!")

        if CometChat.getLoggedInUser() != nil {
            print("User is logged in, registering APNs token...")
            cometchatAPNsHelper.registerTokenForPushNotification(deviceToken: deviceToken)
        } else {
            print("User NOT logged in yet, will register token after login")
            // Store token for later registration
            let hexString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
            UserDefaults.standard.set(hexString, forKey: "pendingAPNsToken")
        }
    }

    // MARK: - APNs Registration Failure
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register for APNs: \(error.localizedDescription)")
    }
}
```

## 5. Unregister the token on logout

Before logging the user out, unregister the push token so the device stops receiving notifications for that user.

```swift lines theme={null}
CometChatNotifications.unregisterPushToken(
    onSuccess: { success in
        print("Push token unregistered: \(success)")
        CometChatUIKit.logout(onSuccess: { _ in
            print("Logout successful")
        }, onError: { error in
            print("Logout failed: \(error.errorDescription)")
        })
    },
    onError: { error in
        print("Token unregister failed: \(error.errorCode) - \(error.errorDescription)")
    }
)
```

Always call `CometChatNotifications.unregisterPushToken()` **before** `CometChatUIKit.logout()`. If you skip this step, the device may continue to receive pushes for the logged-out user.

## 6. Badge count

CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (a string) representing the total unread messages across all conversations for the logged-in user. You can use this to set the app icon badge.

### 6.1 Enable unread badge count on the CometChat Dashboard

1. Go to **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences**.
2. Scroll to the bottom and enable the **Unread Badge Count** toggle.

This ensures CometChat includes the `unreadMessageCount` field in every push payload sent to your app.

### 6.2 Expected payload format

CometChat sends APNs payloads with this structure (relevant fields):

```json theme={null}
{
  "unreadMessageCount": "5",
  "title": "New Message",
  "alert": "John: Hello!",
  "conversationId": "user_abc123",
  "receiverType": "user"
}
```

`unreadMessageCount` is a string representing the total unread messages across all conversations for the logged-in user.

### 6.3 Update the app badge from the push payload

Inside your `UNUserNotificationCenterDelegate` method (for example `willPresent` or a Notification Service Extension), parse `unreadMessageCount` and update the badge:

```swift lines theme={null}
// Inside userNotificationCenter(_:willPresent:) or a Notification Service Extension
let userInfo = notification.request.content.userInfo

if let unreadCountStr = userInfo["unreadMessageCount"] as? String,
   let count = Int(unreadCountStr), count >= 0 {
    DispatchQueue.main.async {
        UIApplication.shared.applicationIconBadgeNumber = count
    }
} else {
    print("No valid unreadMessageCount in payload")
}
```

Setting `applicationIconBadgeNumber` to `0` clears the badge.

### 6.4 Clear badge when the app opens

Clear the badge count when the app launches and every time it returns to the foreground. In your `SceneDelegate` or `AppDelegate`:

```swift lines theme={null}
func sceneDidBecomeActive(_ scene: UIScene) {
    UIApplication.shared.applicationIconBadgeNumber = 0
}
```

This keeps the badge in sync with the actual unread state.

## 7. Navigation from notifications

When the user taps a notification, use `userNotificationCenter(_:didReceive:withCompletionHandler:)` to extract conversation details and navigate to the correct screen.

```swift lines theme={null}
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    let userInfo = response.notification.request.content.userInfo

    guard let receiverType = userInfo["receiverType"] as? String else {
        completionHandler()
        return
    }

    if receiverType == "user" {
        guard let senderUid = userInfo["sender"] as? String,
              let senderName = userInfo["senderName"] as? String else {
            completionHandler()
            return
        }
        let user = User(uid: senderUid, name: senderName)
        let messagesVC = MessagesVC()
        messagesVC.user = user
        navigateToViewController(messagesVC)

    } else if receiverType == "group" {
        guard let groupId = userInfo["receiver"] as? String,
              let groupName = userInfo["receiverName"] as? String else {
            completionHandler()
            return
        }
        let group = Group(guid: groupId, name: groupName, groupType: .public, password: nil)
        let messagesVC = MessagesVC()
        messagesVC.group = group
        navigateToViewController(messagesVC)
    }

    completionHandler()
}
```

The `navigateToViewController` helper (shown in `CometChatPNHelper.swift` above) pushes the `MessagesVC` onto the navigation stack. Ensure the root view controller is ready before navigation -- if the app was terminated, wait until login completes before routing.

## 8. Testing checklist

1. Install on a device; grant notification permission. Verify APNs device token logs.
2. Log in, then confirm both device + VoIP tokens register with CometChat (success callbacks).
3. Send a message from another user:
   * Foreground: ensure `willPresent` shows your chosen presentation.
   * Background/terminated: tapping opens the correct conversation.
4. Trigger an incoming call; CallKit UI should show caller info. Accept should join the call; Decline should reject via CometChat and end CallKit.
5. Rotate tokens (reinstall or toggle VoIP) to ensure re-registration works.

## 6. Troubleshooting

| Symptom                            | Quick checks                                                                                                                              |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| No pushes                          | Entitlements set, APNs provider creds correct, bundle ID matches dashboard, permission granted.                                           |
| Token registration fails           | Run after login; provider IDs correct for device vs VoIP.                                                                                 |
| Taps do nothing                    | Verify notification center delegate and navigation readiness before routing.                                                              |
| Call UI missing                    | Ensure PushKit delegate fires, CallKit capabilities enabled, VoIP provider ID set.                                                        |
| Audio errors                       | Configure `AVAudioSession` for playAndRecord when reporting/accepting calls.                                                              |
| Badge count not showing            | Verify **Unread Badge Count** is enabled in CometChat Dashboard and that your app reads `unreadMessageCount` from the payload.            |
| Notification tap does not navigate | Ensure `UNUserNotificationCenterDelegate` is set, payload contains `receiverType`/`sender`/`receiver`, and root view controller is ready. |
