Skip to content

Commit

Permalink
[Fix]Handle call_display_name on VoIP notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis committed Apr 25, 2024
1 parent 735ac28 commit 8a69ac8
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 46 deletions.
69 changes: 59 additions & 10 deletions Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,29 @@ import PushKit
/// Handles push notifications for CallKit integration.
open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, ObservableObject {

/// Represents the keys that the Payload dictionary
public enum PayloadKey: String {
case stream
case callCid = "call_cid"
case displayName = "call_display_name"
case createdByName = "created_by_display_name"
case createdById = "created_by_id"
}

/// Represents the content of a VoIP push notification.
fileprivate struct StreamVoIPPushNotificationContent {
public struct Content {
var cid: String
var localizedCallerName: String
var callerId: String

init(from payload: PKPushPayload, defaultCallText: String) {
let streamDict = payload.dictionaryPayload["stream"] as? [String: Any]
cid = streamDict?["call_cid"] as? String ?? "unknown"
localizedCallerName = streamDict?["created_by_display_name"] as? String ?? defaultCallText
callerId = streamDict?["created_by_id"] as? String ?? defaultCallText
public init(
cid: String,
localizedCallerName: String,
callerId: String
) {
self.cid = cid
self.localizedCallerName = localizedCallerName
self.callerId = callerId
}
}

Expand Down Expand Up @@ -82,10 +94,8 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs
) {
guard type == .voIP else { return }

let content = StreamVoIPPushNotificationContent(
from: payload,
defaultCallText: defaultCallText
)
let content = decodePayload(payload)

log
.debug(
"Received VoIP push notification with cid:\(content.cid) callerId:\(content.callerId) callerName:\(content.localizedCallerName)."
Expand All @@ -103,6 +113,45 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs
}
)
}

/// Decodes push notification Payload to a type that the CallKit implementation can use.
open func decodePayload(
_ payload: PKPushPayload
) -> Content {
func string(from: [String: Any], key: PayloadKey, fallback: String) -> String {
from[key.rawValue] as? String ?? fallback
}

guard
let streamDict = payload.dictionaryPayload[PayloadKey.stream.rawValue] as? [String: Any]
else {
return .init(
cid: "unknown",
localizedCallerName: defaultCallText,
callerId: defaultCallText
)
}

let cid = string(from: streamDict, key: .callCid, fallback: "unknown")

let displayName = string(from: streamDict, key: .displayName, fallback: "")
/// If no displayName ("display_name", "name", "title") was set, we default to the creator's name.
let localizedCallerName = displayName.isEmpty
? string(from: streamDict, key: .createdByName, fallback: defaultCallText)
: displayName

let callerId = string(
from: streamDict,
key: .createdById,
fallback: defaultCallText
)

return .init(
cid: cid,
localizedCallerName: localizedCallerName,
callerId: callerId
)
}
}

