Notification Support
Push Notifications in Handover
First provide the SDK with the Apple push notification token using:
KindlySDK.setAPNSDeviceToken(_ deviceToken: Data)KindlySDK.setAPNSDeviceToken(_ deviceToken: String)When a Push Notification is Received
Forward the notification to SDK using
KindlySDK.notificationReceived(_ userInfo: [AnyHashable : Any])KindlySDK.notificationReceived(_ notification: UNNotification)The SDK will double-check if notification type is a Kindly notification before handling it.
Check if a Notification is from Kindly
To determine if a notification should be handled by the Kindly SDK or another notification provider (like mParticle or Braze), use the following helper method:
let isKindlyNotification = KindlySDK.isKindlyNotification(_ userInfo: [AnyHashable : Any])This can be particularly useful when integrating with multiple push notification providers. Example usage:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if KindlySDK.isKindlyNotification(userInfo) {
// Handle with Kindly SDK
KindlySDK.notificationReceived(userInfo)
} else {
// Handle with another notification provider (e.g., mParticle/Braze)
otherNotificationProvider.handleNotification(userInfo)
}
completionHandler(.newData)
}When a Notification is Clicked
KindlySDK.notificationResponseReceived(_ response: UNNotificationResponse)When the SDK Displays the Notification
Kindly silent pushes are encrypted; the SDK decrypts each one and presents a local banner with the decrypted title/body â except when the user is already actively reading the chat conversation. The display rule is:
App state | Active screen | SDK presents the banner? |
|---|---|---|
Background | any | â Yes |
Foreground | Chat conversation | â No (the user can already see the message) |
Foreground | Settings, language, image preview, host-app screens, etc. | â Yes |
Intercepting Notifications (shouldHandleNotification)
Every Kindly silent push that the SDK successfully decrypts is forwarded to the host app via the shouldHandleNotification(notification:) delegate method â regardless of foreground/background state or which screen is on top. Use it to run side effects (analytics, logging, in-app indicators) or to take over the presentation entirely with your own UI.
Scenario | Result |
|---|---|
Delegate not set | SDK presents the notification (subject to the display rule above) |
shouldHandleNotification returns true (default) | SDK presents the notification (subject to the display rule above) |
shouldHandleNotification returns false | SDK does not present anything â your app handles it |
The delegate receives an ExternalNotification containing the decrypted fields plus the original userInfo for any extra fields your backend attached:
public struct ExternalNotification {
public let id: String // decrypted chat_id
public let title: String // decrypted title
public let body: String // decrypted body
public let userInfo: [AnyHashable: Any] // original APNs userInfo (aps, custom keys, âĻ)
}Implementation
1. Set the delegate:
KindlySDK.delegate = self2. Conform to KindlyChatClientDelegate:
extension ViewController: KindlyChatClientDelegate {
func shouldHandleNotification(notification: ExternalNotification) -> Bool {
// Always log every Kindly notification, regardless of who presents it
Analytics.track("kindly_notification_received", id: notification.id)
// Example: take over presentation when the app is in a custom in-app inbox
if isShowingCustomInbox {
customInbox.append(notification)
return false // SDK does not present its banner
}
return true // let the SDK present the banner
}
}Notes
- The method has a default implementation that returns true, so you only need to implement it if you want to observe or intercept notifications.
- The callback fires for every successfully decrypted Kindly push â it is not gated by the SDK's foreground/chat-screen display rule. Use it for analytics that need to run even when the SDK is on screen.
- The polarity matches shouldHandleLink: returning true means "the SDK should handle this", returning false means "I'll handle it myself".
- Returning false only suppresses the local banner â the underlying message has already been delivered to the chat session via the websocket / /latest endpoint, so it will appear in the chat history when the user opens it.
Notification Service Extension (recommended, SDK 3.0.10+)
With an extension in your app, Kindly sends a visible push (aps.alert + mutable-content: 1) instead of a silent one. iOS runs the extension before showing the banner, in every app state including when the app was killed, and the extension decrypts the Kindly payload so the banner shows the real title and text. Silent pushes are rate limited by iOS and never delivered to a force-quit app; visible pushes have neither problem.
The switch is per bot: once your app ships the extension, ask your Kindly contact to enable alert notifications for your bot. Kindly only sends the visible push to devices on SDK 3.0.10 or newer, so older app versions keep working exactly as before.
Setup
- File â New â Target â Notification Service Extension, embedded in your app. Replace the generated class with:
import Kindly
final class NotificationService: KindlyNotificationServiceExtension {}- Link Kindly into the extension target. The linker warning about extension safety is expected.
- On both the app target and the extension target add the Keychain Sharing capability with the same group, your app's bundle id:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)ai.kindly.Example</string>
</array>- Tell Kindly the app version that ships the extension.
What changes at runtime
Push | App state | User sees |
|---|---|---|
visible | killed or background | one banner with the decrypted text, produced by the extension |
visible | foreground, on the chat conversation | nothing |
visible | foreground, elsewhere | one banner with the decrypted text |
silent | any | what it sees today |
Without the extension a visible push shows the generic placeholder in the background and the SDK's own decrypted banner in the foreground. Ciphertext is never shown.
Handling the tap
Tapping a Kindly banner opens the chat (KindlySDK.openChatOnNotificationTap, default true). A tap that launched the app opens the chat once start() has run. The remembered tap is replayed only when start() runs within 60 seconds of it; after that it is dropped, with a log line.
Your own delegate
If you keep your own UNUserNotificationCenterDelegate, call KindlySDK.notificationWillPresent(notification, completionHandler:) for Kindly notifications from willPresent so the SDK's display rule applies.
API
open class KindlyNotificationServiceExtension: UNNotificationServiceExtension {
open func customize(_ content: UNMutableNotificationContent, payload: KindlyPushDecryption.DecryptedPayload?)
}
public enum KindlyPushDecryption {
public struct DecryptedPayload { public let chatId: String; public let title: String; public let body: String }
public static func isKindlyNotification(_ userInfo: [AnyHashable: Any]) -> Bool
public static func decrypt(userInfo: [AnyHashable: Any]) -> DecryptedPayload?
public static func apply(_ payload: DecryptedPayload, to content: UNMutableNotificationContent)
public static func isDecrypted(_ userInfo: [AnyHashable: Any]) -> Bool
}decrypt returns nil when the push is not Kindly's, a field is missing, the keychain has no key yet, or decryption fails; the extension then leaves the placeholder in place.
See also
The push notifications reference (PUSH.md) has the complete walkthrough, including hosts with an existing extension for another provider.