Push Notifications

Use Apple VoIP push notifications together with the Sinch SDK and the system calling UI (CallKit or LiveCommunicationKit) for reliable incoming-call delivery and a native user experience.

To fully enable VoIP push notifications in your application, this document will guide you through these in more detail:

  • Configure your iOS app for VoIP push notifications and CallKit / LiveCommunicationKit .
  • Create and upload an APNs Signing Key for your Sinch Application in the Sinch Developer Portal.
  • Configure SinchClient so Sinch manages push notifications (client-side and server-side).
  • Integrate Sinch push APIs with CallKit / LiveCommunicationKit .
  • Ensure the APNs environment matches the app entitlements and code signing.

Please, check our reference application for full implementation and additional functionalities, which is available on GitHub.

info

Apple requires VoIP pushes that represent calls to be reported to the system calling UI (CallKit / LiveCommunicationKit) before your push delegate returns. Failing to do so may result in app termination or future VoIP pushes being throttled.

Configure iOS App with Push Notifications Capability

Enable the Push Notifications capability and ensure your app entitlements include the correct APS environment.

Acquiring a Push Device Token

SinchManagedPush is a component used to simplify acquiring a push device token and registering the token with a SinchClient. SinchManagedPush will make use of PushKit to acquire a push device token, and will automatically register the token with the SinchClient when a client is created (later in the application life cycle). SinchManagedPush should be created as early as possible in the application's life cycle.

Copy
Copied
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  // Create instance to enable push notification.
  private var sinchPush: SinchManagedPush?
  
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: 
                     [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ... 
    
    sinchPush = SinchRTC.managedPush(forAPSEnvironment: .development)
    sinchPush?.delegate = self
    sinchPush?.setDesiredPushType(SinchManagedPush.TypeVoIP)
  }
}
APS environment must match

The APNs environment you pass to SinchRTC.managedPush(forAPSEnvironment:) must match the app’s provisioning (Development vs. Production). Please see the section APS Environments and Provisioning for details.

SinchManagedPush

SinchManagedPush is lightweight and can live independently of a SinchClient. Create it once and keep it for the lifetime of the app.

Enabling Push Notifications for SinchClient

To make Sinch manage push notifications for you end-to-end, both client-side and server-side in terms of acting as an APNs Provider, it's necessary to enable managed push notifications when configuring a SinchClient.

Copy
Copied
  do {
    sinchClient = try SinchRTC.client(withApplicationKey: "yourApplicationKey",
                                      environmentHost: "ocra.api.sinch.com",
                                      userId: "userId",
                                      cli: "cli")
  } catch let error as NSError {
    // Handle error.
  }
  
  sinchClient.enableManagedPushNotifications()

Configuring an APNs Authentication Signing Key

Provide Sinch with your APNs Authentication Token Signing Key so we can send VoIP pushes on your behalf. You create this signing key in your Apple Developer Account and upload the key file to your Sinch Developer Account.

  1. Create an APNs Key in your Apple Developer Account . See the Apple developer documentation on creating the key here .
  2. Upload the key file ( .p8 ) for your Sinch Application in your Sinch Developer Account .

🔒 Your Signing Key is Stored Securely

Sinch will store your signing key in encrypted form using AES256-GCM (AES in GCM mode, using 256-bit keys).

info

We only support APNs Token-based authentication. We no longer support APNs certificate-based functionality.

CallKit

Apple Requirements for VoIP Push Notifications for CallKit

info

Apple introduced stricter requirements for using VoIP push notifications since iOS 13. iOS apps built using the iOS 13 SDK and make use of VoIP push notifications must report each received push notification as an incoming call to CallKit.

Report notifications to the CallKit framework by calling the method CXProvider.reportNewIncomingCall(with:update:completion:). Further, it must report this within the same run-loop, before the delegate method invocation scope PKPushRegistryDelegate.pushRegistry(_:didReceiveIncomingPushWith:for:completion:) ends.

In terms of the Sinch SDK, this means your implementation must report to CallKit in your implementation of SinchManagedPushDelegate.managedPush(_:didReceiveIncomingPushWithPayload:for:).

