From 32935f1cc2db031eb8a2fec7ca46a202614d6922 Mon Sep 17 00:00:00 2001 From: Isobelle Lim Date: Wed, 29 Apr 2026 12:11:56 -0400 Subject: [PATCH 1/3] chore: bump SDK version to 5.3.1 Co-Authored-By: Claude Sonnet 4.6 --- .../CocoapodsExample.xcodeproj/project.pbxproj | 12 ++++++------ .../CocoapodsExample/Podfile | 8 ++++---- .../SPMExample.xcodeproj/project.pbxproj | 16 ++++++++-------- KlaviyoCore.podspec | 2 +- KlaviyoForms.podspec | 4 ++-- KlaviyoLocation.podspec | 4 ++-- KlaviyoSwift.podspec | 4 ++-- KlaviyoSwiftExtension.podspec | 4 ++-- Sources/KlaviyoCore/Utils/Version.swift | 2 +- Tests/KlaviyoCoreTests/NetworkSessionTests.swift | 6 +++--- .../EncodableTests/testEventPayload.1.json | 4 ++-- .../EncodableTests/testKlaviyoRequest.1.json | 4 ++-- .../EncodableTests/testTokenPayload.1.json | 2 +- .../testCreateEmphemeralSesionHeaders.1.txt | 2 +- .../testDefaultUserAgent.1.txt | 2 +- .../EncodableTests/testKlaviyoState.1.json | 6 +++--- .../testValidStateFileExists.1.txt | 2 +- 17 files changed, 42 insertions(+), 42 deletions(-) diff --git a/Examples/KlaviyoSwiftExamples/CocoapodsExample/CocoapodsExample.xcodeproj/project.pbxproj b/Examples/KlaviyoSwiftExamples/CocoapodsExample/CocoapodsExample.xcodeproj/project.pbxproj index ff8cd397..bff52d30 100644 --- a/Examples/KlaviyoSwiftExamples/CocoapodsExample/CocoapodsExample.xcodeproj/project.pbxproj +++ b/Examples/KlaviyoSwiftExamples/CocoapodsExample/CocoapodsExample.xcodeproj/project.pbxproj @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/Examples/KlaviyoSwiftExamples/CocoapodsExample/Podfile b/Examples/KlaviyoSwiftExamples/CocoapodsExample/Podfile index c0ed03e3..c99f2829 100644 --- a/Examples/KlaviyoSwiftExamples/CocoapodsExample/Podfile +++ b/Examples/KlaviyoSwiftExamples/CocoapodsExample/Podfile @@ -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 diff --git a/Examples/KlaviyoSwiftExamples/SPMExample/SPMExample.xcodeproj/project.pbxproj b/Examples/KlaviyoSwiftExamples/SPMExample/SPMExample.xcodeproj/project.pbxproj index c61b12c5..6413682c 100644 --- a/Examples/KlaviyoSwiftExamples/SPMExample/SPMExample.xcodeproj/project.pbxproj +++ b/Examples/KlaviyoSwiftExamples/SPMExample/SPMExample.xcodeproj/project.pbxproj @@ -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; @@ -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; @@ -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 = ""; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/KlaviyoCore.podspec b/KlaviyoCore.podspec index 84f4360f..74cb0c97 100644 --- a/KlaviyoCore.podspec +++ b/KlaviyoCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "KlaviyoCore" - s.version = "5.3.0" + s.version = "5.3.1" s.summary = "Core functionalities for the Klaviyo SDK" s.description = <<-DESC Core functionalities and utilities for the Klaviyo SDK. diff --git a/KlaviyoForms.podspec b/KlaviyoForms.podspec index b89ed6da..ed5b7118 100644 --- a/KlaviyoForms.podspec +++ b/KlaviyoForms.podspec @@ -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 @@ -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 diff --git a/KlaviyoLocation.podspec b/KlaviyoLocation.podspec index c646056f..13df3ec0 100644 --- a/KlaviyoLocation.podspec +++ b/KlaviyoLocation.podspec @@ -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. @@ -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 diff --git a/KlaviyoSwift.podspec b/KlaviyoSwift.podspec index 8c627e23..3a22c0a7 100644 --- a/KlaviyoSwift.podspec +++ b/KlaviyoSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "KlaviyoSwift" - s.version = "5.3.0" + s.version = "5.3.1" s.summary = "Incorporate Klaviyo's event and person tracking and push notifications functionality into iOS applications" s.description = <<-DESC @@ -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 diff --git a/KlaviyoSwiftExtension.podspec b/KlaviyoSwiftExtension.podspec index ed92381c..890a88c7 100644 --- a/KlaviyoSwiftExtension.podspec +++ b/KlaviyoSwiftExtension.podspec @@ -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 @@ -15,5 +15,5 @@ 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' + s.dependency 'KlaviyoCore', '~> 5.3.1' end diff --git a/Sources/KlaviyoCore/Utils/Version.swift b/Sources/KlaviyoCore/Utils/Version.swift index 5e2635f7..a95984eb 100644 --- a/Sources/KlaviyoCore/Utils/Version.swift +++ b/Sources/KlaviyoCore/Utils/Version.swift @@ -8,4 +8,4 @@ import Foundation public let __klaviyoSwiftName = "swift" -public let __klaviyoSwiftVersion = "5.3.0" +public let __klaviyoSwiftVersion = "5.3.1" diff --git a/Tests/KlaviyoCoreTests/NetworkSessionTests.swift b/Tests/KlaviyoCoreTests/NetworkSessionTests.swift index 0d6cd06a..159e16a7 100644 --- a/Tests/KlaviyoCoreTests/NetworkSessionTests.swift +++ b/Tests/KlaviyoCoreTests/NetworkSessionTests.swift @@ -61,7 +61,7 @@ class NetworkSessionTests: XCTestCase { // Verify the result XCTAssertNotNil(result) - XCTAssertEqual(result, "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.0 (test-plugin/1.0.0)") + XCTAssertEqual(result, "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.1 (test-plugin/1.0.0)") // Clean up try FileManager.default.removeItem(at: plistURL) @@ -77,7 +77,7 @@ class NetworkSessionTests: XCTestCase { // Call the function with our mock bundle let result = NetworkSession.defaultUserAgent(bundle: mockBundle) - XCTAssertEqual(result, "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.0") + XCTAssertEqual(result, "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.1") } func testGetPluginConfigurationWithInvalidPlist() { @@ -97,7 +97,7 @@ class NetworkSessionTests: XCTestCase { let result = NetworkSession.defaultUserAgent(bundle: mockBundle) // Verify the result is default - XCTAssertEqual(result, "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.0") + XCTAssertEqual(result, "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.1") // Clean up try FileManager.default.removeItem(at: plistURL) diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json index ea694ced..7688ba94 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json +++ b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json @@ -36,7 +36,7 @@ "OS Version" : "1.1.1", "Push Token" : "", "SDK Name" : "swift", - "SDK Version" : "5.3.0", + "SDK Version" : "5.3.1", "Stuff" : 2 }, "time" : "2009-02-13T23:31:30Z", @@ -44,4 +44,4 @@ }, "type" : "event" } -} \ No newline at end of file +} diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json index 43e97f66..45b8d42f 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json +++ b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json @@ -18,7 +18,7 @@ "manufacturer" : "Orange", "os_name" : "iOS", "os_version" : "1.1.1", - "sdk_version" : "5.3.0" + "sdk_version" : "5.3.1" }, "enablement_status" : "AUTHORIZED", "platform" : "ios", @@ -44,4 +44,4 @@ } }, "id" : "00000000-0000-0000-0000-000000000001" -} \ No newline at end of file +} diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testTokenPayload.1.json b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testTokenPayload.1.json index 847be233..4ccf9c6b 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testTokenPayload.1.json +++ b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testTokenPayload.1.json @@ -14,7 +14,7 @@ "manufacturer" : "Orange", "os_name" : "iOS", "os_version" : "1.1.1", - "sdk_version" : "5.3.0" + "sdk_version" : "5.3.1" }, "enablement_status" : "AUTHORIZED", "platform" : "ios", diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt b/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt index 7bd7d000..894bd397 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt +++ b/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt @@ -8,7 +8,7 @@ - "deflate" ▿ (2 elements) - key: "User-Agent" - - value: "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.0" + - value: "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.1" ▿ (2 elements) - key: "X-Klaviyo-Mobile" - value: "1" diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testDefaultUserAgent.1.txt b/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testDefaultUserAgent.1.txt index 7e057348..766480ce 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testDefaultUserAgent.1.txt +++ b/Tests/KlaviyoCoreTests/__Snapshots__/NetworkSessionTests/testDefaultUserAgent.1.txt @@ -1 +1 @@ -- "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.0" +- "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-swift/5.3.1" diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json b/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json index 87ecbaa9..503b4f4f 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json @@ -15,7 +15,7 @@ "manufacturer" : "Orange", "os_name" : "iOS", "os_version" : "1.1.1", - "sdk_version" : "5.3.0" + "sdk_version" : "5.3.1" }, "pushBackground" : "AVAILABLE", "pushEnablement" : "AUTHORIZED", @@ -42,7 +42,7 @@ "manufacturer" : "Orange", "os_name" : "iOS", "os_version" : "1.1.1", - "sdk_version" : "5.3.0" + "sdk_version" : "5.3.1" }, "enablement_status" : "AUTHORIZED", "platform" : "ios", @@ -70,4 +70,4 @@ "id" : "00000000-0000-0000-0000-000000000001" } ] -} \ No newline at end of file +} diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt index 878c7381..ee33c8b5 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt @@ -29,7 +29,7 @@ - manufacturer: "Orange" - osName: "iOS" - osVersion: "1.1.1" - - sdkVersion: "5.3.0" + - sdkVersion: "5.3.1" - pushBackground: PushBackground.available - pushEnablement: PushEnablement.authorized - pushToken: "blob_token" From 5e78a01ab214b6bdda659f015ed0981feb0621f7 Mon Sep 17 00:00:00 2001 From: Belle Lim Date: Wed, 29 Apr 2026 16:51:34 -0400 Subject: [PATCH 2/3] fix(extension): drop KlaviyoCore dependency by copying shared types (#564) * spike(extension): copy shared types into KlaviyoSwiftExtension, drop KlaviyoCore dep Copy ActionType, KlaviyoCategoryManager (register-only), Logger+Ext, and UNNotificationContent+Klaviyo directly into KlaviyoSwiftExtension so the extension target no longer needs KlaviyoCore. The NSE sandbox prevents sharing a framework across app and extension targets in some configurations. Remove KlaviyoCore from Package.swift (production + test targets) and from KlaviyoSwiftExtension.podspec. Co-Authored-By: Claude Sonnet 4.6 * spike(core): restore pruning in KlaviyoCore, annotate intentional duplication Restore KlaviyoCore/KlaviyoCategoryManager with prune-only functionality and re-wire pruneCategory through KlaviyoSwiftEnvironment + Klaviyo.swift. KlaviyoSwiftExtension retains its register-only copy; pruning is not duplicated. Add sync notes to both KlaviyoCategoryManager copies and both ActionType copies explaining the intentional duplication and the NSE sandbox reason for it. Co-Authored-By: Claude Sonnet 4.6 * fix(tests): stub KlaviyoSwiftEnvironment in setup, update snapshots setUpWithError was leaving klaviyoSwiftEnvironment at .production, causing handle(notificationResponse:) tests to hit the real UNUserNotificationCenter via the pruneCategory defer block. Stub it in setUp so tests are isolated. Update EncodableTest snapshots to reflect current state. Co-Authored-By: Claude Sonnet 4.6 * docs: note v5.3.0 KlaviyoSwiftExtension incompatibility with NSE targets Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- KlaviyoSwiftExtension.podspec | 1 - MIGRATION_GUIDE.md | 4 + Package.swift | 5 +- .../KlaviyoCore/KlaviyoCategoryManager.swift | 95 ++----------- Sources/KlaviyoCore/Models/ActionType.swift | 6 + .../KlaviyoSwiftExtension/ActionType.swift | 17 +++ .../KlaviyoActionButtonParser.swift | 1 - .../KlaviyoCategoryManager.swift | 129 ++++++++++++++++++ .../KlaviyoExtension.swift | 1 - .../KlaviyoSwiftExtension/Logger+Ext.swift | 2 + .../UNNotificationContent+Klaviyo.swift | 28 ++++ .../EncodableTests/testEventPayload.1.json | 2 +- .../EncodableTests/testKlaviyoRequest.1.json | 2 +- Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift | 2 +- Tests/KlaviyoSwiftTests/TestData.swift | 1 + .../EncodableTests/testKlaviyoState.1.json | 2 +- 16 files changed, 205 insertions(+), 93 deletions(-) create mode 100644 Sources/KlaviyoSwiftExtension/ActionType.swift create mode 100644 Sources/KlaviyoSwiftExtension/KlaviyoCategoryManager.swift create mode 100644 Sources/KlaviyoSwiftExtension/UNNotificationContent+Klaviyo.swift diff --git a/KlaviyoSwiftExtension.podspec b/KlaviyoSwiftExtension.podspec index 890a88c7..4d24ebf5 100644 --- a/KlaviyoSwiftExtension.podspec +++ b/KlaviyoSwiftExtension.podspec @@ -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.1' end diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 089735f8..d2aecd53 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -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 diff --git a/Package.swift b/Package.swift index 5a932e4c..a34b8fae 100644 --- a/Package.swift +++ b/Package.swift @@ -93,14 +93,13 @@ let package = Package( ), .target( name: "KlaviyoSwiftExtension", - dependencies: ["KlaviyoCore"], + dependencies: [], path: "Sources/KlaviyoSwiftExtension" ), .testTarget( name: "KlaviyoSwiftExtensionTests", dependencies: [ - "KlaviyoSwiftExtension", - "KlaviyoCore" + "KlaviyoSwiftExtension" ] ), .target( diff --git a/Sources/KlaviyoCore/KlaviyoCategoryManager.swift b/Sources/KlaviyoCore/KlaviyoCategoryManager.swift index 631d0d3d..15e594d1 100644 --- a/Sources/KlaviyoCore/KlaviyoCategoryManager.swift +++ b/Sources/KlaviyoCore/KlaviyoCategoryManager.swift @@ -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 - 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: @@ -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 @@ -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, - new: UNNotificationCategory - ) -> Set { - 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 - } } diff --git a/Sources/KlaviyoCore/Models/ActionType.swift b/Sources/KlaviyoCore/Models/ActionType.swift index 95359aaf..47581cee 100644 --- a/Sources/KlaviyoCore/Models/ActionType.swift +++ b/Sources/KlaviyoCore/Models/ActionType.swift @@ -5,6 +5,12 @@ // Created by Belle Lim on 1/20/26. // +// NOTE: KlaviyoSwiftExtension carries an internal copy of this enum +// (Sources/KlaviyoSwiftExtension/ActionType.swift) because that target cannot +// depend on KlaviyoCore (NSE/share-extension sandbox restriction). The two +// copies are intentionally kept in sync. If you add or rename cases here, +// mirror the change there. + import Foundation /// Represents the supported action types for push notification buttons. diff --git a/Sources/KlaviyoSwiftExtension/ActionType.swift b/Sources/KlaviyoSwiftExtension/ActionType.swift new file mode 100644 index 00000000..f62b4a33 --- /dev/null +++ b/Sources/KlaviyoSwiftExtension/ActionType.swift @@ -0,0 +1,17 @@ +// +// ActionType.swift +// + +// NOTE: KlaviyoCore carries the authoritative copy of this enum +// (Sources/KlaviyoCore/Models/ActionType.swift). This internal copy exists +// because KlaviyoSwiftExtension cannot depend on KlaviyoCore (NSE/share-extension +// sandbox restriction). The two copies are intentionally kept in sync. If you +// add or rename cases here, mirror the change there. + +import Foundation + +/// Represents the supported action types for push notification buttons. +enum ActionType: String, Equatable { + case openApp = "open_app" + case deepLink = "deep_link" +} diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoActionButtonParser.swift b/Sources/KlaviyoSwiftExtension/KlaviyoActionButtonParser.swift index 283dbaf4..4545b152 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoActionButtonParser.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoActionButtonParser.swift @@ -6,7 +6,6 @@ // import Foundation -import KlaviyoCore import OSLog import UserNotifications diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoCategoryManager.swift b/Sources/KlaviyoSwiftExtension/KlaviyoCategoryManager.swift new file mode 100644 index 00000000..a07c83a4 --- /dev/null +++ b/Sources/KlaviyoSwiftExtension/KlaviyoCategoryManager.swift @@ -0,0 +1,129 @@ +// +// KlaviyoCategoryManager.swift +// + +// NOTE: KlaviyoCore carries the authoritative copy of this class +// (Sources/KlaviyoCore/KlaviyoCategoryManager.swift), which also includes +// pruneCategory. This register-only copy exists because KlaviyoSwiftExtension +// cannot depend on KlaviyoCore (NSE/share-extension sandbox restriction). +// The two copies are intentionally kept in sync. If you change registerCategory, +// fetchExistingCategories, or mergeCategories here, mirror the changes there. + +import Foundation +import OSLog +import UserNotifications + +/// Manages the registration of the Klaviyo action button notification category. +/// +/// This manager handles: +/// - Registering unique categories per notification with dynamic actions +/// - Preserving existing categories (including developer-set ones) when adding new categories +class KlaviyoCategoryManager { + static let shared = KlaviyoCategoryManager() + + /// Prefix used for all Klaviyo notification category identifiers + static let categoryIdentifierPrefix = "com.klaviyo.button." + + /// Serial queue to ensure thread-safe category registration + 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 + 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 + 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) + } + } + + // MARK: - Private Methods + + /// Fetches existing notification categories with timeout handling. + /// + /// - Returns: A tuple containing the set of existing categories and a boolean indicating if the fetch timed out + private func fetchExistingCategories() -> (Set, Bool) { + let semaphore = DispatchSemaphore(value: 0) + var existingCategories: Set = [] + var fetchTimedOut = false + + UNUserNotificationCenter.current().getNotificationCategories { categories in + existingCategories = categories + semaphore.signal() + } + + // Wait for categories to be fetched (NSE has tight time constraints) + let result = semaphore.wait(timeout: .now() + 1.0) + if result == .timedOut { + fetchTimedOut = true + } + + return (existingCategories, 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, + new: UNNotificationCategory + ) -> Set { + var merged = existing + merged = merged.filter { $0.identifier != new.identifier } + merged.insert(new) + return merged + } +} diff --git a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift index e1397c1f..32a07768 100644 --- a/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift +++ b/Sources/KlaviyoSwiftExtension/KlaviyoExtension.swift @@ -6,7 +6,6 @@ // import Foundation -import KlaviyoCore import OSLog import UserNotifications diff --git a/Sources/KlaviyoSwiftExtension/Logger+Ext.swift b/Sources/KlaviyoSwiftExtension/Logger+Ext.swift index 9c7790e2..adc8b21f 100644 --- a/Sources/KlaviyoSwiftExtension/Logger+Ext.swift +++ b/Sources/KlaviyoSwiftExtension/Logger+Ext.swift @@ -24,4 +24,6 @@ extension Logger { static let actionButtons = Logger(category: "Action Buttons") /// Logger for Rich Media static let richMedia = Logger(category: "Rich Media") + /// Logger for notification category management + static let notifications = Logger(category: "Notifications") } diff --git a/Sources/KlaviyoSwiftExtension/UNNotificationContent+Klaviyo.swift b/Sources/KlaviyoSwiftExtension/UNNotificationContent+Klaviyo.swift new file mode 100644 index 00000000..6aa86f16 --- /dev/null +++ b/Sources/KlaviyoSwiftExtension/UNNotificationContent+Klaviyo.swift @@ -0,0 +1,28 @@ +// +// UNNotificationContent+Klaviyo.swift +// + +import Foundation +import UserNotifications + +extension Dictionary where Key == AnyHashable { + /// Determines if a notification payload originated from Klaviyo. + /// + /// A notification is considered a Klaviyo notification if it contains + /// a "body" dictionary with a "_k" key in its userInfo. + func isKlaviyoNotification() -> Bool { + guard let properties = self as? [String: Any], + let body = properties["body"] as? [String: Any], + body["_k"] != nil else { + return false + } + return true + } +} + +extension UNNotificationContent { + /// Determines if this notification content originated from Klaviyo. + var isKlaviyoNotification: Bool { + userInfo.isKlaviyoNotification() + } +} diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json index 7688ba94..03871469 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json +++ b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testEventPayload.1.json @@ -44,4 +44,4 @@ }, "type" : "event" } -} +} \ No newline at end of file diff --git a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json index 45b8d42f..b57c2b91 100644 --- a/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json +++ b/Tests/KlaviyoCoreTests/__Snapshots__/EncodableTests/testKlaviyoRequest.1.json @@ -44,4 +44,4 @@ } }, "id" : "00000000-0000-0000-0000-000000000001" -} +} \ No newline at end of file diff --git a/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift b/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift index 9ea46cab..9c5dcd3f 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoSDKTests.swift @@ -22,7 +22,7 @@ class KlaviyoSDKTests: XCTestCase { override func setUpWithError() throws { klaviyo = KlaviyoSDK() environment = KlaviyoEnvironment.test() - klaviyoSwiftEnvironment.pruneCategory = { _ in } + klaviyoSwiftEnvironment = KlaviyoSwiftEnvironment.test() } override func tearDown() async throws { diff --git a/Tests/KlaviyoSwiftTests/TestData.swift b/Tests/KlaviyoSwiftTests/TestData.swift index a21f773e..d1a88081 100644 --- a/Tests/KlaviyoSwiftTests/TestData.swift +++ b/Tests/KlaviyoSwiftTests/TestData.swift @@ -225,6 +225,7 @@ extension KlaviyoSwiftEnvironment { }, setBadgeCount: { _ in nil }, pruneCategory: { _ in + // no-op: UNUserNotificationCenter.current() is unavailable in test runner }) } } diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json b/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json index 503b4f4f..a210b627 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/EncodableTests/testKlaviyoState.1.json @@ -70,4 +70,4 @@ "id" : "00000000-0000-0000-0000-000000000001" } ] -} +} \ No newline at end of file From 3d8ff73dc7887113971430e90074d9ee9c299af7 Mon Sep 17 00:00:00 2001 From: Belle Lim Date: Thu, 30 Apr 2026 10:16:49 -0400 Subject: [PATCH 3/3] fix(ci): revert --include-podspecs for KlaviyoSwiftExtension lint (#561) (#569) KlaviyoSwiftExtension no longer depends on KlaviyoCore after #564 dropped the dependency by copying shared types directly. Restore the standalone lint command and update the dependency comment accordingly. Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/cocoapods-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cocoapods-publish.yml b/.github/workflows/cocoapods-publish.yml index 8cdf9503..a48cfb9e 100644 --- a/.github/workflows/cocoapods-publish.yml +++ b/.github/workflows/cocoapods-publish.yml @@ -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) @@ -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'