Skip to content
Draft
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: 2 additions & 2 deletions .github/workflows/cocoapods-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# This workflow publishes all five podspecs in dependency order:
# 1. KlaviyoCore (foundation, no dependencies)
# 2. KlaviyoSwiftExtension (depends on KlaviyoCore)
# 2. KlaviyoSwiftExtension (standalone, no dependencies)
# 3. KlaviyoSwift (depends on KlaviyoCore)
# 4. KlaviyoForms (depends on KlaviyoSwift)
# 5. KlaviyoLocation (depends on KlaviyoSwift)
Expand Down Expand Up @@ -43,7 +43,7 @@ jobs:
run: |
echo "Validating all podspecs in dependency order..."
pod lib lint KlaviyoCore.podspec --allow-warnings
pod lib lint KlaviyoSwiftExtension.podspec --allow-warnings --include-podspecs='*.podspec'
pod lib lint KlaviyoSwiftExtension.podspec --allow-warnings
pod lib lint KlaviyoSwift.podspec --allow-warnings --include-podspecs='*.podspec'
pod lib lint KlaviyoForms.podspec --allow-warnings --include-podspecs='*.podspec'
pod lib lint KlaviyoLocation.podspec --allow-warnings --include-podspecs='*.podspec'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.cocoapods.example.NotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -508,7 +508,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.cocoapods.example.NotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -654,7 +654,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.cocoapods.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -687,7 +687,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.cocoapods.example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -705,7 +705,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = G3793W2RJ2;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.cocoapods.example.CocoapodsExampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
Expand All @@ -723,7 +723,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = G3793W2RJ2;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.cocoapods.example.CocoapodsExampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
Expand Down
8 changes: 4 additions & 4 deletions Examples/KlaviyoSwiftExamples/CocoapodsExample/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ platform :ios, '13.0'
use_frameworks!

target 'CocoapodsExample' do
pod 'KlaviyoSwift', '5.3.0'
pod 'KlaviyoForms', '5.3.0'
pod 'KlaviyoLocation', '5.3.0'
pod 'KlaviyoSwift', '5.3.1'
pod 'KlaviyoForms', '5.3.1'
pod 'KlaviyoLocation', '5.3.1'
end

target 'NotificationServiceExtension' do
pod 'KlaviyoSwiftExtension', '5.3.0'
pod 'KlaviyoSwiftExtension', '5.3.1'
end
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExample.NotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -458,7 +458,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExample.NotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -609,7 +609,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -641,7 +641,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -659,7 +659,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
Expand All @@ -677,7 +677,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
Expand All @@ -693,7 +693,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
Expand All @@ -709,7 +709,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 5.3.0;
MARKETING_VERSION = 5.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.klaviyo.spm.example.SPMExampleUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
Expand Down
2 changes: 1 addition & 1 deletion KlaviyoCore.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "KlaviyoCore"
s.version = "5.3.0"
s.version = "5.3.1"
Comment thread
belleklaviyo marked this conversation as resolved.
s.summary = "Core functionalities for the Klaviyo SDK"
s.description = <<-DESC
Core functionalities and utilities for the Klaviyo SDK.
Expand Down
4 changes: 2 additions & 2 deletions KlaviyoForms.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "KlaviyoForms"
s.version = "5.3.0"
s.version = "5.3.1"
s.summary = "Klaviyo forms is a new way to engage with your app users"
s.description = <<-DESC
Use Klaviyo forms to include in app forms in your app and engage user with marketing content
Expand All @@ -19,5 +19,5 @@ Pod::Spec.new do |s|
]
}
s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name KlaviyoSwift -package-name KlaviyoCore' }
s.dependency 'KlaviyoSwift', '~> 5.3.0'
s.dependency 'KlaviyoSwift', '~> 5.3.1'
end
4 changes: 2 additions & 2 deletions KlaviyoLocation.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "KlaviyoLocation"
s.version = "5.3.0"
s.version = "5.3.1"
s.summary = "Location services and geofencing for the Klaviyo SDK"
s.description = <<-DESC
Use KlaviyoLocation to enable location-based tracking and geofencing capabilities in your iOS applications.
Expand All @@ -14,6 +14,6 @@ Pod::Spec.new do |s|
s.platform = :ios, '13.0'
s.source_files = 'Sources/KlaviyoLocation/**/*.swift'
s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name KlaviyoLocation -package-name KlaviyoSwift -package-name KlaviyoCore' }
s.dependency 'KlaviyoSwift', '~> 5.3.0'
s.dependency 'KlaviyoSwift', '~> 5.3.1'
s.frameworks = 'CoreLocation'
end
4 changes: 2 additions & 2 deletions KlaviyoSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "KlaviyoSwift"
s.version = "5.3.0"
s.version = "5.3.1"
Comment thread
cursor[bot] marked this conversation as resolved.
s.summary = "Incorporate Klaviyo's event and person tracking and push notifications functionality into iOS applications"