Copy
Copied
func managedPush(_ managedPush: SinchRTC.SinchManagedPush,
                   didReceiveIncomingPushWithPayload payload: [AnyHashable: Any],
                   for type: String) {
  // Extract call information from the push payload.
  let notification = queryPushNotificationPayload(pushPayload) // SinchManagedPush
  
  guard notification.isCall, notification.isValid else { return }
  
  let callNotification = notification.callResult
  
  let uuid = // Get uuid from mapped callNotification.callId
  
  let update = CXCallUpdate()
  update.remoteHandle = CXHandle(type: .generic, value: callNotification.remoteUserId)
  
  self.provider.reportNewIncomingCall(with: uuid, update: update) {
    // Handle error and hangup call if needed.
  }
}

If you do not relay the payload to a SinchClient, call SinchManagedPush.didCompleteProcessingPushPayload(_:) so that PushKit’s completion handler is invoked.

Failing to report to CallKit

If a VoIP push notification is not reported to CallKit then iOS will terminate the application. Repeatedly failing to report calls to CallKit may cause the system to stop delivering any more VoIP push notifications to your app. The exact limit before this behavior kicks in is subject to Apple iOS implementation details and outside the control of the Sinch SDK.

Please also see Apple's Developer documentation on this topic.

Reporting outgoing calls to CallKit

While reporting incoming calls to CallKit's mandatory in order to process incoming VoIP push notifications, the same limitation doesn't apply to outgoing calls. Nevertheless, reporting outgoing calls to CallKit's still required in scenarios when an outgoing call is established (callee answers the call) while the caller app is in background, or the caller device is in locked state.

In such scenarios, as a privacy measure the OS will prevent the audio unit from being initialized for recording because the app is not in foreground, unless the outgoing call is reported to CallKit. For this reason the recommendation is that outgoing calls should be reported to CallKit as well.

Copy
Copied
func call(userId: String, uuid: UUID, with completion: @escaping (Error?) -> Void) {
  let handle = CXHandle(type: .generic, value: userId)

  let startCallAction = CXStartCallAction(call: uuid, handle: handle)
  let startCallTransaction = CXTransaction(action: startCallAction)

  self.callController.request(startCallTransaction, completion: completion)
}

Implement CXProviderDelegate to handle the call actions:

Copy
Copied
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
  ...

  let recipientIdentifier = action.handle.value
  let callResult = callClient.callUser(withId: recipientIdentifier)

  switch callResult {
    case .success(let call):
      ...

      // Assigning the delegate of the newly created SinchCall
      // to track call establishment, progress and ending.
      call.delegate = self
      action.fulfill()
    case .failure(let error):
      action.fail()
  }
}

LiveCommunicationKit

Use LiveCommunicationKit (iOS 17.4+) where CallKit is unavailable or when you prefer the new system calling UI. You still use the Sinch SDK for the media/signaling; LiveCommunicationKit provides the system UI layer.

Apple Requirements for VoIP Push Notifications for LiveCommunicationKit

Report each VoIP push that represents an incoming call to LiveCommunicationKit before your PushKit delegate returns, then relay the payload to SinchClient.

Copy
Copied
func managedPush(_ managedPush: SinchRTC.SinchManagedPush,
                 didReceiveIncomingPushWithPayload payload: [AnyHashable: Any],
                 for type: String) {
  // Extract call information from the push payload.
  let notification = queryPushNotificationPayload(pushPayload) // SinchManagedPush
  
  guard notification.isCall, notification.isValid else { return }
  
  let callNotification = notification.callResult
  let uuid = // Get uuid from mapped callNotification.callId

  let localHandle = Handle(type: .generic, value: "localUserId")
  let remoteHandle = Handle(type: .generic, value: callNotification.remoteUserId)

  let update = Conversation.Update(localMember: localHandle,
                                   activeRemoteMembers: [remoteHandle],
                                   capabilities: nil)

  Task {
    do {
      try await self.conversationManager.reportNewIncomingConversation(uuid: uuid, update: update)
    } catch {
      // Handle error and hangup call if needed.
    }
  }
}

Reporting outgoing conversations to LiveCommunicationKit

Trigger the system UI using StartConversationAction, then start the Sinch call when the system asks you to perform the action.

