From cbabbcd3726c3eefe79283d8be7db624327f8aae Mon Sep 17 00:00:00 2001 From: Sebastian Szturo Date: Thu, 9 Apr 2026 10:47:15 +0900 Subject: [PATCH 1/4] Expose abandoned product params --- .../TrackableSuperwallEvent.swift | 10 +++++ .../Internal Tracking/TrackTests.swift | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 97dbd5a3fa..e5873369f2 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -671,6 +671,16 @@ enum InternalSuperwallEvent { var params = paywallInfo.audienceFilterParams() if let product = product { params["abandoned_product_id"] = product.productIdentifier + let hasRicherProductAttributes = + !product.localizedPrice.isEmpty + || !product.localizedSubscriptionPeriod.isEmpty + || !product.period.isEmpty + + if hasRicherProductAttributes { + for (key, value) in product.attributes { + params["abandoned_product_\(key.camelCaseToSnakeCase())"] = value + } + } } return params default: diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift index 51decc4b4c..6e5c7b8eb4 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift @@ -1695,6 +1695,43 @@ struct TrackingTests { result.parameters.audienceFilterParams["presented_by_event_name"] as? String == paywallInfo.presentedByPlacementWithName) } + @Test func transaction_abandon() async { + let paywallInfo: PaywallInfo = .stub() + let productId = "abc" + let product = StoreProduct( + sk1Product: MockSkProduct(productIdentifier: productId), + entitlements: [.stub()] + ) + let dependencyContainer = DependencyContainer() + let skTransaction = MockSKPaymentTransaction(state: .purchased) + let transaction = await dependencyContainer.makeStoreTransaction(from: skTransaction) + let result = await Superwall.shared.track( + InternalSuperwallEvent.Transaction( + state: .abandon(product), + paywallInfo: paywallInfo, + product: product, + transaction: transaction, + source: .external, + isObserved: false, + storeKitVersion: .storeKit1 + ) + ) + + #expect(result.parameters.audienceFilterParams["$event_name"] as! String == "transaction_abandon") + #expect(result.parameters.audienceFilterParams["$product_period"] != nil) + #expect(result.parameters.audienceFilterParams["$product_period_months"] != nil) + + #expect( + result.parameters.audienceFilterParams["event_name"] as! String == "transaction_abandon") + #expect( + result.parameters.audienceFilterParams["abandoned_product_id"] as! String == productId) + #expect(result.parameters.audienceFilterParams["abandoned_product_identifier"] as! String == productId) + #expect(result.parameters.audienceFilterParams["abandoned_product_period"] != nil) + #expect(result.parameters.audienceFilterParams["abandoned_product_period_months"] != nil) + #expect(result.parameters.audienceFilterParams["abandoned_product_period_years"] != nil) + #expect(result.parameters.audienceFilterParams["abandoned_product_localized_period"] != nil) + } + @Test func transaction_fail() async { let paywallInfo: PaywallInfo = .stub() let productId = "abc" From 0deb490c0fa72ff7323f306854f7634603d63147 Mon Sep 17 00:00:00 2001 From: Sebastian Szturo Date: Thu, 9 Apr 2026 12:33:56 +0900 Subject: [PATCH 2/4] Tighten abandoned product filtering --- .../TrackableSuperwallEvent.swift | 3 +-- .../Internal Tracking/TrackTests.swift | 13 ++++++++----- .../Mocks/SKProductSubscriptionPeriodMock.swift | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index e5873369f2..8084d9d2ee 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -672,8 +672,7 @@ enum InternalSuperwallEvent { if let product = product { params["abandoned_product_id"] = product.productIdentifier let hasRicherProductAttributes = - !product.localizedPrice.isEmpty - || !product.localizedSubscriptionPeriod.isEmpty + !product.localizedSubscriptionPeriod.isEmpty || !product.period.isEmpty if hasRicherProductAttributes { diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift index 6e5c7b8eb4..0deed330c7 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift @@ -1699,7 +1699,10 @@ struct TrackingTests { let paywallInfo: PaywallInfo = .stub() let productId = "abc" let product = StoreProduct( - sk1Product: MockSkProduct(productIdentifier: productId), + sk1Product: MockSkProduct( + subscriptionPeriod: SKProductSubscriptionPeriodMock(numberOfUnits: 1, unit: .month), + productIdentifier: productId + ), entitlements: [.stub()] ) let dependencyContainer = DependencyContainer() @@ -1726,10 +1729,10 @@ struct TrackingTests { #expect( result.parameters.audienceFilterParams["abandoned_product_id"] as! String == productId) #expect(result.parameters.audienceFilterParams["abandoned_product_identifier"] as! String == productId) - #expect(result.parameters.audienceFilterParams["abandoned_product_period"] != nil) - #expect(result.parameters.audienceFilterParams["abandoned_product_period_months"] != nil) - #expect(result.parameters.audienceFilterParams["abandoned_product_period_years"] != nil) - #expect(result.parameters.audienceFilterParams["abandoned_product_localized_period"] != nil) + #expect(result.parameters.audienceFilterParams["abandoned_product_period"] as? String == "month") + #expect(result.parameters.audienceFilterParams["abandoned_product_period_months"] as? String == "1") + #expect(result.parameters.audienceFilterParams["abandoned_product_period_years"] as? String == "0") + #expect((result.parameters.audienceFilterParams["abandoned_product_localized_period"] as? String)?.isEmpty == false) } @Test func transaction_fail() async { diff --git a/Tests/SuperwallKitTests/StoreKit/Mocks/SKProductSubscriptionPeriodMock.swift b/Tests/SuperwallKitTests/StoreKit/Mocks/SKProductSubscriptionPeriodMock.swift index dc6bcd25ff..767e56a370 100644 --- a/Tests/SuperwallKitTests/StoreKit/Mocks/SKProductSubscriptionPeriodMock.swift +++ b/Tests/SuperwallKitTests/StoreKit/Mocks/SKProductSubscriptionPeriodMock.swift @@ -9,7 +9,22 @@ import Foundation import StoreKit final class SKProductSubscriptionPeriodMock: SKProductSubscriptionPeriod { + private let internalNumberOfUnits: Int + private let internalUnit: SKProduct.PeriodUnit + override var numberOfUnits: Int { - return 1 + return internalNumberOfUnits + } + + override var unit: SKProduct.PeriodUnit { + return internalUnit + } + + init( + numberOfUnits: Int = 1, + unit: SKProduct.PeriodUnit = .month + ) { + self.internalNumberOfUnits = numberOfUnits + self.internalUnit = unit } } From 4c0b48328a712654bab631b984267f058d8d3554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:11:48 +0100 Subject: [PATCH 3/4] Add to changelog, updates test, simplifies code --- CHANGELOG.md | 3 ++- .../Trackable Events/TrackableSuperwallEvent.swift | 8 ++------ .../Analytics/Internal Tracking/TrackTests.swift | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ba04b671..98a7000cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # CHANGELOG The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub. - +u ## 4.15.1 ### Enhancements - Adds an `onCustomCallback` parameter to `getPaywall`. - `SuperwallOptions.localResources` now accepts UIImage's from xcasset files, e.g. `UIImage(named: "my-image")`. +- Exposes abandoned transaction product params in audience filters. ## 4.15.0 diff --git a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift index 8084d9d2ee..54023185b6 100644 --- a/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift +++ b/Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift @@ -671,12 +671,8 @@ enum InternalSuperwallEvent { var params = paywallInfo.audienceFilterParams() if let product = product { params["abandoned_product_id"] = product.productIdentifier - let hasRicherProductAttributes = - !product.localizedSubscriptionPeriod.isEmpty - || !product.period.isEmpty - - if hasRicherProductAttributes { - for (key, value) in product.attributes { + if !product.period.isEmpty { + for (key, value) in product.attributes where key != "identifier" { params["abandoned_product_\(key.camelCaseToSnakeCase())"] = value } } diff --git a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift index 0deed330c7..6eec49e010 100644 --- a/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift +++ b/Tests/SuperwallKitTests/Analytics/Internal Tracking/TrackTests.swift @@ -1728,7 +1728,7 @@ struct TrackingTests { result.parameters.audienceFilterParams["event_name"] as! String == "transaction_abandon") #expect( result.parameters.audienceFilterParams["abandoned_product_id"] as! String == productId) - #expect(result.parameters.audienceFilterParams["abandoned_product_identifier"] as! String == productId) + #expect(result.parameters.audienceFilterParams["abandoned_product_identifier"] == nil) #expect(result.parameters.audienceFilterParams["abandoned_product_period"] as? String == "month") #expect(result.parameters.audienceFilterParams["abandoned_product_period_months"] as? String == "1") #expect(result.parameters.audienceFilterParams["abandoned_product_period_years"] as? String == "0") From 4c77411fe6d0434b75657c34bc970640554e6cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20To=CC=88r?= <3296904+yusuftor@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:21:33 +0100 Subject: [PATCH 4/4] Remove stray character in CHANGELOG Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a7000cf8..107de9ccd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # CHANGELOG The changelog for `SuperwallKit`. Also see the [releases](https://github.com/superwall/Superwall-iOS/releases) on GitHub. -u + ## 4.15.1 ### Enhancements