Skip to content

[Billing Plans]: Support billing plans in CustomerInfo#6802

Merged
fire-at-will merged 16 commits into
billing-plans-devfrom
customer-info-updates
May 27, 2026
Merged

[Billing Plans]: Support billing plans in CustomerInfo#6802
fire-at-will merged 16 commits into
billing-plans-devfrom
customer-info-updates

Conversation

@fire-at-will

@fire-at-will fire-at-will commented May 14, 2026

Copy link
Copy Markdown
Contributor

Description

Updates CustomerInfo handling (online/offline) to support purchases for products with billing plans.

Testing

  • Added unit tests
  • Manually verified changes

Note

Medium Risk
Changes how entitlements are mapped and identified by introducing compound product IDs (e.g. product:monthly) and propagating productPlanIdentifier through CustomerInfo, which could affect entitlement unlocking if identifiers don’t match backend payloads.

Overview
Adds billing-plan awareness to CustomerInfo by propagating productPlanIdentifier through CustomerInfoResponse.Subscription and EntitlementInfo, including Apple StoreKit 2 transactions.

Offline entitlements now key lookups by a compound product id (PurchasedSK2Product.id) and the product→entitlement mapping response can include base_plan_id; conversion to ProductEntitlementMapping now builds compound keys (skipping upFront).

Updates tests and fixtures to cover base-plan decoding, compound mapping behavior, and offline entitlement creation for multiple billing plans, plus a small tester-app tweak to use a compound product id.

Reviewed by Cursor Bugbot for commit 2122281. Bugbot is set up for automated code reviews on this repo. Configure here.

@emerge-tools

emerge-tools Bot commented May 14, 2026