Copy
Copied
func call(userId: String, uuid: UUID, with completion: @escaping (Error?) -> Void) {
  let handle = Handle(type: .generic, value: userId)

  let startConversationAction = StartConversationAction(conversationUUID: uuid, 
                                                        handles: [handle], 
                                                        isVideo: false)

  Task {
    do {
      try await self.conversationManager.perform([startConversationAction])
    } catch {
      // Handle error. 
    }
  }
}

Implement ConversationManagerDelegate to handle the call actions:

Copy
Copied
func conversationManager(_ manager: ConversationManager, perform action: ConversationAction) {
  switch action {
    case let start as StartConversationAction: return self.perform(action: start)
    // Other actions processing can be added here.
    default:
      action.fulfill()
  }
}

private func perform(action: StartConversationAction) {
  ...

  guard let recipientIdentifier = action.handles.first?.value, !recipientIdentifier.isEmpty else {
    action.fail()
    callStartedCallback?(.failure(CallError.noRecepientIdentifier))
    return
  }

  let callResult = callResult = callClient.callUser(withId: recipientIdentifier)
    
  switch callResult {
    case .success(let call):
      ...

      // Assigning the delegate of the newly created SinchCall.
      // To track call establishment, progress and ending.
      call.delegate = self

      action.fulfill()
    case .failure(let error):
      action.fail()
  }
}

Extracting Call Information From a Push Payload

At the time when your application receives a push notification you will need to extract some key information about the call based on only the push notification payload. You will need to do this to conform with Apple requirements on reporting a VoIP push notification as an incoming call to CallKit / LiveCommunicationKit, but you may also want to extract application-specific headers for the call.

Use SinchManagedPush.queryPushNotificationPayload(_:) to parse push payloads and obtain call metadata (remote user ID, video offered, headers, etc). You can do this even before creating a SinchClient.

Copy
Copied
let notification = queryPushNotificationPayload(payload)
guard notification.isCall, notification.isValid else { return }
let callInfo = notification.callResult

Unregister a Push Device Token

If the user of the application logs out or performs a similar action, the push notification device token can be unregistered using the method SinchClient.unregisterPushNotificationDeviceToken() to prevent further notifications to be sent to the particular device.

Apple Push Service Environments and Provisioning

When an iOS application is code signed, the embedded Provisioning Profile will have to match the Apple Push Notification Service Environment (also referred to as APS Environment) specified in the app Entitlements.

This means how the app is code signed and what Provisioning Profile is used has an effect on what value should be passed to SinchRTC.managedPush(forAPSEnvironment:).

For example, if your application is signed with a Development provisioning profile it will be bound to the APS Development environment. If it's code signed with a Distribution provisioning profile it will be bound to the APS Production environment.

Typically a Debug build will be code signed with a Development provisioning profile and thus APSEnvironment should be used. And typically a Release build will be code signed with a Distribution provisioning profile and thus APSEnvironment should be used. You are responsible for selecting proper entitlements depending on your build type and signing profile.

iOS not Delivering Notifications

Under certain circumstances, iOS won't deliver a notification to your application even if it was received at device/OS level. Note that this also applies to VoIP push notifications. Exact behavior and limits are subject to iOS internal details, but well-known scenarios where notifications won't be delivered are:

  • The end-user has actively terminated the application. iOS will only start delivering notifications to the application again after the user has actively started the application again.
  • Your app hasn't been reporting VoIP push notifications to CallKit / LiveCommunicationKit. Please see the separate sections above on how to report VoIP push notifications.

Apple Resources

SinchManagedPush and SinchClient Interaction

This section covers details on how SinchManagedPush and SinchClient interact together (automatically).

SinchManagedPush will make use of PKPushRegistry to acquire a push device token. If any SinchClient instances exist, it will register the token via SinchClient.registerPushNotificationDeviceToken:forPushType:apsEnvironment:), which will in turn register the token with the Sinch backend platform. If no instance of SinchClient exists when SinchManagedPush initially acquires the token, it will hold on to the token (in-process memory only) and register it with any SINClient that's created later during the whole application life cycle.

We'd love to hear from you!
Rate this content:
Still have a question?
 
Ask the community.