From cbabbcd3726c3eefe79283d8be7db624327f8aae Mon Sep 17 00:00:00 2001 From: Sebastian Szturo Date: Thu, 9 Apr 2026 10:47:15 +0900 Subject: [PATCH 1/2] 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 97dbd5a3f..e5873369f 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 51decc4b4..6e5c7b8eb 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/2] 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 e5873369f..8084d9d2e 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 6e5c7b8eb..0deed330c 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 dc6bcd25f..767e56a37 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 } }