s.description = <<-DESC
Expand All @@ -17,6 +17,6 @@ Pod::Spec.new do |s|
s.source_files = 'Sources/KlaviyoSwift/**/*.swift'
s.resource_bundles = {"KlaviyoSwift" => ["Sources/KlaviyoSwift/PrivacyInfo.xcprivacy"]}
s.pod_target_xcconfig = { 'OTHER_SWIFT_FLAGS' => '-package-name KlaviyoSwift -package-name KlaviyoCore' }
s.dependency 'KlaviyoCore', '~> 5.3.0'
s.dependency 'KlaviyoCore', '~> 5.3.1'
s.dependency 'AnyCodable-FlightSchool'
end
3 changes: 1 addition & 2 deletions KlaviyoSwiftExtension.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "KlaviyoSwiftExtension"
s.version = "5.3.0"
s.version = "5.3.1"
s.summary = "Incorporate Klaviyo's rich push notifications functionality into your iOS applications"

s.description = <<-DESC
Expand All @@ -15,5 +15,4 @@ Pod::Spec.new do |s|
s.platform = :ios
s.ios.deployment_target = '13.0'
s.source_files = 'Sources/KlaviyoSwiftExtension/**/*.swift'
s.dependency 'KlaviyoCore', '~> 5.3.0'
end
4 changes: 4 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

This guide outlines how developers can migrate from older versions of our SDK to newer ones.

## Migrating to v5.3.1

> **Note:** v5.3.0 introduced a dependency on `KlaviyoCore` in `KlaviyoSwiftExtension` that is incompatible with Notification Service Extension targets. If your NSE target has `APPLICATION_EXTENSION_API_ONLY = YES`, skip v5.3.0 and use v5.3.1 or later.

## Migrating to v5.1.0

