Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: differentiate every push event handler installed in app #751

Merged
merged 3 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/MessagingPush/PushHandling/PushEventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import Foundation
// A protocol that can handle push notification events. Such as when a push is received on the device or when a push is clicked on.
// Note: This is meant to be an abstraction of the iOS `UNUserNotificationCenterDelegate` protocol.
protocol PushEventHandler: AutoMockable {
// The SDK manages multiple push event handlers. We need a way to differentiate them between one another.
// The return value should uniquely identify the handler from other handlers installed in the app.
var identifier: String { get }

// Called when a push notification was acted upon. Either clicked or swiped away.
//
// Replacement of: `userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)`
Expand Down
12 changes: 6 additions & 6 deletions Sources/MessagingPush/PushHandling/PushEventHandlerProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protocol PushEventHandlerProxy: AutoMockable {
// sourcery: InjectSingleton
class PushEventHandlerProxyImpl: PushEventHandlerProxy {
// Use a map so that we only save 1 instance of a given handler.
@Atomic private var nestedDelegates: [String: PushEventHandler] = [:]
@Atomic var nestedDelegates: [String: PushEventHandler] = [:]

private let logger: Logger

Expand All @@ -30,7 +30,7 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy {
}

func addPushEventHandler(_ newHandler: PushEventHandler) {
nestedDelegates[String(describing: newHandler)] = newHandler
nestedDelegates[newHandler.identifier] = newHandler
}

func onPushAction(_ pushAction: PushNotificationAction, completionHandler: @escaping () -> Void) {
Expand Down Expand Up @@ -58,11 +58,11 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy {

// Using logs to give feedback to customer if 1 or more delegates do not call the async completion handler.
// These logs could help in debuggging to determine what delegate did not call the completion handler.
self.logger.info("Sending push notification, \(pushAction.push.title), event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...")
self.logger.info("Sending push notification, \(pushAction.push.title), action event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...")

delegate.onPushAction(pushAction) {
Task { @MainActor in // in case the delegate calls the completion handler on a background thread, we need to switch back to the main thread.
self.logger.info("Received async completion handler from \(nameOfDelegateClass).")
self.logger.info("Received async completion handler from \(nameOfDelegateClass) for action push event.")

if !hasResumed {
hasResumed = true
Expand Down Expand Up @@ -109,11 +109,11 @@ class PushEventHandlerProxyImpl: PushEventHandlerProxy {

// Using logs to give feedback to customer if 1 or more delegates do not call the async completion handler.
// These logs could help in debuggging to determine what delegate did not call the completion handler.
self.logger.info("Sending push notification, \(push.title), event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...")
self.logger.info("Sending push notification, \(push.title), will display event to: \(nameOfDelegateClass)). Customer.io SDK will wait for async completion handler to be called...")

delegate.shouldDisplayPushAppInForeground(push, completionHandler: { delegateShouldDisplayPushResult in
Task { @MainActor in // in case the delegate calls the completion handler on a background thread, we need to switch back to the main thread.
self.logger.info("Received async completion handler from \(nameOfDelegateClass).")
self.logger.info("Received async completion handler from \(nameOfDelegateClass) for will display event.")

if !hasResumed {
hasResumed = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class IOSPushEventListener: PushEventHandler {
self.logger = logger
}

var identifier: String {
"Cio.iOSPushEventListener"
}

func onPushAction(_ pushAction: PushNotificationAction, completionHandler: @escaping () -> Void) {
guard let dateWhenPushDelivered = pushAction.push.deliveryDate else {
logger.debug("[onPushAction] early exist due to missing deliveryDate for action: \(pushAction)")
Expand Down
18 changes: 11 additions & 7 deletions Sources/MessagingPush/UserNotificationsFramework/Wrappers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,19 @@ public struct UNNotificationWrapper: PushNotification {
}
}

class UNUserNotificationCenterDelegateWrapper: PushEventHandler {
class UNUserNotificationCenterDelegateWrapper: PushEventHandler, CustomStringConvertible {
private let delegate: UNUserNotificationCenterDelegate

var description: String {
let nestedDelegateDescription = String(describing: delegate)

return "Cio.NotificationCenterDelegateWrapper(\(nestedDelegateDescription))"
}

var identifier: String {
String(describing: delegate)
}

init(delegate: UNUserNotificationCenterDelegate) {
self.delegate = delegate
}
Expand Down Expand Up @@ -152,9 +162,3 @@ class UNUserNotificationCenterDelegateWrapper: PushEventHandler {
}
}
}

extension UNUserNotificationCenterDelegateWrapper: CustomStringConvertible {
var description: String {
String(describing: delegate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,45 @@ class PushEventHandlerMock: PushEventHandler, Mock {
Mocks.shared.add(mock: self)
}

/**
When setter of the property called, the value given to setter is set here.
When the getter of the property called, the value set here will be returned. Your chance to mock the property.
*/
var underlyingIdentifier: String!
/// `true` if the getter or setter of property is called at least once.
var identifierCalled: Bool {
identifierGetCalled || identifierSetCalled
}

/// `true` if the getter called on the property at least once.
var identifierGetCalled: Bool {
identifierGetCallsCount > 0
}

var identifierGetCallsCount = 0
/// `true` if the setter called on the property at least once.
var identifierSetCalled: Bool {
identifierSetCallsCount > 0
}

var identifierSetCallsCount = 0
/// The mocked property with a getter and setter.
var identifier: String {
get {
mockCalled = true
identifierGetCallsCount += 1
return underlyingIdentifier
}
set(value) {
mockCalled = true
identifierSetCallsCount += 1
underlyingIdentifier = value
}
}

public func resetMock() {
identifierGetCallsCount = 0
identifierSetCallsCount = 0
onPushActionCallsCount = 0
onPushActionReceivedArguments = nil
onPushActionReceivedInvocations = []
Expand Down
8 changes: 8 additions & 0 deletions Tests/MessagingPush/Core/IntegrationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ open class IntegrationTest: UnitTest {

return MessagingPush.shared
}

// Create new mock instance and setup with set of defaults.
func getNewPushEventHandler() -> PushEventHandlerMock {
let newInstance = PushEventHandlerMock()
// We expect that each instance has it's own unique identifier.
newInstance.underlyingIdentifier = .random
return newInstance
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {
func test_givenOtherPushHandlers_givenClickedOnCioPush_expectPushClickHandledByCioSdk() {
let expectOtherClickHandlerToGetCallback = expectation(description: "Receive a callback")

let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
givenOtherPushHandler.onPushActionClosure = { _, onComplete in
expectOtherClickHandlerToGetCallback.fulfill()
onComplete()
Expand All @@ -90,7 +90,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

func test_givenOtherPushHandlers_givenClickedOnPushNotSentFromCio_expectPushClickHandledByOtherHandler() {
let givenPush = PushNotificationStub.getPushNotSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()

let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
givenOtherPushHandler.onPushActionClosure = { _, onComplete in
Expand All @@ -108,21 +108,17 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

// Important to test that 2+ 3rd party push handlers for some use cases.
func test_givenMultiplePushHandlers_givenClickedOnCioPush_expectPushClickHandledByCioSdk() {
let expectOtherPushHandlersCalled = expectation(description: "Receive a callback")
expectOtherPushHandlersCalled.expectedFulfillmentCount = 2
let expectHandler1Called = expectation(description: "Receive a callback")
let expectHandler2Called = expectation(description: "Receive a callback")

// In order to add 2+ push handlers to SDK, each class needs to have a unique name.
// The SDK only accepts unique push event handlers. Creating this class makes each push handler unique.
class PushEventHandlerMock2: PushEventHandlerMock {}

let givenOtherPushHandler1 = PushEventHandlerMock()
let givenOtherPushHandler2 = PushEventHandlerMock2()
let givenOtherPushHandler1 = getNewPushEventHandler()
let givenOtherPushHandler2 = getNewPushEventHandler()
givenOtherPushHandler1.onPushActionClosure = { _, onComplete in
expectOtherPushHandlersCalled.fulfill()
expectHandler1Called.fulfill()
onComplete()
}
givenOtherPushHandler2.onPushActionClosure = { _, onComplete in
expectOtherPushHandlersCalled.fulfill()
expectHandler2Called.fulfill()
onComplete()
}
addOtherPushEventHandler(givenOtherPushHandler1)
Expand All @@ -144,7 +140,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {
func test_givenMultiplePushHandlers_givenClickedOnCioPush_givenOtherPushHandlerDoesNotCallCompletionHandler_expectCompletionHandlerDoesNotGetCalled() {
let expectOtherClickHandlerToGetCallback = expectation(description: "Receive a callback")
let givenPush = PushNotificationStub.getPushSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()

givenOtherPushHandler.onPushActionClosure = { _, _ in
// Do not call completion handler.
Expand All @@ -163,7 +159,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

func test_givenMultiplePushHandlers_givenClickedOnCioPush_givenOtherPushHandlerCallsCompletionHandler_expectCioSdkHandlesPush() {
let givenPush = PushNotificationStub.getPushSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
givenOtherPushHandler.onPushActionClosure = { _, onComplete in
onComplete()
}
Expand All @@ -189,7 +185,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

func test_onPushAction_givenMultiplePushClickHandlers_simulateFcmSdkSwizzlingBehavior_expectNoInfiniteLoop() {
let givenPush = PushNotificationStub.getPushNotSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
let givenPushClickAction = PushNotificationActionStub(push: givenPush, didClickOnPush: true)

let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
Expand All @@ -211,7 +207,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

func test_shouldDisplayPushAppInForeground_givenMultiplePushClickHandlers_simulateFcmSdkSwizzlingBehavior_expectNoInfiniteLoop() {
let givenPush = PushNotificationStub.getPushNotSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()

let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 1 // the other push click handler should only be called once, indicating an infinite loop is not created.
Expand Down Expand Up @@ -242,7 +238,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

func test_onPushAction_givenMultiplePushClickHandlers_thirdPartySdkCallsCompletionHandlerTwice_expectSdkDoesNotCrash() {
let givenPush = PushNotificationStub.getPushNotSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
let givenPushClickAction = PushNotificationActionStub(push: givenPush, didClickOnPush: true)

let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
Expand All @@ -264,7 +260,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {

func test_shouldDisplayPushAppInForeground_givenMultiplePushClickHandlers_thirdPartySdkCallsCompletionHandlerTwice_expectSdkDoesNotCrash() {
let givenPush = PushNotificationStub.getPushNotSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()

let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 1 // the other push click handler should only be called once, indicating an infinite loop is not created.
Expand Down Expand Up @@ -296,7 +292,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {
func test_givenClickOnLocalPush_expectOtherClickHandlerHandlesClickEvent() {
let givenLocalPush = PushNotificationStub.getLocalPush(pushId: .random)

let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 1
givenOtherPushHandler.onPushActionClosure = { _, onComplete in
Expand All @@ -316,7 +312,7 @@ class AutomaticPushClickedIntegrationTest: IntegrationTest {
let givenLocalPush = PushNotificationStub.getLocalPush(pushId: givenHardCodedPushId)
let givenSecondLocalPush = PushNotificationStub.getLocalPush(pushId: givenHardCodedPushId)

let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
let expectOtherClickHandlerHandlesPush = expectation(description: "Other push handler should handle push.")
expectOtherClickHandlerHandlesPush.expectedFulfillmentCount = 2 // Expect click handler to be able to handle both pushes, because each push is unique.
givenOtherPushHandler.onPushActionClosure = { _, onComplete in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest {
let givenPush = PushNotificationStub.getPushNotSentFromCIO()
var otherPushHandlerCalled = false

let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
givenOtherPushHandler.shouldDisplayPushAppInForegroundClosure = { _, onComplete in
otherPushHandlerCalled = true

Expand All @@ -72,7 +72,7 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest {
let givenPush = PushNotificationStub.getPushSentFromCIO()
let expectOtherPushHandlerCallbackCalled = expectation(description: "Expect other push handler callback called")

let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()
givenOtherPushHandler.shouldDisplayPushAppInForegroundClosure = { _, onComplete in
// We expect that other push handler gets callback of push event from CIO push
expectOtherPushHandlerCallbackCalled.fulfill()
Expand All @@ -97,12 +97,8 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest {
let expectOtherPushHandlerCallbackCalled = expectation(description: "Expect other push handler callback called")
expectOtherPushHandlerCallbackCalled.expectedFulfillmentCount = 2

// In order to add 2+ push handlers to SDK, each class needs to have a unique name.
// The SDK only accepts unique push event handlers. Creating this class makes each push handler unique.
class PushEventHandlerMock2: PushEventHandlerMock {}

let givenOtherPushHandler1 = PushEventHandlerMock()
let givenOtherPushHandler2 = PushEventHandlerMock2()
let givenOtherPushHandler1 = getNewPushEventHandler()
let givenOtherPushHandler2 = getNewPushEventHandler()
givenOtherPushHandler1.shouldDisplayPushAppInForegroundClosure = { _, onComplete in
expectOtherPushHandlerCallbackCalled.fulfill()

Expand All @@ -128,7 +124,7 @@ class AutomaticPushDeliveredAppInForegrondTest: IntegrationTest {

let expectOtherClickHandlerToGetCallback = expectation(description: "Receive a callback")
let givenPush = PushNotificationStub.getPushSentFromCIO()
let givenOtherPushHandler = PushEventHandlerMock()
let givenOtherPushHandler = getNewPushEventHandler()

givenOtherPushHandler.shouldDisplayPushAppInForegroundClosure = { _, _ in
// Do not call completion handler.
Expand Down
Loading
Loading