Copy link
Copy Markdown

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 18.0 MB ⬆️ 33.5 kB (0.19%) 64.8 MB ⬆️ 112.1 kB (0.17%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.local-source
1.0 (1) 4.1 MB ⬆️ 17.4 kB (0.42%) 12.4 MB ⬆️ 38.4 kB (0.31%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.cocoapods
1.0 (1) 6.2 MB ⬆️ 21.2 kB (0.34%) 27.3 MB ⬆️ 67.7 kB (0.25%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.spm
1.0 (1) 4.2 MB ⬆️ 18.1 kB (0.43%) 10.8 MB ⬆️ 39.0 kB (0.36%) ⏳ Needs approval

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 112.1 kB (0.17%)
Total download size change: ⬆️ 33.5 kB (0.19%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 30.3 kB
📝 RCInstallmentsInfo.Objc Metadata ⬆️ 8.8 kB
RevenueCat.ProductsManager.ProductsManager ⬆️ 6.6 kB
DYLD.Exports ⬆️ 3.7 kB
Code Signature ⬆️ 3.4 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 38.4 kB (0.31%)
Total download size change: ⬆️ 17.4 kB (0.42%)

Largest size changes

Item Install Size Change
RevenueCat.ProductsManager.products(withIdentifiers,completion) ⬆️ 2.6 kB
📝 RCInstallmentsInfo.Objc Metadata ⬆️ 2.1 kB
DYLD.Exports ⬆️ 1.8 kB
DYLD.String Table ⬆️ 1.6 kB
📝 RevenueCat.TestStoreProduct.init(localizedTitle,price,currencyCod... ⬆️ 1.2 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 67.7 kB (0.25%)
Total download size change: ⬆️ 21.2 kB (0.34%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 18.7 kB
RevenueCat.ProductsManager.products(withIdentifiers,completion) ⬆️ 2.6 kB
📝 RCInstallmentsInfo.Objc Metadata ⬆️ 2.1 kB
DYLD.Exports ⬆️ 1.7 kB
Code Signature ⬆️ 1.7 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 39.0 kB (0.36%)
Total download size change: ⬆️ 18.1 kB (0.43%)

Largest size changes

Item Install Size Change
RevenueCat.ProductsManager.products(withIdentifiers,completion) ⬆️ 2.6 kB
📝 RCInstallmentsInfo.Objc Metadata ⬆️ 2.1 kB
DYLD.Exports ⬆️ 1.8 kB
Code Signature ⬆️ 1.2 kB
📝 RevenueCat.TestStoreProduct.init(localizedTitle,price,currencyCod... ⬆️ 1.2 kB
View Treemap

Image of diff


🛸 Powered by Emerge Tools

Comment trigger: Size diff threshold of 100.00kB exceeded

@fire-at-will fire-at-will changed the title Customer info updates [Billing Plans]: Support billing plans in CustomerInfo May 14, 2026
@fire-at-will fire-at-will marked this pull request as ready for review May 19, 2026 22:06
@fire-at-will fire-at-will requested a review from a team as a code owner May 19, 2026 22:06
@fire-at-will

Copy link
Copy Markdown
Contributor Author

Marking ready for review to get cursor reviews


for product in products {
for entitlement in mapping.entitlements(for: product.productIdentifier) {
for entitlement in mapping.entitlements(for: product.id) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Subscriptions dictionary loses data for same-productIdentifier products

Medium Severity

The subscriptions dictionary is keyed by productIdentifier (base), so when multiple PurchasedSK2Product entries share the same base product identifier but have different billing plans, only the last one survives. Meanwhile, the entitlements lookup on line 83 uses product.id (compound identifier), creating an inconsistency: entitlements are correctly resolved for all products, but subscription data (expiration dates, active subscriptions) is silently dropped for all but the last product with a given productIdentifier. This means products with distinct billing plans that share a base StoreKit product ID won't all appear in activeSubscriptions.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2122281. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think that this can happen in practice. StoreKit only allows you to be subscribed to one billing plan on a product at a time, so you can't be subscribed to both com.rc.product and com.rc.product:monthly at the same time, so the conflict can't occur.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

But… to store kit, aren't they both actually com.rc.product?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@JZDesign yes but we are converting those to compound ids for the mapping, see the change right below.

@fire-at-will fire-at-will requested a review from tonidero May 20, 2026 20:30
@@ -0,0 +1,17 @@
{
"product_entitlement_mapping": {
"com.revenuecat.foo_1:monthly": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we have some tests that verify what happens if we have multiple entries here, one for the "monthly" plan and one for the "upfront" with different entitlements (weird scenario... not sure if we want to support it?), and see how the offline customer info calculator would handle that?

Also, in case we don't want to support different entitlements for different "base plans", would it make sense to not include it here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great idea!

We do support having different entitlements across the different billing plans on a single StoreKit product. In this case, we'd want to test that com.rc.product (upFront variant) maps to one entitlement, and that com.rc.product:monthly maps to a different one.

I've updated the existing test that uses this fixture to do that here: 57352d5

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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 57352d5. Configure here.

default:
return rawValue
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Asymmetric handling of unknown billing plan types breaks offline entitlements

Medium Severity

The static compoundProductIDPlanComponent(from:) returns unknown raw values as-is (creating compound mapping keys like "product:yearly"), while the instance compoundProductIDPlanComponent returns nil for unknown types. Since BillingPlanType.from(storeKitBillingPlanType:) returns nil for unrecognized StoreKit billing plans, the device-side PurchasedSK2Product.id will be just the base productIdentifier. This means the mapping has compound keys for unknown plan types that the device-side code can never produce, causing mapping.entitlements(for: product.id) to fail for future billing plan types that the backend knows about but the SDK doesn't yet recognize.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 57352d5. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is a case we can handle gracefully without an SDK update. If Apple introduces a new billing plan type, the raw value we get from StoreKit likely won't match the RevenueCat/backend plan identifier we use in compound product IDs. For example, the current StoreKit raw values are:

  • Monthly: MONTHLY
  • Up-front: BILLED_UPFRONT

So we can't safely infer the backend identifier heuristically for future billing plan types.

I also don't think this is a major concern in practice. Supporting a new billing plan type would require an SDK update anyway, and users purchasing that plan would likely be doing so through an SDK version that already knows how to map it for offline entitlements.

@fire-at-will fire-at-will merged commit f1caf3b into billing-plans-dev May 27, 2026
42 of 43 checks passed
@fire-at-will fire-at-will deleted the customer-info-updates branch May 27, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants