[Billing Plans]: Only return intro offers on billing plan when fetching for a billing plan product#6890
Conversation
| self.compoundProductIdentifier = compoundProductIdentifier | ||
| self.installmentsInfo = installmentsInfo | ||
| #if compiler(>=6.3.2) | ||
| self._pricingTerms = Self.pricingTerms(for: sk2Product, installmentsInfo: installmentsInfo) |
There was a problem hiding this comment.
Extracted this out to be computed when the product is created so that we don't need to recompute it each time offers are loaded.
|
| Name | Version | Download | Change | Install | Change | Approval |
|---|---|---|---|---|---|---|
| BinarySizeTest com.revenuecat.binary-size-test.local-source |
1.0 (1) | 4.2 MB | ⬆️ 23.4 kB (0.56%) | 12.7 MB | ⬆️ 52.3 kB (0.42%) | ⏳ Needs approval |
| RevenueCat com.revenuecat.PaywallsTester |
1.0 (1) | 18.4 MB | ⬆️ 61.1 kB (0.33%) | 66.6 MB | ⬆️ 231.4 kB (0.35%) | N/A |
| BinarySizeTest com.revenuecat.binary-size-test.cocoapods |
1.0 (1) | 6.4 MB | ⬆️ 31.7 kB (0.5%) | 28.1 MB | ⬆️ 110.7 kB (0.4%) | ⏳ Needs approval |
| BinarySizeTest com.revenuecat.binary-size-test.spm |
1.0 (1) | 4.3 MB | ⬆️ 23.2 kB (0.54%) | 11.1 MB | ⬆️ 48.5 kB (0.44%) | ⏳ Needs approval |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 52.3 kB (0.42%)
Total download size change: ⬆️ 23.4 kB (0.56%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 3.5 kB |
| 📝 RevenueCat.WinBackOfferEligibilityCalculator.winbackOffersByID(fo... | ⬆️ 2.2 kB |
| DYLD.Exports | ⬆️ 1.7 kB |
| 📝 RCInstallmentsInfo.Objc Metadata | ⬆️ 1.6 kB |
| Swift.Sequence.compactMap | ⬇️ -1.3 kB |
RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester
⚖️ Compare build
⏱️ Analyze build performance
Total install size change: ⬆️ 231.4 kB (0.35%)
Total download size change: ⬆️ 61.1 kB (0.33%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 84.4 kB |
| 📝 RCInstallmentsInfo.Objc Metadata | ⬆️ 7.5 kB |
| Code Signature | ⬆️ 5.5 kB |
| DYLD.Exports | ⬆️ 4.9 kB |
| RevenueCat.ProductsManager.ProductsManager | ⬆️ 4.4 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 110.7 kB (0.4%)
Total download size change: ⬆️ 31.7 kB (0.5%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 39.8 kB |
| Code Signature | ⬆️ 2.5 kB |
| 📝 RevenueCat.WinBackOfferEligibilityCalculator.winbackOffersByID(fo... | ⬆️ 2.3 kB |
| DYLD.Exports | ⬆️ 1.6 kB |
| 📝 RCInstallmentsInfo.Objc Metadata | ⬆️ 1.6 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 48.5 kB (0.44%)
Total download size change: ⬆️ 23.2 kB (0.54%)
Largest size changes
| Item | Install Size Change |
|---|---|
| 📝 RevenueCat.WinBackOfferEligibilityCalculator.winbackOffersByID(fo... | ⬆️ 2.2 kB |
| DYLD.Exports | ⬆️ 1.7 kB |
| 📝 RCInstallmentsInfo.Objc Metadata | ⬆️ 1.6 kB |
| Swift.Sequence.compactMap | ⬇️ -1.3 kB |
| Strings.Unmapped | ⬆️ 1.2 kB |
🛸 Powered by Emerge Tools
Comment trigger: Size diff threshold of 100.00kB exceeded
| func contains( | ||
| subscriptionOfferType: StoreKit.Product.SubscriptionOffer.OfferType, | ||
| on billingPlanType: BillingPlanType | ||
| func containsSubscriptionOfferTypeOnBillingPlan( |
There was a problem hiding this comment.
Since we now scope the SK2Product to just one billing plan, the way the method was structured previously doesn't really make sense anymore. Updated it to check for an offer on the pricing terms contained in the product!
rickvdl
left a comment
There was a problem hiding this comment.
Nice! Couple of questions mainly :)
| .first | ||
| .flatMap { StoreProductDiscount(sk2Discount: $0, currencyCode: self.currencyCode) } | ||
| } else { | ||
| return introductoryDiscountOnProduct() |
There was a problem hiding this comment.
As you mentioned we probably can’t unit test the billing-plan support through SKTestSession, but could we at least test the introductoryDiscountOnProduct() branches of this getter?
There was a problem hiding this comment.
Yes! Luckily, this path is already covered by existing tests:
StoreProductTests.testSk2DetailsWrapsCorrectly() fetches a StoreKit 2 product with an intro offer from a SKConfig file and verifies the intro offer's contents. This exercises the plain introductoryDiscountOnProduct() branch of the getter since it doesn't have a billing plan on it
| .subscription? | ||
| .pricingTerms | ||
| .first(where: { $0.billingPlanType == billingPlan.skBillingPlanType }) | ||
| if self.compoundProductIdentifier.productPlanIdentifier != nil, |
There was a problem hiding this comment.
It took me a bit (without having all the context) to understand this line, and it’s used in a couple of places, would it make sense to extract it to a helper like
/// Whether this product represents a specific billing plan rather than the base product.
var representsBillingPlan: Bool {
self.compoundProductIdentifier.productPlanIdentifier != nil
}
There was a problem hiding this comment.
Love this! Good to not repeat yourself, and it definitely makes things easier to read. Added the computed property + tests and switched over all of those calls in 094317b
|
|
||
| #if compiler(>=6.3.2) | ||
| if let installmentsInfo = sk2StoreProduct.installmentsInfo, | ||
| if sk2StoreProduct.installmentsInfo != nil, |
There was a problem hiding this comment.
Could it (theoretically) be that we’re divergenging here, since in other places we’re checking for self.compoundProductIdentifier.productPlanIdentifier != nil? Should we combine these checks just to be safe?
There was a problem hiding this comment.
In practice, probably not, but I think it still makes sense to standardize how we're computing this. In 094317b, I switched this to use our new SK2StoreProduct. representsBillingPlan instead :)
rickvdl
left a comment
There was a problem hiding this comment.
Just one question; would it make sense to (even thought they won't work) already write and include the SK tests so that they'll either succeed or gain our attention once the Xcode bug is resolved like Antonio suggested over Slack?
|
@rickvdl Yeah, I think we should! I'm going to go ahead and merge this into the dev branch and then will create a new PR going into the dev branch with several tests backed by SKConfig files |




Description
This PR fixes a bug where we would return intro offers on the main StoreKit product for products represented by a billing plan. This meant that if an upFront variation of a product had an intro offer, but the
monthlybilling plan did not,StoreProduct.introductoryDiscountwould return the intro offer, even though it isn't present on the monthly billing plan.Testing
Note
Medium Risk
Changes purchase-facing offer and intro-eligibility behavior for billing-plan SK2 products on iOS 26.4+; wrong pricing-term selection would mislead paywalls but scope is narrow and guarded by compiler/OS availability.
Overview
Fixes billing-plan
StoreProductinstances incorrectly surfacing introductory and promotional offers from the base StoreKit subscription when those offers exist only on other pricing terms (e.g. up-front vs monthly).SK2StoreProductnow treats a product as a billing plan whencompoundProductIdentifierincludes a product plan id, caches the matchingpricingTermsat init, and uses that slice forintroductoryDiscount,discounts, and intro-eligibility checks instead of the subscription-level offers. Eligibility uses the renamedcontainsSubscriptionOfferTypeOnBillingPlanhelper. Unit tests coverrepresentsBillingPlanfor with/without plan id.Reviewed by Cursor Bugbot for commit 094317b. Bugbot is set up for automated code reviews on this repo. Configure here.