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

CHNL-14775: Add badge incrementing logic to extension #244

Merged
merged 21 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
aad6d2c
Handle new push payloads in extension and add sync events
belleklaviyo Dec 13, 2024
a21de67
add README changes
belleklaviyo Dec 16, 2024
4b5ce4d
Add extra setup instructions for extension
belleklaviyo Dec 17, 2024
feffbb7
change default to autoclear and add disabling instructions
belleklaviyo Dec 18, 2024
fab95ea
flip fallback value
belleklaviyo Dec 18, 2024
a5963ac
flip logic back for clarity
belleklaviyo Dec 18, 2024
06c945a
add badge_value check to default enum case
belleklaviyo Dec 18, 2024
a3e2166
Merge branch 'master' into bl/badge_count_support
belleklaviyo Dec 18, 2024
14c4638
make syncBadgeCount a KlaviyoAction
belleklaviyo Dec 18, 2024
8659c9b
add syncBadgeCount to tests
belleklaviyo Dec 19, 2024
b7c0c2a
fix failing test
belleklaviyo Dec 19, 2024
9a6c182
cleanup on logic and naming
belleklaviyo Dec 20, 2024
ae4689c
do not handle any unknown future config cases
belleklaviyo Dec 20, 2024
4cc0bd2
move setup instructions to installation section
belleklaviyo Dec 20, 2024
e495bdf
autoclearing copy changes
belleklaviyo Dec 20, 2024
1f8c9c6
move remaining badge count section
belleklaviyo Dec 20, 2024
2c6719e
revert README changes as separate PR
belleklaviyo Dec 20, 2024
ef72845
Merge branch 'master' into bl/badge_count_support
belleklaviyo Jan 2, 2025
b866bee
remove all changes to README
belleklaviyo Jan 2, 2025
ddafda6
switch guard to if let to still execute rich media setup
belleklaviyo Jan 3, 2025
4e9254f
separate out rich media setup and badge setup
belleklaviyo Jan 3, 2025
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
66 changes: 26 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -43,7 +47,7 @@ Once integrated, your marketing team will be able to better understand your app
## Installation
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down Expand Up @@ -91,7 +95,7 @@ Once integrated, your marketing team will be able to better understand your app
</details>

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)
Expand Down Expand Up @@ -274,6 +278,26 @@ 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:
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved

(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
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved
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
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved

#### Autoclearing Badge Count
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved

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`.
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved

#### 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.
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved

### Receiving Push Notifications

Expand Down Expand Up @@ -313,44 +337,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.

Expand Down
10 changes: 5 additions & 5 deletions Sources/KlaviyoCore/KlaviyoEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public struct KlaviyoEnvironment {
notificationCenterPublisher: @escaping (NSNotification.Name) -> AnyPublisher<Notification, Never>,
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?,
Expand All @@ -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
Expand Down Expand Up @@ -96,7 +96,7 @@ public struct KlaviyoEnvironment {
public var notificationCenterPublisher: (NSNotification.Name) -> AnyPublisher<Notification, Never>
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
Expand Down Expand Up @@ -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 ?? false
},
startReachability: {
try reachabilityService?.startNotifier()
Expand Down
1 change: 1 addition & 0 deletions Sources/KlaviyoSwift/Klaviyo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved
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))
Expand Down
17 changes: 15 additions & 2 deletions Sources/KlaviyoSwift/StateManagement/StateManagement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -303,8 +304,10 @@ struct KlaviyoReducer: ReducerProtocol {
.run { send in
let settings = await environment.getNotificationSettings()
await send(KlaviyoAction.setPushEnablement(settings))
let autoclearing = await environment.getBadgeAutoClearingSetting()
if autoclearing {
let disabled = await environment.getBadgeAutoClearingIsDisabled()
if disabled {
KlaviyoBadgeCountUtil.syncBadgeCount()
} else {
await send(KlaviyoAction.setBadgeCount(0))
}
},
Expand Down Expand Up @@ -585,6 +588,16 @@ extension Store where State == KlaviyoState, Action == KlaviyoAction {
reducer: KlaviyoReducer())
}

enum KlaviyoBadgeCountUtil {
static func syncBadgeCount() {
DispatchQueue.main.async {
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down
29 changes: 28 additions & 1 deletion Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -24,6 +30,26 @@ public enum KlaviyoExtensionSDK {
bestAttemptContent: UNMutableNotificationContent,
contentHandler: @escaping (UNNotificationContent) -> Void,
fallbackMediaType: String = "jpeg") {
// handle badge setting from the push notification payload
belleklaviyo marked this conversation as resolved.
Show resolved Hide resolved
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")
}
bestAttemptContent.badge = (badgeValue as NSNumber)
}
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)
Expand Down Expand Up @@ -122,7 +148,8 @@ public enum KlaviyoExtensionSDK {
guard let attachment = try? UNNotificationAttachment(
identifier: "",
url: localFileURLWithType,
options: nil) else {
options: nil)
else {
completion(nil)
return
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/KlaviyoCoreTests/TestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ extension KlaviyoEnvironment {
notificationCenterPublisher: { _ in Empty<Notification, Never>().eraseToAnyPublisher() },
getNotificationSettings: { .authorized },
getBackgroundSetting: { .available },
getBadgeAutoClearingSetting: { true },
getBadgeAutoClearingIsDisabled: { false },
startReachability: {},
stopReachability: {},
reachabilityStatus: { nil },
Expand Down
2 changes: 1 addition & 1 deletion Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension KlaviyoEnvironment {
notificationCenterPublisher: { _ in Empty<Notification, Never>().eraseToAnyPublisher() },
getNotificationSettings: { .authorized },
getBackgroundSetting: { .available },
getBadgeAutoClearingSetting: { true },
getBadgeAutoClearingIsDisabled: { false },
startReachability: {},
stopReachability: {},
reachabilityStatus: { nil },
Expand Down
4 changes: 2 additions & 2 deletions Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Loading