### Deep Linking
Expand Down
5 changes: 2 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,13 @@ let package = Package(
),
.target(
name: "KlaviyoSwiftExtension",
dependencies: ["KlaviyoCore"],
dependencies: [],
path: "Sources/KlaviyoSwiftExtension"
),
.testTarget(
name: "KlaviyoSwiftExtensionTests",
dependencies: [
"KlaviyoSwiftExtension",
"KlaviyoCore"
"KlaviyoSwiftExtension"
]
),
.target(
Expand Down
95 changes: 12 additions & 83 deletions Sources/KlaviyoCore/KlaviyoCategoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,34 @@
// Created by Belle Lim on 1/20/26.
//

// NOTE: pruneCategory is intentionally not duplicated in KlaviyoSwiftExtension.
// That target carries a separate register-only KlaviyoCategoryManager
// (Sources/KlaviyoSwiftExtension/KlaviyoCategoryManager.swift) because it cannot
// depend on KlaviyoCore (NSE/share-extension sandbox restriction). Pruning is
// only needed in the main app (KlaviyoSwift) and lives exclusively here.

import Foundation
import OSLog
import UserNotifications

/// Manages the registration of the Klaviyo action button notification category.
/// Manages pruning of stale Klaviyo notification categories in the main app.
///
/// This manager handles:
/// - Registering unique categories per notification with dynamic actions
/// - Preserving existing categories (including developer-set ones) when adding new categories
/// Registration is handled by KlaviyoSwiftExtension's copy of this class (NSE context).
/// This class is responsible only for pruning stale categories after a notification is
/// opened or dismissed from the main app.
public class KlaviyoCategoryManager {
public static let shared = KlaviyoCategoryManager()

/// Prefix used for all Klaviyo notification category identifiers
public static let categoryIdentifierPrefix = "com.klaviyo.button."

/// Serial queue to ensure thread-safe category registration
/// Serial queue to ensure thread-safe category updates
private let queue = DispatchQueue(label: "com.klaviyo.category.registration", qos: .userInitiated)

private init() {}

// MARK: - Public Methods

/// Registers a Klaviyo action button notification category with the given actions.
///
/// Each notification should use a unique category identifier to prevent race conditions
/// where multiple notifications with different buttons overwrite each other's category.
/// This is a risk when either multiple notifications arrive simultaneously or multiple
/// notifications with action buttons sit in the Notification Center and are opened later.
///
/// This method:
/// 1. Creates a UNNotificationCategory with the provided actions and identifier
/// 2. Merges with existing categories, preserving all other registered categories
///
/// - Parameters:
/// - categoryIdentifier: Unique identifier for this notification's category
/// - actions: Array of notification actions to include in the category
public func registerCategory(categoryIdentifier: String, actions: [UNNotificationAction]) {
// Use serial queue to ensure thread-safe registration when multiple notifications arrive simultaneously
queue.sync {
// Create the category
let category = UNNotificationCategory(
identifier: categoryIdentifier,
actions: actions,
intentIdentifiers: [],
options: .customDismissAction
)

// Get existing categories
let (existingCategories, fetchTimedOut) = fetchExistingCategories()

// If fetch timed out, proceed with empty set to avoid blocking
// The category will still be registered, but we won't preserve existing ones
// This is acceptable since NSE has tight time constraints
let mergedCategories: Set<UNNotificationCategory>
if fetchTimedOut {
// If we timed out, just register the new category
// This is a trade-off: we might lose some existing categories, but we avoid blocking
if #available(iOS 14.0, *) {
Logger.notifications.warning("Could not retrieve existing categories. Prioritizing and setting the incoming category. Existing categories may be lost.")
}
mergedCategories = [category]
} else {
// Merge categories normally
mergedCategories = self.mergeCategories(existing: existingCategories, new: category)
}

// Register the merged set
if #available(iOS 14.0, *) {
Logger.notifications.warning("Registered new notification category '\(categoryIdentifier)'. Total categories: \(mergedCategories.count)")
}
UNUserNotificationCenter.current().setNotificationCategories(mergedCategories)
}
}

/// Removes a notification category from the registered categories and prunes any other stale categories.
///
/// This method:
Expand Down Expand Up @@ -153,7 +107,7 @@ public class KlaviyoCategoryManager {
semaphore.signal()
}

// Wait for categories to be fetched (NSE has tight time constraints)
// Wait for categories to be fetched
let result = semaphore.wait(timeout: .now() + 1.0)
if result == .timedOut {
fetchTimedOut = true
Expand Down Expand Up @@ -182,29 +136,4 @@ public class KlaviyoCategoryManager {

return (deliveredNotifications, fetchTimedOut)
}

/// Merges a new category with existing categories.
///
/// This ensures that when we call `setNotificationCategories()`, we don't remove
/// categories that developers have already registered. We only update/replace
/// categories with the same identifier as the new one.
///
/// - Parameters:
/// - existing: Set of currently registered categories
/// - new: New category to add or update
/// - Returns: Merged set of categories with the new category added/updated
private func mergeCategories(
existing: Set<UNNotificationCategory>,
new: UNNotificationCategory
) -> Set<UNNotificationCategory> {
var merged = existing

// Remove any existing category with the same ID (update case)
merged = merged.filter { $0.identifier != new.identifier }

// Add the new category
merged.insert(new)

return merged
}
}
Loading
Loading