[Billing Plans] Support fetching & purchasing products with billing plans#6783
Conversation
* introduce CompoundProductIdentifierTests * build CompoundProductIdentifier from single string * only request SK product identifiers in ProductsManager.products() * hello world * old xcode compiler fix * introduce InstallmentInfos * lint * api testers * Update baseline swiftinterface files (#6768) * InstallmentsInfoFactory * unit tests * CEC API tester fix * include more availabilities * allow fetching of both base product + compound product at the same time * make hashable/equals contracts use same properties * add log for invalid product identifiers + tests * lint * lint * StoreProduct: revert productIdentifier to be the store product ID + introduce billingPlanIdentifier * add sk2_unrecognized_billing_plan_identifer log * update string value * add product fetching logs * add additional fields to InstallmentsInfo * add InstallmentsInfo.billingPlanType internally * fix compilation * linting * Update baseline swiftinterface files (#6773) * update purchase testers * BillingPlanType API * Update baseline swiftinterface files (#6779) * remove billingPlanIdentifier * Update baseline swiftinterface files (#6780) --------- Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
* introduce CompoundProductIdentifierTests * build CompoundProductIdentifier from single string * only request SK product identifiers in ProductsManager.products() * hello world * old xcode compiler fix * introduce InstallmentInfos * lint * api testers * Update baseline swiftinterface files (#6768) * InstallmentsInfoFactory * unit tests * CEC API tester fix * include more availabilities * allow fetching of both base product + compound product at the same time * make hashable/equals contracts use same properties * add log for invalid product identifiers + tests * lint * lint * StoreProduct: revert productIdentifier to be the store product ID + introduce billingPlanIdentifier * add sk2_unrecognized_billing_plan_identifer log * update string value * add product fetching logs * add additional fields to InstallmentsInfo * add InstallmentsInfo.billingPlanType internally * OSAgnosticBillingPlanType * fix compilation * linting * Update baseline swiftinterface files (#6773) * purchase billing plans * update purchase testers * throw error * fix test compilation * BillingPlanType API * Update baseline swiftinterface files (#6779) * remove billingPlanIdentifier * update to use public BillingPlanType instead of OSAgnosticBillingPlanType * Update project.pbxproj * add logs * lint * more lint * error log * dont apply upFront by default on all purchases that dont specify it --------- Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
…ackages (#6784) * introduce CompoundProductIdentifierTests * build CompoundProductIdentifier from single string * only request SK product identifiers in ProductsManager.products() * hello world * old xcode compiler fix * introduce InstallmentInfos * lint * api testers * Update baseline swiftinterface files (#6768) * InstallmentsInfoFactory * unit tests * CEC API tester fix * include more availabilities * allow fetching of both base product + compound product at the same time * make hashable/equals contracts use same properties * add log for invalid product identifiers + tests * lint * lint * StoreProduct: revert productIdentifier to be the store product ID + introduce billingPlanIdentifier * add sk2_unrecognized_billing_plan_identifer log * update string value * add product fetching logs * add additional fields to InstallmentsInfo * add InstallmentsInfo.billingPlanType internally * fix compilation * linting * Update baseline swiftinterface files (#6773) * update purchase testers * BillingPlanType API * Update baseline swiftinterface files (#6779) * remove billingPlanIdentifier * Update baseline swiftinterface files (#6780) * include billing plans in offerings response * Update StoreProductTests.swift * lint * Update baseline swiftinterface files (#6785) * only run test on iOS 26.4+ * rename StoreProduct.compoundProductIdentifier to id and use it for equals * lint * fix hash * update TestStoreProduct * Update project.pbxproj * remove out of date test --------- Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Generated by 🚫 Danger |
…ling plans (#6799) * update post receipt values for billing plan purchases * lint * fix test
…upported OS versions (#6805) * dont return billing plan products on unsupported OS versions * negative log assertion in other test * lint * remove productsRequestFactory assert
…hen in SK1 Mode (#6806) * dont return products for compound product identifiers * lint * remove availability check * fix log assertions on old OS versions * fix test compilation * test fix * log fix * Update ProductsManagerTests.swift
* update product caching cache key * add additional tests * lint
…g plan (#6837) * check eligibility for monthly billing plan * lint * package lookup fix * not found products calculation fix * fix tests * small refactor * re-introduce sk1 log * move populateSK2CompoundProductsIfSupported to ProductsFetcherSK2 * refactor to structure things better * pre-compute storeKitProductIdentifiers
|
| Name | Version | Download | Change | Install | Change | Approval |
|---|---|---|---|---|---|---|
| RevenueCat com.revenuecat.PaywallsTester |
1.0 (1) | 18.5 MB | ⬆️ 64.5 kB (0.35%) | 66.9 MB | ⬆️ 227.3 kB (0.34%) | N/A |
| BinarySizeTest com.revenuecat.binary-size-test.local-source |
1.0 (1) | 4.2 MB | ⬆️ 21.5 kB (0.51%) | 12.8 MB | ⬆️ 52.0 kB (0.41%) | ⏳ Needs approval |
| BinarySizeTest com.revenuecat.binary-size-test.cocoapods |
1.0 (1) | 6.4 MB | ⬆️ 33.7 kB (0.53%) | 28.3 MB | ⬆️ 110.8 kB (0.4%) | ⏳ Needs approval |
| BinarySizeTest com.revenuecat.binary-size-test.spm |
1.0 (1) | 4.4 MB | ⬆️ 19.3 kB (0.45%) | 11.1 MB | ⬆️ 44.0 kB (0.4%) | ⏳ Needs approval |
RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester
⚖️ Compare build
⏱️ Analyze build performance
Total install size change: ⬆️ 227.3 kB (0.34%)
Total download size change: ⬆️ 64.5 kB (0.35%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 84.4 kB |
| 📝 RCInstallmentsInfo.Objc Metadata | ⬆️ 7.5 kB |
| Code Signature | ⬆️ 6.3 kB |
| DYLD.Exports | ⬆️ 4.9 kB |
| RevenueCat.ProductsManager.ProductsManager | ⬆️ 4.4 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 52.0 kB (0.41%)
Total download size change: ⬆️ 21.5 kB (0.51%)
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 |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 110.8 kB (0.4%)
Total download size change: ⬆️ 33.7 kB (0.53%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 40.0 kB |
| Code Signature | ⬆️ 2.5 kB |
| 📝 RevenueCat.WinBackOfferEligibilityCalculator.winbackOffersByID(fo... | ⬆️ 2.2 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: ⬆️ 44.0 kB (0.4%)
Total download size change: ⬆️ 19.3 kB (0.45%)
Largest size changes
| Item | Install Size Change |
|---|---|
| 📝 RevenueCat.WinBackOfferEligibilityCalculator.winbackOffersByID(fo... | ⬆️ 2.2 kB |
| DYLD.Exports | ⬆️ 1.6 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
* add BillingPlanType.value * rename to rawValue/remove from() * api tester * silly test fix * Update baseline swiftinterface files (#6797) * Update baseline swiftinterface files (#6798) * customer info updates for products with billing plans * lint * standardize calculations * Update FetchProductsView.swift * rename * lint * add more tests * Update ButtonComponentViewModelInteractionTests.swift * test with decoding two entitlements on product with upfront and monthly --------- Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
…plans (#6846) * check eligibility for monthly billing plan * lint * package lookup fix * not found products calculation fix * fix tests * small refactor * re-introduce sk1 log * move populateSK2CompoundProductsIfSupported to ProductsFetcherSK2 * refactor to structure things better * return promo offers for billing plan * lint
…ts with billing plans (#6845)
tonidero
left a comment
There was a problem hiding this comment.
Did a quick look over and spotted a few small things, but nothing big. I didn't do an in-depth review so I would prefer someone else to approve 🙏
| ) | ||
|
|
||
| default: | ||
| return nil |
There was a problem hiding this comment.
Should we log some error in this case? Feels like it should never be hit?
There was a problem hiding this comment.
Actually, seems this is only used for tests? If so, maybe we can move it to the tests source set? That way we would avoid using it accidentally.
There was a problem hiding this comment.
This is used in CompoundProductIdentifierResolver.resolve(), which is ultimately called by ProductsFetcherSK1 & ProductsFetcherSK2 when products are fetched. It allows us to parse product IDs with colons in them, like when a developer calls Purchases.shared.products(["com.rc.product:monthly"])
I agree though that this shouldn't normally be hit! The only time that this should be hit is when a developer manually requests a product ID with more than one colon in it, like: Purchases.shared.products(["com.rc.product:monthly:extra"]).
I've added a warning log in 63dbe0b
| /** | ||
| The product plan identifier that unlocked this entitlement (for a Google Play subscription purchase) | ||
| The product plan identifier that unlocked this entitlement (for Google Play subscription purchases | ||
| and Apple purchases with non-upFront billing plans) |
There was a problem hiding this comment.
We will need to update this doc in Android/hybrids.
| @objc public let commitmentInstallmentPeriod: SubscriptionPeriod | ||
|
|
||
| /// Price charged for each installment billing period. | ||
| @objc public let installmentBillingPrice: Decimal |
There was a problem hiding this comment.
I guess this can be calculated by having the price of the product and the commmitmentInstallmentsCount? (which I see is what we do in the factory). Still it can be useful as a convenience utility 👍
…undProductIdentifierResolver.swift Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>
ajpallares
left a comment
There was a problem hiding this comment.
Amazing job! I think this looks great!
Just added some final suggestions regarding some missing API tests.
…ng for a billing plan product (#6890) * only return intro offers on billing plan when one is used * simplify logic with representsBillingPlan
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e39fcb4. Configure here.





Motivation
Adds iOS support for 12month commitment products in StoreKit 2, including fetching, caching, displaying, purchasing, eligibility checks, and CustomerInfo handling.
Description
This PR introduces SDK support for compound product identifiers in the format:
{productIdentifier}:{productPlanIdentifier}For example:
com.revenuecat.subscription:monthlyThe SDK now resolves these compound identifiers to the base StoreKit product ID for product requests, then reconstructs billing-plan-specific
StoreProducts when StoreKit 2 returns eligible pricing terms.Main changes:
BillingPlanTypeandInstallmentsInfoAPIs.StoreProduct.installmentsInfofor eligible monthly billing plan products.StoreProduct.idinternally for caching, equality, offerings lookup, intro eligibility, and purchase flows so products with the same StoreKit ID but different billing plans remain distinct..billingPlanType(...)into purchase options.Testing
Added or updated coverage for:
InstallmentsInfoFactoryBillingPlanTypeManually tested the changes using both SKConfig files and the sandbox environment.
Availability Requirements
Products with 12mo commitments are only available when these three requirements are met:
Other Notes
The code in this PR has already been approved in prior pull requests.
Note
Medium Risk
Touches core product fetch, purchase, offerings, caching, and CustomerInfo paths; behavior is gated by OS/compiler but mis-resolution could hide products or fail purchases for billing-plan SKUs.
Overview
Adds StoreKit 2 billing-plan support for compound product IDs (
productId:plan, e.g.com.app.sub:monthly), including publicBillingPlanTypeandInstallmentsInfo, plusStoreProduct.installmentsInfoon iOS 26.4+ when built with Xcode 26.5+.Fetching & identity: A shared resolver parses compound IDs, dedupes StoreKit requests to the base product ID, and SK2 builds plan-specific
StoreProducts from pricing terms (with SK1/OS-version filtering and logging). Caching, offerings, entitlement mapping, andStoreProduct.idnow key off compound IDs so multiple plans for one StoreKit product stay distinct.Purchase & eligibility: SK2 purchases pass
.billingPlanTypewhen the product has installments info; intro, promotional, and win-back eligibility use billing-plan pricing terms (with testable adapters for win-back).ProductRequestDatauses installment billing price for monthly plans.CustomerInfo / offline: Subscriptions and entitlements carry
productPlanIdentifier; offline SK2 products and mapping align with Android-styleproduct:plankeys.Also adds API testers, PurchaseTester fetch UI, and broad unit tests.
Reviewed by Cursor Bugbot for commit 5b828d7. Bugbot is set up for automated code reviews on this repo. Configure here.