Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,15 @@ enum InternalSuperwallEvent {
var params = paywallInfo.audienceFilterParams()
if let product = product {
params["abandoned_product_id"] = product.productIdentifier
let hasRicherProductAttributes =
!product.localizedSubscriptionPeriod.isEmpty
|| !product.period.isEmpty
Comment on lines +674 to +676
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Guard condition always passes for real App Store products

localizedPrice is non-empty for every real App Store product (subscriptions, consumables, non-consumables alike), so the hasRicherProductAttributes OR-condition is effectively always true for any StoreKit product. This means non-subscription products will also have their subscription-specific period fields (e.g. abandoned_product_period_months: "", abandoned_product_period_days: "") emitted as empty strings in the audience filter params, which may create noise in the filter picker UI.

If the intent is to expose duration fields only for subscription products, consider checking localizedSubscriptionPeriod alone (or check !product.period.isEmpty), since those are empty for non-subscription products:

let hasRicherProductAttributes = !product.localizedSubscriptionPeriod.isEmpty
  || !product.period.isEmpty
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Analytics/Internal Tracking/Trackable Events/TrackableSuperwallEvent.swift
Line: 674-677

Comment:
**Guard condition always passes for real App Store products**

`localizedPrice` is non-empty for every real App Store product (subscriptions, consumables, non-consumables alike), so the `hasRicherProductAttributes` OR-condition is effectively always `true` for any StoreKit product. This means non-subscription products will also have their subscription-specific period fields (e.g. `abandoned_product_period_months: ""`, `abandoned_product_period_days: ""`) emitted as empty strings in the audience filter params, which may create noise in the filter picker UI.

If the intent is to expose duration fields only for subscription products, consider checking `localizedSubscriptionPeriod` alone (or check `!product.period.isEmpty`), since those are empty for non-subscription products:

```swift
let hasRicherProductAttributes = !product.localizedSubscriptionPeriod.isEmpty
  || !product.period.isEmpty
```

How can I resolve this? If you propose a fix, please make it concise.


if hasRicherProductAttributes {
for (key, value) in product.attributes {
params["abandoned_product_\(key.camelCaseToSnakeCase())"] = value
}
}
}
return params
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,46 @@ 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(
subscriptionPeriod: SKProductSubscriptionPeriodMock(numberOfUnits: 1, unit: .month),
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"] 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 {
let paywallInfo: PaywallInfo = .stub()
let productId = "abc"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}