extension CallKitPushNotificationAdapter: InjectionKey {
Expand Down
111 changes: 76 additions & 35 deletions StreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class CallKitPushNotificationAdapterTests: XCTestCase {

func test_unregister_registryWasConfiguredCorrectly() {
subject.register()

subject.unregister()

XCTAssertNil(subject.registry.delegate)
Expand Down Expand Up @@ -68,45 +68,28 @@ final class CallKitPushNotificationAdapterTests: XCTestCase {
// MARK: - pushRegistry(_:didReceiveIncomingPushWith:for:completion:)

func test_pushRegistryDidReceiveIncomingPush_typeIsVoIP_reportIncomingCallWasCalledAsExpected() {
let pushPayload = MockPKPushPayload()
pushPayload.stubType = .voIP
pushPayload.stubDictionaryPayload = [
"stream": [
"call_cid": "123",
"created_by_display_name": "TestUser",
"created_by_id": "test_user"
]
]

let completionWasCalledExpectation = expectation(description: "Completion was called.")
subject.pushRegistry(
subject.registry,
didReceiveIncomingPushWith: pushPayload,
for: pushPayload.type,
completion: { completionWasCalledExpectation.fulfill() }
assertDidReceivePushNotification(
.init(
cid: "123",
localizedCallerName: "TestUser",
callerId: "test_user"
)
)

XCTAssertEqual(callKitService.reportIncomingCallWasCalled?.cid, "123")
XCTAssertEqual(callKitService.reportIncomingCallWasCalled?.callerName, "TestUser")
XCTAssertEqual(callKitService.reportIncomingCallWasCalled?.callerId, "test_user")
callKitService.reportIncomingCallWasCalled?.completion(nil)

wait(for: [completionWasCalledExpectation], timeout: defaultTimeout)
}

func test_pushRegistryDidReceiveIncomingPush_typeIsNotVoIP_reportIncomingCallWasNotCalled() {
let pushPayload = MockPKPushPayload()
pushPayload.stubType = .fileProvider
pushPayload.stubDictionaryPayload = [:]

subject.pushRegistry(
subject.registry,
didReceiveIncomingPushWith: pushPayload,
for: pushPayload.type,
completion: {}
func test_pushRegistryDidReceiveIncomingPush_typeIsVoIPWithDisplayNameAndCallerName_reportIncomingCallWasCalledAsExpected() {
assertDidReceivePushNotification(
.init(
cid: "123",
localizedCallerName: "TestUser",
callerId: "test_user"
),
displayName: "Stream Group Call"
)
}

XCTAssertNil(callKitService.reportIncomingCallWasCalled)
func test_pushRegistryDidReceiveIncomingPush_typeIsNotVoIP_reportIncomingCallWasNotCalled() {
assertDidReceivePushNotification(contentType: .fileProvider)
}

// MARK: - Private helpers
Expand All @@ -124,6 +107,64 @@ final class CallKitPushNotificationAdapterTests: XCTestCase {
for: .voIP
)
}

private func assertDidReceivePushNotification(
_ content: CallKitPushNotificationAdapter.Content? = nil,
contentType: PKPushType = .voIP,
displayName: String = "",
file: StaticString = #file,
line: UInt = #line
) {
let pushPayload = MockPKPushPayload()
pushPayload.stubType = contentType
pushPayload.stubDictionaryPayload = content.map { [
"stream": [
"call_cid": $0.cid,
"call_display_name": displayName,
"created_by_display_name": $0.localizedCallerName,
"created_by_id": $0.callerId
]
] } ?? [:]

let completionWasCalledExpectation = expectation(description: "Completion was called.")
completionWasCalledExpectation.isInverted = content == nil
subject.pushRegistry(
subject.registry,
didReceiveIncomingPushWith: pushPayload,
for: pushPayload.type,
completion: { completionWasCalledExpectation.fulfill() }
)

if let content {
XCTAssertEqual(
callKitService.reportIncomingCallWasCalled?.cid,
content.cid,
file: file,
line: line
)
XCTAssertEqual(
callKitService.reportIncomingCallWasCalled?.callerName,
displayName.isEmpty ? content.localizedCallerName : displayName,
file: file,
line: line
)
XCTAssertEqual(
callKitService.reportIncomingCallWasCalled?.callerId,
content.callerId,
file: file,
line: line
)
callKitService.reportIncomingCallWasCalled?.completion(nil)
} else {
XCTAssertNil(
callKitService.reportIncomingCallWasCalled,
file: file,
line: line
)
}

wait(for: [completionWasCalledExpectation], timeout: defaultTimeout)
}
}

// MARK: - Mocks
Expand Down
15 changes: 14 additions & 1 deletion docusaurus/docs/iOS/06-advanced/03-callkit-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ struct MyCustomView: View {

By doing that, the `CallKitAdapter` will make sure to unregister the `VoIP` token from receiving notifications._createMdxContent

#### Call display name

The Stream backend fills 2 properties in the VoIP push notification payload that can be used as the display name of the call.
- **call_display_name**
The `call_display_name` is a calculated property that evaluates the following properties on the Call object, in the order they are being presented:
- `display_name`
- `name`
- `title`
If none of the fields above are being set, the property will be empty.

- **created_by_display_name**
The property is always set and contains the name of the user who created the call.

### VoIP Token Observation

Even though by using the `CallKitAdapter` abstracts most of the `CallKit` & `PushKit` complexity from you, there are still cases where you may want to acces the device's `VoIP` token (e.g. to persist it ).
Expand Down Expand Up @@ -215,4 +228,4 @@ The important part is the `onContinueUserActivity`, where we listen to `INStartC

Additionally, if you have integration with the native contacts on iOS (`Contacts` framework), you can extract the full name, phone number etc, and use those to provide more details for the members. Alternatively, you can call our `queryUsers` method to get more user information that's available on the Stream backend.

If you are using UIKit, you should implement the method `application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void)` in your `AppDelegate`, and provide a similar handling as in the SwiftUI sample.
If you are using UIKit, you should implement the method `application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void)` in your `AppDelegate`, and provide a similar handling as in the SwiftUI sample.

0 comments on commit 8a69ac8

Please sign in to comment.