From aad6d2c28e1b91a7e2e01a59bcd41bf774bb9da8 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 13 Dec 2024 12:05:00 -0500 Subject: [PATCH 01/16] Handle new push payloads in extension and add sync events --- Sources/KlaviyoSwift/Klaviyo.swift | 1 + .../StateManagement/StateManagement.swift | 13 +++++++++ .../KlaviyoExtension.swift | 28 ++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Sources/KlaviyoSwift/Klaviyo.swift b/Sources/KlaviyoSwift/Klaviyo.swift index 18f3155d..9edfb347 100644 --- a/Sources/KlaviyoSwift/Klaviyo.swift +++ b/Sources/KlaviyoSwift/Klaviyo.swift @@ -163,6 +163,7 @@ public struct KlaviyoSDK { /// - deepLinkHandler: a completion handler that will be called when a notification contains a deep link. /// - Returns: true if the notificaiton originated from Klaviyo, false otherwise. public func handle(notificationResponse: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void, deepLinkHandler: ((URL) -> Void)? = nil) -> Bool { + KlaviyoBadgeCountUtil.syncBadgeCount() if let properties = notificationResponse.notification.request.content.userInfo as? [String: Any], let body = properties["body"] as? [String: Any], let _ = body["_k"] { create(event: Event(name: ._openedPush, properties: properties)) diff --git a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift index ac9e6abb..0b052730 100644 --- a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift @@ -289,6 +289,7 @@ struct KlaviyoReducer: ReducerProtocol { guard case .initialized = state.initalizationState else { return .none } + KlaviyoBadgeCountUtil.syncBadgeCount() return EffectPublisher.cancel(ids: [RequestId.self, FlushTimer.self]) .concatenate(with: .run(operation: { send in await send(.cancelInFlightRequests) @@ -306,6 +307,8 @@ struct KlaviyoReducer: ReducerProtocol { let autoclearing = await environment.getBadgeAutoClearingSetting() if autoclearing { await send(KlaviyoAction.setBadgeCount(0)) + } else { + KlaviyoBadgeCountUtil.syncBadgeCount() } }, environment.timer(state.flushInterval) @@ -585,6 +588,16 @@ extension Store where State == KlaviyoState, Action == KlaviyoAction { reducer: KlaviyoReducer()) } +enum KlaviyoBadgeCountUtil { + static func syncBadgeCount() { + DispatchQueue.main.async { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + userDefaults.set(UIApplication.shared.applicationIconBadgeNumber, forKey: "badgeCount") + } + } + } +} + extension Event { func updateEventWithState(state: inout KlaviyoState) -> Event { let identifiers = Identifiers( diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift index f1609062..26d64d19 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift @@ -7,6 +7,12 @@ import Foundation import UserNotifications +private enum KlaviyoBadgeConfig: String { + case incrementOne = "increment_one" + case setCount = "set_count" + case setProperty = "set_property" +} + public enum KlaviyoExtensionSDK { /// Call this method when you receive a rich push notification in the notification service extension. /// This method should be called from within `didReceive(_:withContentHandler:)` method of `UNNotificationServiceExtension`. @@ -24,6 +30,25 @@ public enum KlaviyoExtensionSDK { bestAttemptContent: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void, fallbackMediaType: String = "jpeg") { + // handle badge setting from the push notification payload + if let badgeConfig = bestAttemptContent.userInfo["badge_config"] as? String { + switch badgeConfig { + case KlaviyoBadgeConfig.incrementOne.rawValue: + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + let currentBadgeCount = userDefaults.integer(forKey: "badgeCount") + userDefaults.set(currentBadgeCount + 1, forKey: "badgeCount") + bestAttemptContent.badge = (currentBadgeCount + 1 as NSNumber) + } + case KlaviyoBadgeConfig.setCount.rawValue, KlaviyoBadgeConfig.setProperty.rawValue: + if let badgeValue = bestAttemptContent.userInfo["badge_value"] as? Int { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + userDefaults.set(badgeValue, forKey: "badgeCount") + } + } + default: break + } + } + // 1a. get the rich media url from the push notification payload guard let imageURLString = bestAttemptContent.userInfo["rich-media"] as? String else { contentHandler(bestAttemptContent) @@ -122,7 +147,8 @@ public enum KlaviyoExtensionSDK { guard let attachment = try? UNNotificationAttachment( identifier: "", url: localFileURLWithType, - options: nil) else { + options: nil) + else { completion(nil) return } From a21de67a8d0ab25f3d53f21552cd43028e948286 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Mon, 16 Dec 2024 18:12:23 -0500 Subject: [PATCH 02/16] add README changes --- README.md | 65 +++++++------------ .../KlaviyoExtension.swift | 1 + 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 59ad4d00..73dd105e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ - [Prerequisites](#prerequisites) - [Collecting Push Tokens](#collecting-push-tokens) - [Request Push Notification Permission](#request-push-notification-permission) + - [Badge Count](#badge-count) + - [Set Up](#set-up) + - [Autoclearing Badge Count](#autoclearing-badge-count) + - [Handling Other Badging Sources](#handling-other-badging-sources) - [Receiving Push Notifications](#receiving-push-notifications) - [Tracking Open Events](#tracking-open-events) - [Deep Linking](#deep-linking) @@ -43,7 +47,7 @@ Once integrated, your marketing team will be able to better understand your app ## Installation 1. Enable push notification capabilities in your Xcode project. The section "Enable the push notification capability" in this [Apple developer guide](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns#2980170) provides detailed instructions. -2. [Optional but recommended] If you intend to use [rich push notifications](#rich-push) add a [Notification service extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your xcode project. A notification service app extension ships as a separate bundle inside your iOS app. To add this extension to your app: +2. [Optional but recommended] If you intend to use [rich push notifications](#rich-push) or [custom badge counts](#custom-badge-count) add a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your Xcode project. A notification service app extension ships as a separate bundle inside your iOS app. To add this extension to your app: - Select File > New > Target in Xcode. - Select the Notification Service Extension target from the iOS > Application extension section. - Click Next. @@ -91,7 +95,7 @@ Once integrated, your marketing team will be able to better understand your app 4. Finally, in the `NotificationService.swift` file add the code for the two required delegates from [this](Examples/KlaviyoSwiftExamples/SPMExample/NotificationServiceExtension/NotificationService.swift) file. - This sample covers calling into Klaviyo so that we can download and attach the media to the push notification. + This sample covers calling into Klaviyo so that we can download and attach the media to the push notification as well as handle custom badge counts. ## Initialization The SDK must be initialized with the short alphanumeric [public API key](https://help.klaviyo.com/hc/en-us/articles/115005062267#difference-between-public-and-private-api-keys1) @@ -274,6 +278,25 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau return true } ``` +### Badge Count + +#### Set Up +Klaviyo supports custom badge counts when configuring your push notification in the editor dashboard. To set up your app, so the Klaviyo SDK properly handles them: + +(Pre-requisite: Set up the Push Notification Service Extension) +1. In XCode, select your main app target, then go to Signing & Capabilities +2. Add an App Groups capability and click the plus in the new section to add a new App Group +3. Pick a name based on the scheme `group.[MainTargetBundleId].[descriptor]` +4. Select your Service Extension target, and add the same App Group with the same name +5. In your app's `Info.plist`, add a new entry for `Klaviyo_App_Group` as a String with the App Group name + +#### Autoclearing Badge Count + +If you want to automatically clear the badge count of your app to 0 when the app is opened, in your app's `Info.plist`, add a new entry for `Klaviyo_badge_autoclearing` as a Boolean set to `YES`. + +#### Handling Other Badging Sources + +Klaviyo SDK handles Klaviyo pushes, but if you have other sources that change the badge count, use the `KlaviyoSDK().setBadgeCount(:)` method wherever you change the badge count to keep in sync with SDK count. ### Receiving Push Notifications @@ -313,44 +336,6 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } } ``` -When tracking opened push notification, you can also decrement the badge count on the app icon by adding the following code to the `userNotificationCenter:didReceive:withCompletionHandler` method: - -```swift - func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - // decrement the badge count on the app icon - if #available(iOS 16.0, *) { - UNUserNotificationCenter.current().setBadgeCount(UIApplication.shared.applicationIconBadgeNumber - 1) - } else { - UIApplication.shared.applicationIconBadgeNumber -= 1 - } - - // If this notification is Klaviyo's notification we'll handle it - // else pass it on to the next push notification service to which it may belong - let handled = KlaviyoSDK().handle(notificationResponse: response, withCompletionHandler: completionHandler) - if !handled { - completionHandler() - } - } -``` - -Additionally, if you just want to reset the badge count to zero when the app is opened(note that this could be from -the user just opening the app independent of the push message), you can add the following code to -the `applicationDidBecomeActive` method in the app delegate: - -```swift - -func applicationDidBecomeActive(_ application: UIApplication) { - // reset the badge count on the app icon - if #available(iOS 16.0, *) { - UNUserNotificationCenter.current().setBadgeCount(0) - } else { - UIApplication.shared.applicationIconBadgeNumber = 0 - } -} -``` Once your first push notifications are sent and opened, you should start to see _Opened Push_ metrics within your Klaviyo dashboard. diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift index 26d64d19..507a93d0 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift @@ -44,6 +44,7 @@ public enum KlaviyoExtensionSDK { if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { userDefaults.set(badgeValue, forKey: "badgeCount") } + bestAttemptContent.badge = (badgeValue as NSNumber) } default: break } From 4b5ce4d57cc995becf574495334acea2a3d02520 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Tue, 17 Dec 2024 11:59:41 -0500 Subject: [PATCH 03/16] Add extra setup instructions for extension --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 73dd105e..dccc0505 100644 --- a/README.md +++ b/README.md @@ -283,12 +283,13 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau #### Set Up Klaviyo supports custom badge counts when configuring your push notification in the editor dashboard. To set up your app, so the Klaviyo SDK properly handles them: -(Pre-requisite: Set up the Push Notification Service Extension) +(Pre-requisite: Set up the Notification Service Extension) 1. In XCode, select your main app target, then go to Signing & Capabilities 2. Add an App Groups capability and click the plus in the new section to add a new App Group -3. Pick a name based on the scheme `group.[MainTargetBundleId].[descriptor]` +3. Pick a name based on the scheme `group.com.[MainTargetBundleId].[descriptor]` 4. Select your Service Extension target, and add the same App Group with the same name 5. In your app's `Info.plist`, add a new entry for `Klaviyo_App_Group` as a String with the App Group name +6. In your Notification Service Extension's `Info.plist`, add a new entry for `Klaviyo_App_Group` as a String with the App Group name #### Autoclearing Badge Count From feffbb7b9db546177af9d4e2f1426a40bf4a4038 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Wed, 18 Dec 2024 11:18:24 -0500 Subject: [PATCH 04/16] change default to autoclear and add disabling instructions --- README.md | 2 +- Sources/KlaviyoCore/KlaviyoEnvironment.swift | 10 +++++----- .../KlaviyoSwift/StateManagement/StateManagement.swift | 8 ++++---- Tests/KlaviyoCoreTests/TestUtils.swift | 2 +- Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift | 2 +- .../StateManagementEdgeCaseTests.swift | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index dccc0505..1b07118d 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ Klaviyo supports custom badge counts when configuring your push notification in #### Autoclearing Badge Count -If you want to automatically clear the badge count of your app to 0 when the app is opened, in your app's `Info.plist`, add a new entry for `Klaviyo_badge_autoclearing` as a Boolean set to `YES`. +By default, Klaviyo SDK automatically clears all badges on app open. If you want to disable this behavior, in your app's `Info.plist`, add a new entry for `disable_Klaviyo_badge_autoclearing` as a Boolean set to `YES`. #### Handling Other Badging Sources diff --git a/Sources/KlaviyoCore/KlaviyoEnvironment.swift b/Sources/KlaviyoCore/KlaviyoEnvironment.swift index ea286c2d..0b4c4372 100644 --- a/Sources/KlaviyoCore/KlaviyoEnvironment.swift +++ b/Sources/KlaviyoCore/KlaviyoEnvironment.swift @@ -22,7 +22,7 @@ public struct KlaviyoEnvironment { notificationCenterPublisher: @escaping (NSNotification.Name) -> AnyPublisher, getNotificationSettings: @escaping () async -> PushEnablement, getBackgroundSetting: @escaping () -> PushBackground, - getBadgeAutoClearingSetting: @escaping () async -> Bool, + getBadgeAutoClearingIsDisabled: @escaping () async -> Bool, startReachability: @escaping () throws -> Void, stopReachability: @escaping () -> Void, reachabilityStatus: @escaping () -> Reachability.NetworkStatus?, @@ -49,7 +49,7 @@ public struct KlaviyoEnvironment { self.notificationCenterPublisher = notificationCenterPublisher self.getNotificationSettings = getNotificationSettings self.getBackgroundSetting = getBackgroundSetting - self.getBadgeAutoClearingSetting = getBadgeAutoClearingSetting + self.getBadgeAutoClearingIsDisabled = getBadgeAutoClearingIsDisabled self.startReachability = startReachability self.stopReachability = stopReachability self.reachabilityStatus = reachabilityStatus @@ -96,7 +96,7 @@ public struct KlaviyoEnvironment { public var notificationCenterPublisher: (NSNotification.Name) -> AnyPublisher public var getNotificationSettings: () async -> PushEnablement public var getBackgroundSetting: () -> PushBackground - public var getBadgeAutoClearingSetting: () async -> Bool + public var getBadgeAutoClearingIsDisabled: () async -> Bool public var startReachability: () throws -> Void public var stopReachability: () -> Void @@ -154,8 +154,8 @@ public struct KlaviyoEnvironment { getBackgroundSetting: { .create(from: UIApplication.shared.backgroundRefreshStatus) }, - getBadgeAutoClearingSetting: { - Bundle.main.object(forInfoDictionaryKey: "Klaviyo_badge_autoclearing") as? Bool ?? true + getBadgeAutoClearingIsDisabled: { + Bundle.main.object(forInfoDictionaryKey: "disable_Klaviyo_badge_autoclearing") as? Bool ?? true }, startReachability: { try reachabilityService?.startNotifier() diff --git a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift index 0b052730..3f1f43ee 100644 --- a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift @@ -304,11 +304,11 @@ struct KlaviyoReducer: ReducerProtocol { .run { send in let settings = await environment.getNotificationSettings() await send(KlaviyoAction.setPushEnablement(settings)) - let autoclearing = await environment.getBadgeAutoClearingSetting() - if autoclearing { - await send(KlaviyoAction.setBadgeCount(0)) - } else { + let disabled = await environment.getBadgeAutoClearingIsDisabled() + if disabled { KlaviyoBadgeCountUtil.syncBadgeCount() + } else { + await send(KlaviyoAction.setBadgeCount(0)) } }, environment.timer(state.flushInterval) diff --git a/Tests/KlaviyoCoreTests/TestUtils.swift b/Tests/KlaviyoCoreTests/TestUtils.swift index 427bef74..45effce0 100644 --- a/Tests/KlaviyoCoreTests/TestUtils.swift +++ b/Tests/KlaviyoCoreTests/TestUtils.swift @@ -87,7 +87,7 @@ extension KlaviyoEnvironment { notificationCenterPublisher: { _ in Empty().eraseToAnyPublisher() }, getNotificationSettings: { .authorized }, getBackgroundSetting: { .available }, - getBadgeAutoClearingSetting: { true }, + getBadgeAutoClearingIsDisabled: { false }, startReachability: {}, stopReachability: {}, reachabilityStatus: { nil }, diff --git a/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift b/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift index 99baadea..2172ab28 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift @@ -35,7 +35,7 @@ extension KlaviyoEnvironment { notificationCenterPublisher: { _ in Empty().eraseToAnyPublisher() }, getNotificationSettings: { .authorized }, getBackgroundSetting: { .available }, - getBadgeAutoClearingSetting: { true }, + getBadgeAutoClearingIsDisabled: { false }, startReachability: {}, stopReachability: {}, reachabilityStatus: { nil }, diff --git a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift index de96dc2d..93da00d7 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift @@ -347,7 +347,7 @@ class StateManagementEdgeCaseTests: XCTestCase { @MainActor func testDefaultBadgeClearingOn() async throws { let apiKey = "fake-key" - environment.getBadgeAutoClearingSetting = { true } + environment.getBadgeAutoClearingIsDisabled = { false } let expectation = XCTestExpectation(description: "Should set badge to 0") klaviyoSwiftEnvironment.setBadgeCount = { _ in expectation.fulfill() @@ -380,7 +380,7 @@ class StateManagementEdgeCaseTests: XCTestCase { @MainActor func testDefaultBadgeClearingOff() async { let apiKey = "fake-key" - environment.getBadgeAutoClearingSetting = { false } + environment.getBadgeAutoClearingIsDisabled = { true } let expectation = XCTestExpectation(description: "Should not set badge to 0") expectation.isInverted = true klaviyoSwiftEnvironment.setBadgeCount = { _ in From fab95ea051853676512ab15dbda1a6f0646df0ea Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Wed, 18 Dec 2024 11:45:24 -0500 Subject: [PATCH 05/16] flip fallback value --- Sources/KlaviyoCore/KlaviyoEnvironment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/KlaviyoCore/KlaviyoEnvironment.swift b/Sources/KlaviyoCore/KlaviyoEnvironment.swift index 0b4c4372..38ab2ea2 100644 --- a/Sources/KlaviyoCore/KlaviyoEnvironment.swift +++ b/Sources/KlaviyoCore/KlaviyoEnvironment.swift @@ -155,7 +155,7 @@ public struct KlaviyoEnvironment { .create(from: UIApplication.shared.backgroundRefreshStatus) }, getBadgeAutoClearingIsDisabled: { - Bundle.main.object(forInfoDictionaryKey: "disable_Klaviyo_badge_autoclearing") as? Bool ?? true + Bundle.main.object(forInfoDictionaryKey: "disable_Klaviyo_badge_autoclearing") as? Bool ?? false }, startReachability: { try reachabilityService?.startNotifier() From a5963ac4ea5702b7c307ab8b2ca62bf658f2a99b Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Wed, 18 Dec 2024 13:28:06 -0500 Subject: [PATCH 06/16] flip logic back for clarity --- README.md | 2 +- Sources/KlaviyoCore/KlaviyoEnvironment.swift | 10 +++++----- .../KlaviyoSwift/StateManagement/StateManagement.swift | 8 ++++---- Tests/KlaviyoCoreTests/TestUtils.swift | 2 +- Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift | 2 +- .../StateManagementEdgeCaseTests.swift | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1b07118d..c93d290c 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ Klaviyo supports custom badge counts when configuring your push notification in #### Autoclearing Badge Count -By default, Klaviyo SDK automatically clears all badges on app open. If you want to disable this behavior, in your app's `Info.plist`, add a new entry for `disable_Klaviyo_badge_autoclearing` as a Boolean set to `YES`. +By default, Klaviyo SDK automatically clears all badges on app open. If you want to disable this behavior, in your app's `Info.plist`, add a new entry for `klaviyo_badge_autoclearing` as a Boolean set to `NO`. You can turn this on again by setting this to `YES`. #### Handling Other Badging Sources diff --git a/Sources/KlaviyoCore/KlaviyoEnvironment.swift b/Sources/KlaviyoCore/KlaviyoEnvironment.swift index 38ab2ea2..6ff336fe 100644 --- a/Sources/KlaviyoCore/KlaviyoEnvironment.swift +++ b/Sources/KlaviyoCore/KlaviyoEnvironment.swift @@ -22,7 +22,7 @@ public struct KlaviyoEnvironment { notificationCenterPublisher: @escaping (NSNotification.Name) -> AnyPublisher, getNotificationSettings: @escaping () async -> PushEnablement, getBackgroundSetting: @escaping () -> PushBackground, - getBadgeAutoClearingIsDisabled: @escaping () async -> Bool, + getBadgeAutoClearingSetting: @escaping () async -> Bool, startReachability: @escaping () throws -> Void, stopReachability: @escaping () -> Void, reachabilityStatus: @escaping () -> Reachability.NetworkStatus?, @@ -49,7 +49,7 @@ public struct KlaviyoEnvironment { self.notificationCenterPublisher = notificationCenterPublisher self.getNotificationSettings = getNotificationSettings self.getBackgroundSetting = getBackgroundSetting - self.getBadgeAutoClearingIsDisabled = getBadgeAutoClearingIsDisabled + self.getBadgeAutoClearingSetting = getBadgeAutoClearingSetting self.startReachability = startReachability self.stopReachability = stopReachability self.reachabilityStatus = reachabilityStatus @@ -96,7 +96,7 @@ public struct KlaviyoEnvironment { public var notificationCenterPublisher: (NSNotification.Name) -> AnyPublisher public var getNotificationSettings: () async -> PushEnablement public var getBackgroundSetting: () -> PushBackground - public var getBadgeAutoClearingIsDisabled: () async -> Bool + public var getBadgeAutoClearingSetting: () async -> Bool public var startReachability: () throws -> Void public var stopReachability: () -> Void @@ -154,8 +154,8 @@ public struct KlaviyoEnvironment { getBackgroundSetting: { .create(from: UIApplication.shared.backgroundRefreshStatus) }, - getBadgeAutoClearingIsDisabled: { - Bundle.main.object(forInfoDictionaryKey: "disable_Klaviyo_badge_autoclearing") as? Bool ?? false + getBadgeAutoClearingSetting: { + Bundle.main.object(forInfoDictionaryKey: "klaviyo_badge_autoclearing") as? Bool ?? true }, startReachability: { try reachabilityService?.startNotifier() diff --git a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift index 3f1f43ee..0b052730 100644 --- a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift @@ -304,11 +304,11 @@ struct KlaviyoReducer: ReducerProtocol { .run { send in let settings = await environment.getNotificationSettings() await send(KlaviyoAction.setPushEnablement(settings)) - let disabled = await environment.getBadgeAutoClearingIsDisabled() - if disabled { - KlaviyoBadgeCountUtil.syncBadgeCount() - } else { + let autoclearing = await environment.getBadgeAutoClearingSetting() + if autoclearing { await send(KlaviyoAction.setBadgeCount(0)) + } else { + KlaviyoBadgeCountUtil.syncBadgeCount() } }, environment.timer(state.flushInterval) diff --git a/Tests/KlaviyoCoreTests/TestUtils.swift b/Tests/KlaviyoCoreTests/TestUtils.swift index 45effce0..427bef74 100644 --- a/Tests/KlaviyoCoreTests/TestUtils.swift +++ b/Tests/KlaviyoCoreTests/TestUtils.swift @@ -87,7 +87,7 @@ extension KlaviyoEnvironment { notificationCenterPublisher: { _ in Empty().eraseToAnyPublisher() }, getNotificationSettings: { .authorized }, getBackgroundSetting: { .available }, - getBadgeAutoClearingIsDisabled: { false }, + getBadgeAutoClearingSetting: { true }, startReachability: {}, stopReachability: {}, reachabilityStatus: { nil }, diff --git a/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift b/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift index 2172ab28..99baadea 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift @@ -35,7 +35,7 @@ extension KlaviyoEnvironment { notificationCenterPublisher: { _ in Empty().eraseToAnyPublisher() }, getNotificationSettings: { .authorized }, getBackgroundSetting: { .available }, - getBadgeAutoClearingIsDisabled: { false }, + getBadgeAutoClearingSetting: { true }, startReachability: {}, stopReachability: {}, reachabilityStatus: { nil }, diff --git a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift index 93da00d7..de96dc2d 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift @@ -347,7 +347,7 @@ class StateManagementEdgeCaseTests: XCTestCase { @MainActor func testDefaultBadgeClearingOn() async throws { let apiKey = "fake-key" - environment.getBadgeAutoClearingIsDisabled = { false } + environment.getBadgeAutoClearingSetting = { true } let expectation = XCTestExpectation(description: "Should set badge to 0") klaviyoSwiftEnvironment.setBadgeCount = { _ in expectation.fulfill() @@ -380,7 +380,7 @@ class StateManagementEdgeCaseTests: XCTestCase { @MainActor func testDefaultBadgeClearingOff() async { let apiKey = "fake-key" - environment.getBadgeAutoClearingIsDisabled = { true } + environment.getBadgeAutoClearingSetting = { false } let expectation = XCTestExpectation(description: "Should not set badge to 0") expectation.isInverted = true klaviyoSwiftEnvironment.setBadgeCount = { _ in From 06c945aa4f21b98a2fccaa67a16b2e69b012488e Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Wed, 18 Dec 2024 13:45:12 -0500 Subject: [PATCH 07/16] add badge_value check to default enum case --- Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift index 507a93d0..b5bdc65a 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift @@ -46,7 +46,13 @@ public enum KlaviyoExtensionSDK { } bestAttemptContent.badge = (badgeValue as NSNumber) } - default: break + default: + if let badgeValue = bestAttemptContent.userInfo["badge_value"] as? Int { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + userDefaults.set(badgeValue, forKey: "badgeCount") + } + bestAttemptContent.badge = (badgeValue as NSNumber) + } } } From 14c4638947dbabc8349721e412828be7039ab03a Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Wed, 18 Dec 2024 16:38:25 -0500 Subject: [PATCH 08/16] make syncBadgeCount a KlaviyoAction --- Sources/KlaviyoSwift/Klaviyo.swift | 3 +-- .../StateManagement/StateManagement.swift | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/KlaviyoSwift/Klaviyo.swift b/Sources/KlaviyoSwift/Klaviyo.swift index 8856fbe5..aad7cdf8 100644 --- a/Sources/KlaviyoSwift/Klaviyo.swift +++ b/Sources/KlaviyoSwift/Klaviyo.swift @@ -163,7 +163,6 @@ public struct KlaviyoSDK { /// - deepLinkHandler: a completion handler that will be called when a notification contains a deep link. /// - Returns: true if the notificaiton originated from Klaviyo, false otherwise. public func handle(notificationResponse: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void, deepLinkHandler: ((URL) -> Void)? = nil) -> Bool { - KlaviyoBadgeCountUtil.syncBadgeCount() if let properties = notificationResponse.notification.request.content.userInfo as? [String: Any], let body = properties["body"] as? [String: Any], let _ = body["_k"] { create(event: Event(name: ._openedPush, properties: properties)) @@ -179,9 +178,9 @@ public struct KlaviyoSDK { completionHandler() } } - return true } + dispatchOnMainThread(action: .syncBadgeCount) return false } } diff --git a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift index 0b052730..131542de 100644 --- a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift @@ -55,6 +55,9 @@ enum KlaviyoAction: Equatable { /// call to set the app badge count as well as update the stored value in the User Defaults suite case setBadgeCount(Int) + /// call to sync the stored value in the User Defaults suite with the currently displayed badge count provided by `UIApplication.shared.applicationIconBadgeNumber` + case syncBadgeCount + /// called when the user wants to reset the existing profile from state case resetProfile @@ -111,7 +114,7 @@ enum KlaviyoAction: Equatable { case .setEmail, .setPhoneNumber, .setExternalId, .setPushToken, .setPushEnablement, .enqueueProfile, .setProfileProperty, .setBadgeCount, .resetProfile, .resetStateAndDequeue, .enqueueEvent, .fetchForms, .handleFormsResponse: return true - case .initialize, .completeInitialization, .deQueueCompletedResults, .networkConnectivityChanged, .flushQueue, .sendRequest, .stop, .start, .cancelInFlightRequests, .requestFailed: + case .initialize, .completeInitialization, .deQueueCompletedResults, .networkConnectivityChanged, .flushQueue, .sendRequest, .stop, .start, .syncBadgeCount, .cancelInFlightRequests, .requestFailed: return false } } @@ -289,10 +292,10 @@ struct KlaviyoReducer: ReducerProtocol { guard case .initialized = state.initalizationState else { return .none } - KlaviyoBadgeCountUtil.syncBadgeCount() return EffectPublisher.cancel(ids: [RequestId.self, FlushTimer.self]) .concatenate(with: .run(operation: { send in await send(.cancelInFlightRequests) + await send(KlaviyoAction.syncBadgeCount) })) case .start: @@ -308,7 +311,7 @@ struct KlaviyoReducer: ReducerProtocol { if autoclearing { await send(KlaviyoAction.setBadgeCount(0)) } else { - KlaviyoBadgeCountUtil.syncBadgeCount() + await send(KlaviyoAction.syncBadgeCount) } }, environment.timer(state.flushInterval) @@ -516,6 +519,14 @@ struct KlaviyoReducer: ReducerProtocol { _ = klaviyoSwiftEnvironment.setBadgeCount(count) } + case .syncBadgeCount: + DispatchQueue.main.async { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + userDefaults.set(UIApplication.shared.applicationIconBadgeNumber, forKey: "badgeCount") + } + } + return .none + case .resetProfile: guard case .initialized = state.initalizationState else { @@ -588,16 +599,6 @@ extension Store where State == KlaviyoState, Action == KlaviyoAction { reducer: KlaviyoReducer()) } -enum KlaviyoBadgeCountUtil { - static func syncBadgeCount() { - DispatchQueue.main.async { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { - userDefaults.set(UIApplication.shared.applicationIconBadgeNumber, forKey: "badgeCount") - } - } - } -} - extension Event { func updateEventWithState(state: inout KlaviyoState) -> Event { let identifiers = Identifiers( From 8659c9b126082f28cc00ca92834bbeb8df4b049f Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Thu, 19 Dec 2024 09:53:18 -0500 Subject: [PATCH 09/16] add syncBadgeCount to tests --- .../KlaviyoSwift/StateManagement/StateManagement.swift | 8 +++++--- .../KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift | 1 + Tests/KlaviyoSwiftTests/StateManagementTests.swift | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift index 131542de..7ac6acac 100644 --- a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift @@ -520,9 +520,11 @@ struct KlaviyoReducer: ReducerProtocol { } case .syncBadgeCount: - DispatchQueue.main.async { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { - userDefaults.set(UIApplication.shared.applicationIconBadgeNumber, forKey: "badgeCount") + Task { + await MainActor.run { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + userDefaults.set(UIApplication.shared.applicationIconBadgeNumber, forKey: "badgeCount") + } } } return .none diff --git a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift index de96dc2d..efe0f785 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift @@ -405,6 +405,7 @@ class StateManagementEdgeCaseTests: XCTestCase { await store.receive(.start) await store.receive(.flushQueue) await store.receive(.setPushEnablement(PushEnablement.authorized)) + await store.receive(.syncBadgeCount) await fulfillment(of: [expectation], timeout: 1, enforceOrder: true) } diff --git a/Tests/KlaviyoSwiftTests/StateManagementTests.swift b/Tests/KlaviyoSwiftTests/StateManagementTests.swift index 74f66f48..0ec96196 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementTests.swift @@ -433,6 +433,7 @@ class StateManagementTests: XCTestCase { $0.queue = [request, request2] $0.requestsInFlight = [] } + await store.receive(.syncBadgeCount) } // MARK: - Test pending profile From b7c0c2ab10a5b494e72fc624e0d462691192bb05 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Thu, 19 Dec 2024 15:04:28 -0500 Subject: [PATCH 10/16] fix failing test --- Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift b/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift index 3a237063..92eae7f7 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift @@ -146,6 +146,7 @@ class KlaviyoSDKTests: XCTestCase { // MARK: test unhandle push notification func testUnhandlePushNotification() throws { + let expectation = setupActionAssertion(expectedAction: .syncBadgeCount) let callback = XCTestExpectation(description: "callback is not made") callback.isInverted = true let data: [AnyHashable: Any] = [ @@ -161,7 +162,7 @@ class KlaviyoSDKTests: XCTestCase { callback.fulfill() } - wait(for: [callback], timeout: 1.0) + wait(for: [callback, expectation], timeout: 1.0) XCTAssertFalse(handled) } From 9a6c182c8d8ee04ef9a794bc73987ba892ed81f0 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 20 Dec 2024 10:58:04 -0500 Subject: [PATCH 11/16] cleanup on logic and naming --- README.md | 4 +-- .../KlaviyoSwiftEnvironment.swift | 2 +- .../StateManagement/StateManagement.swift | 6 ++-- .../KlaviyoExtension.swift | 28 ++++++++----------- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c93d290c..23396dbd 100644 --- a/README.md +++ b/README.md @@ -288,8 +288,8 @@ Klaviyo supports custom badge counts when configuring your push notification in 2. Add an App Groups capability and click the plus in the new section to add a new App Group 3. Pick a name based on the scheme `group.com.[MainTargetBundleId].[descriptor]` 4. Select your Service Extension target, and add the same App Group with the same name -5. In your app's `Info.plist`, add a new entry for `Klaviyo_App_Group` as a String with the App Group name -6. In your Notification Service Extension's `Info.plist`, add a new entry for `Klaviyo_App_Group` as a String with the App Group name +5. In your app's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name +6. In your Notification Service Extension's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name #### Autoclearing Badge Count diff --git a/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift b/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift index 164a871f..f34fc93d 100644 --- a/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift +++ b/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift @@ -31,7 +31,7 @@ struct KlaviyoSwiftEnvironment { stateChangePublisher: StateChangePublisher().publisher, setBadgeCount: { count in Task { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "klaviyo_app_group") as? String) { if #available(iOS 16.0, *) { try? await UNUserNotificationCenter.current().setBadgeCount(count) } else { diff --git a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift index 7ac6acac..9c353438 100644 --- a/Sources/KlaviyoSwift/StateManagement/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement/StateManagement.swift @@ -111,10 +111,10 @@ enum KlaviyoAction: Equatable { case let .enqueueEvent(event) where event.metric.name == ._openedPush: return false - case .setEmail, .setPhoneNumber, .setExternalId, .setPushToken, .setPushEnablement, .enqueueProfile, .setProfileProperty, .setBadgeCount, .resetProfile, .resetStateAndDequeue, .enqueueEvent, .fetchForms, .handleFormsResponse: + case .enqueueEvent, .enqueueProfile, .fetchForms, .handleFormsResponse, .resetProfile, .resetStateAndDequeue, .setBadgeCount, .setEmail, .setExternalId, .setPhoneNumber, .setProfileProperty, .setPushEnablement, .setPushToken: return true - case .initialize, .completeInitialization, .deQueueCompletedResults, .networkConnectivityChanged, .flushQueue, .sendRequest, .stop, .start, .syncBadgeCount, .cancelInFlightRequests, .requestFailed: + case .cancelInFlightRequests, .completeInitialization, .deQueueCompletedResults, .flushQueue, .initialize, .networkConnectivityChanged, .requestFailed, .sendRequest, .start, .stop, .syncBadgeCount: return false } } @@ -522,7 +522,7 @@ struct KlaviyoReducer: ReducerProtocol { case .syncBadgeCount: Task { await MainActor.run { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { + if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "klaviyo_app_group") as? String) { userDefaults.set(UIApplication.shared.applicationIconBadgeNumber, forKey: "badgeCount") } } diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift index b5bdc65a..250d016e 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift @@ -32,28 +32,24 @@ public enum KlaviyoExtensionSDK { fallbackMediaType: String = "jpeg") { // handle badge setting from the push notification payload if let badgeConfig = bestAttemptContent.userInfo["badge_config"] as? String { + guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "klaviyo_app_group") as? String, + let userDefaults = UserDefaults(suiteName: appGroup) else { + return + } + + var newBadgeValue: Int? switch badgeConfig { case KlaviyoBadgeConfig.incrementOne.rawValue: - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { - let currentBadgeCount = userDefaults.integer(forKey: "badgeCount") - userDefaults.set(currentBadgeCount + 1, forKey: "badgeCount") - bestAttemptContent.badge = (currentBadgeCount + 1 as NSNumber) - } - case KlaviyoBadgeConfig.setCount.rawValue, KlaviyoBadgeConfig.setProperty.rawValue: - if let badgeValue = bestAttemptContent.userInfo["badge_value"] as? Int { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { - userDefaults.set(badgeValue, forKey: "badgeCount") - } - bestAttemptContent.badge = (badgeValue as NSNumber) - } + let currentBadgeCount = userDefaults.integer(forKey: "badgeCount") + newBadgeValue = currentBadgeCount + 1 default: if let badgeValue = bestAttemptContent.userInfo["badge_value"] as? Int { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "Klaviyo_App_Group") as? String) { - userDefaults.set(badgeValue, forKey: "badgeCount") - } - bestAttemptContent.badge = (badgeValue as NSNumber) + newBadgeValue = badgeValue } } + + userDefaults.set(newBadgeValue, forKey: "badgeCount") + bestAttemptContent.badge = newBadgeValue as? NSNumber } // 1a. get the rich media url from the push notification payload From ae4689c0c83628a4271e6289011b4d3e58304539 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 20 Dec 2024 14:30:21 -0500 Subject: [PATCH 12/16] do not handle any unknown future config cases --- README.md | 2 +- .../KlaviyoSwiftEnvironment.swift | 18 +++++++------ .../KlaviyoExtension.swift | 27 ++++++++++++++----- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 23396dbd..986d8cca 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ By default, Klaviyo SDK automatically clears all badges on app open. If you want #### Handling Other Badging Sources -Klaviyo SDK handles Klaviyo pushes, but if you have other sources that change the badge count, use the `KlaviyoSDK().setBadgeCount(:)` method wherever you change the badge count to keep in sync with SDK count. +Klaviyo SDK will automatically handle the badge count associated with Klaviyo pushes. If you need to manually update the badge count to account for other notification sources, use the `KlaviyoSDK().setBadgeCount(:)` method, which will update the badge count and keep it in sync with the Klaviyo SDK. This method should be used instead of (rather than in addition to) setting the badge count using `UNUserNotificationCenter` and/or `UIApplication` methods. ### Receiving Push Notifications diff --git a/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift b/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift index f34fc93d..cebb8ce7 100644 --- a/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift +++ b/Sources/KlaviyoSwift/KlaviyoSwiftEnvironment.swift @@ -31,16 +31,18 @@ struct KlaviyoSwiftEnvironment { stateChangePublisher: StateChangePublisher().publisher, setBadgeCount: { count in Task { - if let userDefaults = UserDefaults(suiteName: Bundle.main.object(forInfoDictionaryKey: "klaviyo_app_group") as? String) { - if #available(iOS 16.0, *) { - try? await UNUserNotificationCenter.current().setBadgeCount(count) - } else { - await MainActor.run { - UIApplication.shared.applicationIconBadgeNumber = count - } + guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "klaviyo_app_group") as? String, + let userDefaults = UserDefaults(suiteName: appGroup) else { + return + } + if #available(iOS 16.0, *) { + try? await UNUserNotificationCenter.current().setBadgeCount(count) + } else { + await MainActor.run { + UIApplication.shared.applicationIconBadgeNumber = count } - userDefaults.set(count, forKey: "badgeCount") } + userDefaults.set(count, forKey: "badgeCount") } }) }() diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift index 250d016e..f5b2809a 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift @@ -7,10 +7,20 @@ import Foundation import UserNotifications -private enum KlaviyoBadgeConfig: String { - case incrementOne = "increment_one" - case setCount = "set_count" - case setProperty = "set_property" +private enum KlaviyoBadgeConfig { + case incrementOne + case setCount + case setProperty + case unknown(value: String) + + init(rawValue: String) { + switch rawValue { + case "increment_one": self = .incrementOne + case "set_count": self = .setCount + case "set_property": self = .setProperty + default: self = .unknown(value: rawValue) + } + } } public enum KlaviyoExtensionSDK { @@ -31,21 +41,24 @@ public enum KlaviyoExtensionSDK { contentHandler: @escaping (UNNotificationContent) -> Void, fallbackMediaType: String = "jpeg") { // handle badge setting from the push notification payload - if let badgeConfig = bestAttemptContent.userInfo["badge_config"] as? String { + if let badgeConfigValue = bestAttemptContent.userInfo["badge_config"] as? String { guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "klaviyo_app_group") as? String, let userDefaults = UserDefaults(suiteName: appGroup) else { return } var newBadgeValue: Int? + let badgeConfig = KlaviyoBadgeConfig(rawValue: badgeConfigValue) switch badgeConfig { - case KlaviyoBadgeConfig.incrementOne.rawValue: + case .incrementOne: let currentBadgeCount = userDefaults.integer(forKey: "badgeCount") newBadgeValue = currentBadgeCount + 1 - default: + case .setCount, .setProperty: if let badgeValue = bestAttemptContent.userInfo["badge_value"] as? Int { newBadgeValue = badgeValue } + case .unknown: + return } userDefaults.set(newBadgeValue, forKey: "badgeCount") From 4cc0bd28c0cea9d63266455387bd12f7473f14a3 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 20 Dec 2024 14:46:06 -0500 Subject: [PATCH 13/16] move setup instructions to installation section --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 986d8cca..665010ec 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ - [Collecting Push Tokens](#collecting-push-tokens) - [Request Push Notification Permission](#request-push-notification-permission) - [Badge Count](#badge-count) - - [Set Up](#set-up) - [Autoclearing Badge Count](#autoclearing-badge-count) - [Handling Other Badging Sources](#handling-other-badging-sources) - [Receiving Push Notifications](#receiving-push-notifications) @@ -47,7 +46,7 @@ Once integrated, your marketing team will be able to better understand your app ## Installation 1. Enable push notification capabilities in your Xcode project. The section "Enable the push notification capability" in this [Apple developer guide](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns#2980170) provides detailed instructions. -2. [Optional but recommended] If you intend to use [rich push notifications](#rich-push) or [custom badge counts](#custom-badge-count) add a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your Xcode project. A notification service app extension ships as a separate bundle inside your iOS app. To add this extension to your app: +2. If you intend to use [rich push notifications](#rich-push) or [custom badge counts](#custom-badge-count) add a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your Xcode project. A Notification Service Extension ships as a separate bundle inside your iOS app. To add this extension to your app: - Select File > New > Target in Xcode. - Select the Notification Service Extension target from the iOS > Application extension section. - Click Next. @@ -58,6 +57,15 @@ Once integrated, your marketing team will be able to better understand your app If this exceeds your app's minimum supported iOS version, push notifications may not display attached media on older devices. To avoid this, ensure the extension's minimum deployment target matches that of your app. ⚠️ + Set up an App Group between your main app target and your Notification Service Extension. + - Select your main app target > Signing & Capabilities + - Select + Capability (make sure it is set to All not Debug or Release) > App Groups + - Create a new App Group based on the recommended naming scheme `group.com.[MainTargetBundleId].[descriptor]` + - In your app's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name + - Select your Notification Service Extension target > Signing & Capabilities + - Add an App Group with the same name as the main target's App Group + - In your Notification Service Extension's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name + 3. Based on which dependency manager you use, follow the instructions below to install the Klaviyo's dependencies.
From e495bdfa9d81d2ace6ceb9780771b8bc5c344f1f Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 20 Dec 2024 14:51:47 -0500 Subject: [PATCH 14/16] autoclearing copy changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 665010ec..2df6a057 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ Klaviyo supports custom badge counts when configuring your push notification in #### Autoclearing Badge Count -By default, Klaviyo SDK automatically clears all badges on app open. If you want to disable this behavior, in your app's `Info.plist`, add a new entry for `klaviyo_badge_autoclearing` as a Boolean set to `NO`. You can turn this on again by setting this to `YES`. +By default, the Klaviyo SDK automatically clears the badge count on app open. If you want to disable this behavior, add a new entry for `klaviyo_badge_autoclearing` as a Boolean set to `NO` in your app's `Info.plist`. You can re-enable automatically clearing badges by setting this value to `YES`. #### Handling Other Badging Sources From 1f8c9c6cdfe84c065a58512206f1f7a8a5800ebb Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 20 Dec 2024 15:08:12 -0500 Subject: [PATCH 15/16] move remaining badge count section --- README.md | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 2df6a057..7ac9c8d2 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,15 @@ - [Prerequisites](#prerequisites) - [Collecting Push Tokens](#collecting-push-tokens) - [Request Push Notification Permission](#request-push-notification-permission) - - [Badge Count](#badge-count) - - [Autoclearing Badge Count](#autoclearing-badge-count) - - [Handling Other Badging Sources](#handling-other-badging-sources) - [Receiving Push Notifications](#receiving-push-notifications) - [Tracking Open Events](#tracking-open-events) - [Deep Linking](#deep-linking) - [Option 1: URL Schemes](#option-1-URL-schemes) - [Option 2: Universal Links](#option-2-universal-links) - [Rich Push](#rich-push) + - [Badge Count](#badge-count) + - [Autoclearing](#autoclearing) + - [Handling Other Badging Sources](#handling-other-badging-sources) - [Additional Details](#additional-details) - [Sandbox Support](#sandbox-support) - [SDK Data Transfer](#sdk-data-transfer) @@ -286,26 +286,6 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau return true } ``` -### Badge Count - -#### Set Up -Klaviyo supports custom badge counts when configuring your push notification in the editor dashboard. To set up your app, so the Klaviyo SDK properly handles them: - -(Pre-requisite: Set up the Notification Service Extension) -1. In XCode, select your main app target, then go to Signing & Capabilities -2. Add an App Groups capability and click the plus in the new section to add a new App Group -3. Pick a name based on the scheme `group.com.[MainTargetBundleId].[descriptor]` -4. Select your Service Extension target, and add the same App Group with the same name -5. In your app's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name -6. In your Notification Service Extension's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name - -#### Autoclearing Badge Count - -By default, the Klaviyo SDK automatically clears the badge count on app open. If you want to disable this behavior, add a new entry for `klaviyo_badge_autoclearing` as a Boolean set to `NO` in your app's `Info.plist`. You can re-enable automatically clearing badges by setting this value to `YES`. - -#### Handling Other Badging Sources - -Klaviyo SDK will automatically handle the badge count associated with Klaviyo pushes. If you need to manually update the badge count to account for other notification sources, use the `KlaviyoSDK().setBadgeCount(:)` method, which will update the badge count and keep it in sync with the Klaviyo SDK. This method should be used instead of (rather than in addition to) setting the badge count using `UNUserNotificationCenter` and/or `UIApplication` methods. ### Receiving Push Notifications @@ -513,6 +493,18 @@ project setup with the code from the `KlaviyoSwiftExtension`. Below are instruct Once you have these three things, you can then use the push notifications tester and send a local push notification to make sure that everything was set up correctly. +#### Badge Count + +Klaviyo supports setting or incrementing the badge count when you send a push notification. For this functionality to work, you must set up the Notification Service Extension and an App Group as outlined under the [Installation](#installation) section. + +##### Autoclearing + +By default, the Klaviyo SDK automatically clears the badge count on app open. If you want to disable this behavior, add a new entry for `klaviyo_badge_autoclearing` as a Boolean set to `NO` in your app's `Info.plist`. You can re-enable automatically clearing badges by setting this value to `YES`. + +##### Handling Other Badging Sources + +Klaviyo SDK will automatically handle the badge count associated with Klaviyo pushes. If you need to manually update the badge count to account for other notification sources, use the `KlaviyoSDK().setBadgeCount(:)` method, which will update the badge count and keep it in sync with the Klaviyo SDK. This method should be used instead of (rather than in addition to) setting the badge count using `UNUserNotificationCenter` and/or `UIApplication` methods. + ## Additional Details ### Sandbox Support From 2c6719e2b23cdc4691d0194fd9b37b980eefbb9c Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Fri, 20 Dec 2024 17:11:09 -0500 Subject: [PATCH 16/16] revert README changes as separate PR --- README.md | 118 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 7ac9c8d2..74027d45 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,6 @@ - [Option 1: URL Schemes](#option-1-URL-schemes) - [Option 2: Universal Links](#option-2-universal-links) - [Rich Push](#rich-push) - - [Badge Count](#badge-count) - - [Autoclearing](#autoclearing) - - [Handling Other Badging Sources](#handling-other-badging-sources) - [Additional Details](#additional-details) - [Sandbox Support](#sandbox-support) - [SDK Data Transfer](#sdk-data-transfer) @@ -46,7 +43,7 @@ Once integrated, your marketing team will be able to better understand your app ## Installation 1. Enable push notification capabilities in your Xcode project. The section "Enable the push notification capability" in this [Apple developer guide](https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns#2980170) provides detailed instructions. -2. If you intend to use [rich push notifications](#rich-push) or [custom badge counts](#custom-badge-count) add a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your Xcode project. A Notification Service Extension ships as a separate bundle inside your iOS app. To add this extension to your app: +2. [Optional but recommended] If you intend to use [rich push notifications](#rich-push) add a [Notification service extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your xcode project. A notification service app extension ships as a separate bundle inside your iOS app. To add this extension to your app: - Select File > New > Target in Xcode. - Select the Notification Service Extension target from the iOS > Application extension section. - Click Next. @@ -57,15 +54,6 @@ Once integrated, your marketing team will be able to better understand your app If this exceeds your app's minimum supported iOS version, push notifications may not display attached media on older devices. To avoid this, ensure the extension's minimum deployment target matches that of your app. ⚠️ - Set up an App Group between your main app target and your Notification Service Extension. - - Select your main app target > Signing & Capabilities - - Select + Capability (make sure it is set to All not Debug or Release) > App Groups - - Create a new App Group based on the recommended naming scheme `group.com.[MainTargetBundleId].[descriptor]` - - In your app's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name - - Select your Notification Service Extension target > Signing & Capabilities - - Add an App Group with the same name as the main target's App Group - - In your Notification Service Extension's `Info.plist`, add a new entry for `klaviyo_app_group` as a String with the App Group name - 3. Based on which dependency manager you use, follow the instructions below to install the Klaviyo's dependencies.
@@ -103,7 +91,7 @@ Once integrated, your marketing team will be able to better understand your app
4. Finally, in the `NotificationService.swift` file add the code for the two required delegates from [this](Examples/KlaviyoSwiftExamples/SPMExample/NotificationServiceExtension/NotificationService.swift) file. - This sample covers calling into Klaviyo so that we can download and attach the media to the push notification as well as handle custom badge counts. + This sample covers calling into Klaviyo so that we can download and attach the media to the push notification. ## Initialization The SDK must be initialized with the short alphanumeric [public API key](https://help.klaviyo.com/hc/en-us/articles/115005062267#difference-between-public-and-private-api-keys1) @@ -264,24 +252,24 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau UIApplication.shared.registerForRemoteNotifications() let center = UNUserNotificationCenter.current() - center.delegate = self as? UNUserNotificationCenterDelegate // the type casting can be removed once the delegate has been implemented - let options: UNAuthorizationOptions = [.alert, .sound, .badge] - // use the below options if you are interested in using provisional push notifications. Note that using this will not - // show the push notifications prompt to the user. - // let options: UNAuthorizationOptions = [.alert, .sound, .badge, .provisional] - center.requestAuthorization(options: options) { granted, error in - if let error = error { - // Handle the error here. - print("error = ", error) - } - - // Irrespective of the authorization status call `registerForRemoteNotifications` here so that - // the `didRegisterForRemoteNotificationsWithDeviceToken` delegate is called. Doing this - // will make sure that Klaviyo always has the latest push authorization status. + center.delegate = self as? UNUserNotificationCenterDelegate // the type casting can be removed once the delegate has been implemented + let options: UNAuthorizationOptions = [.alert, .sound, .badge] + // use the below options if you are interested in using provisional push notifications. Note that using this will not + // show the push notifications prompt to the user. + // let options: UNAuthorizationOptions = [.alert, .sound, .badge, .provisional] + center.requestAuthorization(options: options) { granted, error in + if let error = error { + // Handle the error here. + print("error = ", error) + } + + // Irrespective of the authorization status call `registerForRemoteNotifications` here so that + // the `didRegisterForRemoteNotificationsWithDeviceToken` delegate is called. Doing this + // will make sure that Klaviyo always has the latest push authorization status. DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } - } + } return true } @@ -325,6 +313,44 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } } ``` +When tracking opened push notification, you can also decrement the badge count on the app icon by adding the following code to the `userNotificationCenter:didReceive:withCompletionHandler` method: + +```swift + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + // decrement the badge count on the app icon + if #available(iOS 16.0, *) { + UNUserNotificationCenter.current().setBadgeCount(UIApplication.shared.applicationIconBadgeNumber - 1) + } else { + UIApplication.shared.applicationIconBadgeNumber -= 1 + } + + // If this notification is Klaviyo's notification we'll handle it + // else pass it on to the next push notification service to which it may belong + let handled = KlaviyoSDK().handle(notificationResponse: response, withCompletionHandler: completionHandler) + if !handled { + completionHandler() + } + } +``` + +Additionally, if you just want to reset the badge count to zero when the app is opened(note that this could be from +the user just opening the app independent of the push message), you can add the following code to +the `applicationDidBecomeActive` method in the app delegate: + +```swift + +func applicationDidBecomeActive(_ application: UIApplication) { + // reset the badge count on the app icon + if #available(iOS 16.0, *) { + UNUserNotificationCenter.current().setBadgeCount(0) + } else { + UIApplication.shared.applicationIconBadgeNumber = 0 + } +} +``` Once your first push notifications are sent and opened, you should start to see _Opened Push_ metrics within your Klaviyo dashboard. @@ -361,16 +387,16 @@ In order to edit the Info.plist directly, just fill in your app specific details ```xml CFBundleURLTypes - - CFBundleTypeRole - Editor - CFBundleURLName - {your_unique_identifier} - CFBundleURLSchemes - - {your_URL_scheme} - - + + CFBundleTypeRole + Editor + CFBundleURLName + {your_unique_identifier} + CFBundleURLSchemes + + {your_URL_scheme} + + ``` @@ -383,7 +409,7 @@ This needs to be done in the Info.plist directly: ```xml LSApplicationQueriesSchemes - {your custom URL scheme} + {your custom URL scheme} ``` @@ -493,18 +519,6 @@ project setup with the code from the `KlaviyoSwiftExtension`. Below are instruct Once you have these three things, you can then use the push notifications tester and send a local push notification to make sure that everything was set up correctly. -#### Badge Count - -Klaviyo supports setting or incrementing the badge count when you send a push notification. For this functionality to work, you must set up the Notification Service Extension and an App Group as outlined under the [Installation](#installation) section. - -##### Autoclearing - -By default, the Klaviyo SDK automatically clears the badge count on app open. If you want to disable this behavior, add a new entry for `klaviyo_badge_autoclearing` as a Boolean set to `NO` in your app's `Info.plist`. You can re-enable automatically clearing badges by setting this value to `YES`. - -##### Handling Other Badging Sources - -Klaviyo SDK will automatically handle the badge count associated with Klaviyo pushes. If you need to manually update the badge count to account for other notification sources, use the `KlaviyoSDK().setBadgeCount(:)` method, which will update the badge count and keep it in sync with the Klaviyo SDK. This method should be used instead of (rather than in addition to) setting the badge count using `UNUserNotificationCenter` and/or `UIApplication` methods. - ## Additional Details ### Sandbox Support