? offerTagsAndroid;
final PricingPhasesAndroid? pricingPhasesAndroid;
+ final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid;
SubscriptionOffer({
required this.id,
@@ -814,6 +857,17 @@ enum class PaymentMode {
this.offerTokenAndroid,
this.offerTagsAndroid,
this.pricingPhasesAndroid,
+ this.installmentPlanDetailsAndroid,
+ });
+}
+
+class InstallmentPlanDetailsAndroid {
+ final int commitmentPaymentsCount;
+ final int subsequentCommitmentPaymentsCount;
+
+ InstallmentPlanDetailsAndroid({
+ required this.commitmentPaymentsCount,
+ required this.subsequentCommitmentPaymentsCount,
});
}
@@ -854,6 +908,11 @@ var base_plan_id_android: String
var offer_token_android: String
var offer_tags_android: Array[String]
var pricing_phases_android: PricingPhasesAndroid
+var installment_plan_details_android: InstallmentPlanDetailsAndroid
+
+class InstallmentPlanDetailsAndroid:
+ var commitment_payments_count: int
+ var subsequent_commitment_payments_count: int
class SubscriptionPeriod:
var unit: SubscriptionPeriodUnit
diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx
index 1c077fb7..f4f05c91 100644
--- a/packages/docs/src/pages/docs/types/purchase.tsx
+++ b/packages/docs/src/pages/docs/types/purchase.tsx
@@ -547,8 +547,81 @@ function TypesPurchase() {
)
+
+
+ pendingPurchaseUpdateAndroid
+ |
+
+ Pending subscription upgrade/downgrade details. When a user
+ initiates a plan change, this contains the new product IDs
+ and purchase token for the pending transaction. Returns null
+ if no pending update exists. See{' '}
+
+ PendingPurchaseUpdateAndroid
+ {' '}
+ below. (
+
+ Billing Library 5.0+
+
+ )
+ |
+
+
+
+
+ PendingPurchaseUpdateAndroid{' '}
+
+ (from{' '}
+
+ Purchase.PendingPurchaseUpdate
+
+ )
+
+
+
+ Contains details about a pending subscription upgrade or downgrade.
+ When a user changes their subscription plan, the new plan may be
+ pending until the current billing period ends.
+
+
+
+
+ | Name |
+ Summary |
+
+
+
+
+
+ products
+ |
+
+ List of product IDs for the pending purchase update.
+ These are the new products the user is switching to.
+ |
+
+
+
+ purchaseToken
+ |
+
+ Unique token identifying the pending transaction.
+ Use this to track or manage the pending update.
+ |
+
+
+
+
>
),
}}
diff --git a/packages/docs/src/pages/docs/updates/notes.tsx b/packages/docs/src/pages/docs/updates/notes.tsx
index 5d9a82f7..3165ade2 100644
--- a/packages/docs/src/pages/docs/updates/notes.tsx
+++ b/packages/docs/src/pages/docs/updates/notes.tsx
@@ -26,6 +26,92 @@ function Notes() {
useScrollToHash();
const allNotes: Note[] = [
+ // GQL 1.3.17 / Google 1.3.28 - Feb 11, 2026
+ {
+ id: 'gql-1-3-17-google-1-3-28',
+ date: new Date('2026-02-11'),
+ element: (
+
+
+ 📅 openiap-gql v1.3.17 / openiap-google v1.3.28 - Android BillingClient Enhancement
+
+
+
+ Added new fields from Google Play Billing Library 5.0+ and 7.0+ for offer details, installment plans, and pending subscription updates.
+
+
+ {/* Section 1: purchaseOptionId */}
+
+
+ 1. purchaseOptionId for One-Time Purchase Offers
+
+
+ Identifies which purchase option the user selected for one-time products with multiple offers.
+
+
+
+
+ {/* Section 2: InstallmentPlanDetailsAndroid */}
+
+
+ 2. InstallmentPlanDetailsAndroid for Subscriptions
+
+
+ Subscription installment plans - users pay over a commitment period (e.g., 12 monthly payments).
+
+
{`type InstallmentPlanDetailsAndroid {
+ commitmentPaymentsCount: Int! # Initial commitment payments
+ subsequentCommitmentPaymentsCount: Int! # Renewal commitment (0 = reverts to normal)
+}`}
+
+
+
+ {/* Section 3: PendingPurchaseUpdateAndroid */}
+
+
+ 3. PendingPurchaseUpdateAndroid for Upgrades/Downgrades
+
+
+ Track pending subscription plan changes that take effect at the end of the current billing period.
+
+
{`type PendingPurchaseUpdateAndroid {
+ products: [String!]! # New product IDs user is switching to
+ purchaseToken: String! # Token for the pending transaction
+}`}
+
+
+
+ {/* References */}
+
+ References
+
+
+
+ ),
+ },
// GQL 1.3.16 / Apple 1.3.14 - Jan 26, 2026
{
id: 'gql-1-3-16-apple-1-3-14',
@@ -36,75 +122,60 @@ function Notes() {
📅 openiap-gql v1.3.16 / openiap-apple v1.3.14 - ExternalPurchaseCustomLink Support (iOS 18.1+)
- New: ExternalPurchaseCustomLink API Support
-
- Added full support for Apple's ExternalPurchaseCustomLink API (iOS 18.1+) for apps using
- custom external purchase links with token-based reporting.
-
-
- New APIs:
-
-
-
- | Method |
- Description |
-
-
-
- isEligibleForExternalPurchaseCustomLinkIOS() | Check if app can use ExternalPurchaseCustomLink API |
- getExternalPurchaseCustomLinkTokenIOS(tokenType) | Get token for reporting to Apple's External Purchase Server API |
- showExternalPurchaseCustomLinkNoticeIOS(noticeType) | Show CustomLink-specific disclosure notice sheet |
-
-
-
- New Types:
-
- ExternalPurchaseCustomLinkTokenTypeIOS - Token types: acquisition, services
- ExternalPurchaseCustomLinkNoticeTypeIOS - Notice types: browser
- ExternalPurchaseCustomLinkTokenResultIOS - Token result with token and error
- ExternalPurchaseCustomLinkNoticeResultIOS - Notice result with continued and error
-
-
-
-
- Improved: presentExternalPurchaseNoticeSheetIOS()
-
- Now returns externalPurchaseToken field when user continues. This token is required for
- reporting transactions to Apple's External Purchase Server API.
-
-
-{`// Before
+
+ Added full support for Apple's ExternalPurchaseCustomLink API (iOS 18.1+) for apps using custom external purchase links with token-based reporting.
+
+
+
+
1. New APIs
+
+ isEligibleForExternalPurchaseCustomLinkIOS() - Check if app can use ExternalPurchaseCustomLink API
+ getExternalPurchaseCustomLinkTokenIOS(tokenType) - Get token for reporting to Apple's External Purchase Server API
+ showExternalPurchaseCustomLinkNoticeIOS(noticeType) - Show CustomLink-specific disclosure notice sheet
+
+
+
+
+
2. New Types
+
+ ExternalPurchaseCustomLinkTokenTypeIOS - Token types: acquisition, services
+ ExternalPurchaseCustomLinkNoticeTypeIOS - Notice types: browser
+ ExternalPurchaseCustomLinkTokenResultIOS - Token result with token and error
+ ExternalPurchaseCustomLinkNoticeResultIOS - Notice result with continued and error
+
+
+
+
+
3. Improved presentExternalPurchaseNoticeSheetIOS()
+
+ Now returns externalPurchaseToken field when user continues. This token is required for reporting transactions to Apple's External Purchase Server API.
+
+
{`// Before
result.result // "continue" or "dismissed"
result.error // optional error
// After (v1.3.14+)
result.result // "continue" or "dismissed"
result.externalPurchaseToken // Token string (when result is "continue")
-result.error // optional error`}
-
-
- API Distinction:
-
-
-
- | API |
- iOS Version |
- Use Case |
-
-
-
- ExternalPurchase | 17.4+ | Basic external purchase notice |
- ExternalPurchaseCustomLink | 18.1+ | Custom links with token-based reporting |
-
-
-
- References:
-
+result.error // optional error`}
+
+
+
+
4. API Comparison
+
+ ExternalPurchase (17.4+): Basic external purchase notice | ExternalPurchaseCustomLink (18.1+): Custom links with token-based reporting
+
+
+
+
+ References
+
+
),
},
@@ -118,36 +189,26 @@ result.error // optional error`}
📅 openiap-gql v1.3.15 / openiap-google v1.3.27 / openiap-apple v1.3.13 - Bug Fix
- Android - Fix SubscriptionProductReplacementParams ReplacementMode Mapping:
-
- Fixed incorrect replacementModeConstant mapping in applySubscriptionProductReplacementParams.
- The function was using values from the legacy SubscriptionUpdateParams.ReplacementMode API instead of
- the new SubscriptionProductReplacementParams.ReplacementMode API (Billing Library 8.1.0+).
-
-
- Issue: #71
-
-
-
-
-
- | Mode |
- Before (Wrong) |
- After (Correct) |
-
-
-
- | CHARGE_FULL_PRICE | 5 | 4 |
- | DEFERRED | 6 | 5 |
- | KEEP_EXISTING | 7 | 6 |
-
-
-
- References:
-
+
+ Fixed incorrect replacementModeConstant mapping in applySubscriptionProductReplacementParams. The function was using values from the legacy SubscriptionUpdateParams.ReplacementMode API instead of the new SubscriptionProductReplacementParams.ReplacementMode API (Billing Library 8.1.0+). Issue: #71
+
+
+
+
Mode Value Changes
+
+ CHARGE_FULL_PRICE: 5 → 4
+ DEFERRED: 6 → 5
+ KEEP_EXISTING: 7 → 6
+
+
+
+
+ References
+
+
),
},
@@ -161,73 +222,54 @@ result.error // optional error`}
📅 openiap-gql v1.3.14 / openiap-google v1.3.25 / openiap-apple v1.3.13 - Breaking Changes & Bug Fixes
- iOS - Subscription-Only Props Cleanup (Breaking Change):
-
- Removed subscription-specific fields from RequestPurchaseIosProps. These fields now only exist in RequestSubscriptionIosProps.
-
-
- introductoryOfferEligibility - Removed from RequestPurchaseIosProps
- promotionalOfferJWS - Removed from RequestPurchaseIosProps
- winBackOffer - Removed from RequestPurchaseIosProps
-
-
- Migration: If using these fields for non-subscription purchases, move to requestSubscription() API.
-
-
-
-
- Known Issue - introductoryOfferEligibility API (Issue #68):
-
- The current introductoryOfferEligibility field uses Boolean type, but Apple's actual
- introductoryOfferEligibility(compactJWS:) API
- requires a JWS string parameter, not a boolean.
-
-
- This will be corrected in a future release. The API signature will change from Boolean to String (JWS).
-
-
-
-
- Android - Fix displayPrice for Subscriptions with Free Trials:
-
- Fixed an issue where displayPrice returned "Free" or "$0.00" for subscription products
- with free trials, instead of the actual base/recurring price.
-
-
-{`// Before (bug)
-product.displayPrice // "Free" or "$0.00"
-product.price // 0.0
-
-// After (fixed)
-product.displayPrice // "$9.99" (base recurring price)
-product.price // 9.99
-
-// Note: Free trial info is still available in subscriptionOffers
-product.subscriptionOffers[0].displayPrice // "$0.00"
-product.subscriptionOffers[0].paymentMode // "free-trial"`}
-
-
-
-
- Apple v1.3.13 - Objective-C Bridge Updates:
-
- Updated OpenIapModule+ObjC.swift to properly expose new Swift async functions to Objective-C.
- This is critical for kmp-iap and other platforms using Kotlin/Native cinterop.
-
-
- - Added ObjC wrappers for new purchase option parameters
- - Ensures all Swift async functions are callable from Kotlin Multiplatform
-
-
- Note: When updating iOS functions in OpenIapModule.swift, always update OpenIapModule+ObjC.swift as well.
- See Objective-C Bridge Documentation.
-
-
- References:
-
+
+ Breaking changes for iOS subscription props, bug fixes for Android displayPrice, and Objective-C bridge updates.
+
+
+
+
1. iOS - Subscription-Only Props Cleanup (Breaking Change)
+
+ Removed subscription-specific fields from RequestPurchaseIosProps. These fields now only exist in RequestSubscriptionIosProps.
+
+
+ introductoryOfferEligibility - Removed
+ promotionalOfferJWS - Removed
+ winBackOffer - Removed
+
+
Migration: Use requestSubscription() API.
+
+
+
+
+
+
3. Android - Fix displayPrice for Subscriptions with Free Trials
+
+ Fixed displayPrice returning "Free" or "$0.00" instead of actual base/recurring price.
+
+
{`// Before (bug): displayPrice = "Free", price = 0.0
+// After (fixed): displayPrice = "$9.99", price = 9.99
+// Free trial info available in: subscriptionOffers[0].displayPrice`}
+
+
+
+
4. Apple v1.3.13 - Objective-C Bridge Updates
+
+ Updated OpenIapModule+ObjC.swift to expose new Swift async functions to Objective-C. Critical for kmp-iap. See Objective-C Bridge Documentation.
+
+
+
+
+ References
+
+
),
},
@@ -241,78 +283,66 @@ product.subscriptionOffers[0].paymentMode // "free-trial"`}
📅 openiap-gql v1.3.13 / openiap-google v1.3.24 / openiap-apple v1.3.11 - Platform API Gap Analysis
- iOS - Win-Back Offers (iOS 18+):
-
- Added support for win-back offers to re-engage churned subscribers.
-
-
- winBackOffer - New field in RequestPurchaseIosProps and RequestSubscriptionIosProps
- WinBackOfferInputIOS - Input type with offerId field
- SubscriptionOfferTypeIOS.WinBack - New enum value
-
-
-{`// Apply win-back offer to subscription purchase
-requestSubscription({
- sku: 'premium_monthly',
- winBackOffer: { offerId: 'winback_50_off' }
-});`}
-
-
- iOS - JWS Promotional Offers (iOS 15+, WWDC 2025):
-
- New signature format using compact JWS string for promotional offers. Back-deployed to iOS 15.
-
-
- promotionalOfferJWS - New field in purchase props
- PromotionalOfferJWSInputIOS - Input type with offerId and jws fields
-
-
- Note: Requires Xcode 16.4+ to compile. Falls back to legacy signature-based offers until then.
-
-
- iOS - Introductory Offer Eligibility Override (iOS 15+, WWDC 2025):
-
- introductoryOfferEligibility - Override system eligibility check for intro offers
- - Set
true to indicate eligible, false for not eligible, nil for system default
-
-
- Note: Requires Xcode 16.4+ to compile. System determines eligibility automatically until then.
-
-
-
-
- Android - Product Status Codes (Billing 8.0+):
-
- Product-level status codes indicating why products couldn't be fetched.
-
-
- ProductStatusAndroid - New enum with values: Ok, NotFound, NoOffersAvailable, Unknown
- productStatusAndroid - New field on ProductAndroid and ProductSubscriptionAndroid
-
-
-{`// Check product fetch status
-val product = fetchProducts(skus).firstOrNull()
-when (product?.productStatusAndroid) {
- ProductStatusAndroid.Ok -> { /* Success */ }
- ProductStatusAndroid.NotFound -> { /* SKU doesn't exist */ }
- ProductStatusAndroid.NoOffersAvailable -> { /* User not eligible */ }
- ProductStatusAndroid.Unknown -> { /* Unknown status */ }
- null -> { /* No product or status */ }
-}`}
-
-
- Android - Auto Service Reconnection:
-
- enableAutoServiceReconnection() is now always enabled internally since OpenIAP uses Billing Library 8.3.0+.
- No configuration needed - the library automatically re-establishes connection if disconnected.
-
-
- References:
-
+
+ New iOS win-back offers, JWS promotional offers, and Android product status codes.
+
+
+
+
1. iOS - Win-Back Offers (iOS 18+)
+
+ Added support for win-back offers to re-engage churned subscribers.
+
+
+ winBackOffer - New field in purchase props
+ WinBackOfferInputIOS - Input type with offerId field
+ SubscriptionOfferTypeIOS.WinBack - New enum value
+
+
+
+
+
2. iOS - JWS Promotional Offers (iOS 15+, WWDC 2025)
+
+ New signature format using compact JWS string for promotional offers. Back-deployed to iOS 15. Requires Xcode 16.4+.
+
+
+ promotionalOfferJWS - New field in purchase props
+ PromotionalOfferJWSInputIOS - Input type with offerId and jws fields
+
+
+
+
+
3. iOS - Introductory Offer Eligibility Override (iOS 15+, WWDC 2025)
+
+ introductoryOfferEligibility - Override system eligibility check. Set true/false/nil for system default. Requires Xcode 16.4+.
+
+
+
+
+
4. Android - Product Status Codes (Billing 8.0+)
+
+ Product-level status codes indicating why products couldn't be fetched.
+
+
+ ProductStatusAndroid - Enum: Ok, NotFound, NoOffersAvailable, Unknown
+ productStatusAndroid - New field on ProductAndroid
+
+
+
+
+
5. Android - Auto Service Reconnection
+
+ enableAutoServiceReconnection() is now always enabled internally since OpenIAP uses Billing Library 8.3.0+.
+
+
+
+
+ References
+
+
),
},
@@ -326,87 +356,62 @@ when (product?.productStatusAndroid) {
📅 openiap-gql v1.3.12 / openiap-google v1.3.22 / openiap-apple v1.3.10 - Standardized Offer Types
- New Cross-Platform Offer Types:
-
- Introduced standardized DiscountOffer and SubscriptionOffer types
- for unified handling of discounts and subscription offers across iOS and Android.
-
-
- DiscountOffer (One-time products):
-
- - Cross-platform type for one-time purchase discounts
- - Android-specific fields use suffix:
offerTokenAndroid, fullPriceMicrosAndroid, percentageDiscountAndroid
- - Replaces deprecated
ProductAndroidOneTimePurchaseOfferDetail
-
-
- SubscriptionOffer:
-
- - Cross-platform type for subscription offers (introductory, promotional)
- - Includes
paymentMode: FreeTrial, PayAsYouGo, PayUpFront
- - Android fields:
offerTokenAndroid, basePlanIdAndroid
- - iOS fields:
signatureIOS, keyIdentifierIOS
- - Replaces deprecated
ProductSubscriptionAndroidOfferDetails, DiscountOfferIOS, DiscountIOS
-
-
- New Fields on Product Types:
-
-{`// ProductAndroid & ProductIOS now include:
-discountOffers: [DiscountOffer!] // One-time product discounts
-subscriptionOffers: [SubscriptionOffer!] // Subscription offers`}
-
-
- PaymentMode Logic Fix (Android):
-
- - Fixed
determinePaymentMode in BillingConverters
- - Zero price → FreeTrial (regardless of recurrenceMode)
- - NON_RECURRING (3) with paid → PayUpFront
- - FINITE_RECURRING (2) / INFINITE_RECURRING (1) with paid → PayAsYouGo
-
-
- Deprecated Types:
-
- -
-
ProductAndroidOneTimePurchaseOfferDetail{' '}
- → Use DiscountOffer
-
- -
-
ProductSubscriptionAndroidOfferDetails{' '}
- → Use SubscriptionOffer
-
- -
-
DiscountOfferIOS{' '}
- → Use SubscriptionOffer
-
- -
-
DiscountIOS{' '}
- → Use SubscriptionOffer
-
- -
-
oneTimePurchaseOfferDetailsAndroid (field){' '}
- → Use discountOffers
-
- -
-
subscriptionOfferDetailsAndroid (field){' '}
- → Use subscriptionOffers
-
-
-
- Migration Example:
-
-{`// Before (deprecated)
-val discount = product.oneTimePurchaseOfferDetailsAndroid?.firstOrNull()
-val offerToken = discount?.offerToken
-
-// After (recommended)
-val discount = product.discountOffers?.firstOrNull()
-val offerToken = discount?.offerTokenAndroid`}
-
-
- References:
-
+
+ Introduced standardized DiscountOffer and SubscriptionOffer types for unified handling across iOS and Android.
+
+
+
+
1. DiscountOffer (One-time products)
+
+ - Cross-platform type for one-time purchase discounts
+ - Android fields:
offerTokenAndroid, fullPriceMicrosAndroid, percentageDiscountAndroid
+ - Replaces deprecated
ProductAndroidOneTimePurchaseOfferDetail
+
+
+
+
+
2. SubscriptionOffer
+
+ - Cross-platform type for subscription offers (introductory, promotional)
+ - Includes
paymentMode: FreeTrial, PayAsYouGo, PayUpFront
+ - Replaces deprecated
ProductSubscriptionAndroidOfferDetails, DiscountOfferIOS, DiscountIOS
+
+
+
+
+
3. New Fields on Product Types
+
+ discountOffers: [DiscountOffer!] - One-time product discounts
+ subscriptionOffers: [SubscriptionOffer!] - Subscription offers
+
+
+
+
+
4. PaymentMode Logic Fix (Android)
+
+ - Zero price → FreeTrial (regardless of recurrenceMode)
+ - NON_RECURRING (3) with paid → PayUpFront
+ - FINITE_RECURRING (2) / INFINITE_RECURRING (1) with paid → PayAsYouGo
+
+
+
+
+
5. Deprecated Types
+
+ ProductAndroidOneTimePurchaseOfferDetail → DiscountOffer
+ ProductSubscriptionAndroidOfferDetails → SubscriptionOffer
+ oneTimePurchaseOfferDetailsAndroid → discountOffers
+ subscriptionOfferDetailsAndroid → subscriptionOffers
+
+
+
+
+ References
+
+
),
},
@@ -420,160 +425,49 @@ val offerToken = discount?.offerTokenAndroid`}
📅 openiap-gql v1.3.11 / openiap-google v1.3.21 / openiap-apple v1.3.9 - PurchaseState Cleanup
- {/* PurchaseState Changes */}
- PurchaseState Simplified:
-
- Removed unused Failed, Restored, and{' '}
- Deferred states from PurchaseState enum.
-
-
-{`// Before
-enum PurchaseState {
- Pending, Purchased, Failed, Restored, Deferred, Unknown
-}
-
-// After
-enum PurchaseState {
- Pending, Purchased, Unknown
-}`}
-
- Why removed?
-
- -
-
Failed - Both platforms return errors instead of Purchase objects on failure
-
- -
-
Restored - Restored purchases return as Purchased state
-
- -
-
Deferred - iOS StoreKit 2 has no transaction state; Android uses Pending
-
-
-
- Note: Apple StoreKit 1's{' '}
-
- SKPaymentTransactionState
- {' '}
- (purchasing, purchased, failed, restored, deferred) is fully deprecated. StoreKit 2 uses{' '}
-
- Product.PurchaseResult
- {' '}
- instead, which only provides a Transaction on success.
-
- References:
-
-
-
-
-
- API Consolidation: Deprecated{' '}
- AlternativeBillingModeAndroid in favor of unified{' '}
- BillingProgramAndroid enum.
-
-
- {/* GQL 1.3.11 Changes */}
- GQL v1.3.11 Other Changes:
-
- -
-
BillingProgramAndroid.USER_CHOICE_BILLING{' '}
- - New enum value for User Choice Billing (7.0+)
-
- -
-
AlternativeBillingModeAndroid - Deprecated
-
- -
-
InitConnectionConfig.alternativeBillingModeAndroid - Deprecated
-
- -
-
RequestPurchaseProps.useAlternativeBilling - Deprecated{' '}
- (only logged debug info, had no effect on purchase flow)
-
-
-
- {/* Google 1.3.21 Changes */}
- Google v1.3.21 Changes:
-
- -
- Updated
OpenIapModule.initConnection() to handle{' '}
- enableBillingProgramAndroid config
-
- -
- Maps
USER_CHOICE_BILLING to internal AlternativeBillingMode.USER_CHOICE
-
- -
- Maps
EXTERNAL_OFFER to internal AlternativeBillingMode.ALTERNATIVE_ONLY
-
- -
- Example app updated to use new API
-
-
-
- Migration Guide:
-
-
-
- | Before (Deprecated) |
- After (Recommended) |
-
-
-
-
- alternativeBillingModeAndroid: USER_CHOICE |
- enableBillingProgramAndroid: USER_CHOICE_BILLING |
-
-
- alternativeBillingModeAndroid: ALTERNATIVE_ONLY |
- enableBillingProgramAndroid: EXTERNAL_OFFER |
-
-
-
-
-{`// Before (deprecated)
-val config = InitConnectionConfig(
- alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice
-)
-
-// After (recommended)
-val config = InitConnectionConfig(
- enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
-)`}
-
+
+ Simplified PurchaseState enum and deprecated AlternativeBillingModeAndroid in favor of BillingProgramAndroid.
+
+
+
+
1. PurchaseState Simplified
+
+ Removed unused Failed, Restored, Deferred states. Now: Pending, Purchased, Unknown
+
+
+ Failed - Platforms return errors instead
+ Restored - Returns as Purchased state
+ Deferred - StoreKit 2 has no transaction state; Android uses Pending
+
+
+
+
+
2. API Consolidation - BillingProgramAndroid
+
+ Deprecated AlternativeBillingModeAndroid in favor of unified BillingProgramAndroid enum.
+
+
+ BillingProgramAndroid.USER_CHOICE_BILLING - New enum value (7.0+)
+ AlternativeBillingModeAndroid - Deprecated
+ InitConnectionConfig.alternativeBillingModeAndroid - Deprecated
+
+
+
+
+
3. Migration
+
+ alternativeBillingModeAndroid: USER_CHOICE → enableBillingProgramAndroid: USER_CHOICE_BILLING
+ alternativeBillingModeAndroid: ALTERNATIVE_ONLY → enableBillingProgramAndroid: EXTERNAL_OFFER
+
+
+
+
+ References
+
+
),
},
@@ -584,185 +478,47 @@ val config = InitConnectionConfig(
element: (
- 📅 openiap-gql v1.3.10 / openiap-google v1.3.19 / openiap-apple v1.3.8 -{' '}
-
- Google Play Billing 8.3.0 External Payments
-
+ 📅 openiap-gql v1.3.10 / openiap-google v1.3.19 / openiap-apple v1.3.8 - Google Play Billing 8.3.0 External Payments
- {/* GQL 1.3.10 */}
-
- GQL v1.3.10 - InitConnectionConfig Enhancement:
-
-
- Added enableBillingProgramAndroid field to{' '}
- InitConnectionConfig for easier billing program setup during connection initialization.
-
-
- -
-
-
enableBillingProgramAndroid: BillingProgramAndroid
- {' '}
- - Enable a specific billing program during initConnection()
-
-
-
-{`// Enable External Payments during connection
-val config = InitConnectionConfig(
- enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments
-)
-iapStore.initConnection(config)`}
-
-
- This provides a cleaner alternative to calling enableBillingProgram(){' '}
- separately before initConnection().
-
-
-
-
- {/* Apple 1.3.8 */}
-
- Apple v1.3.8 - Auto Connection Management:
-
-
- All API methods now automatically call initConnection() internally
- if the connection hasn't been established yet. This eliminates the need to
- manually call initConnection() before using any API.
-
-
- -
-
ensureConnection() - New internal helper that automatically initializes the connection when needed
-
- -
- All public API methods (
fetchProducts, requestPurchase,{' '}
- finishTransaction, etc.) now use ensureConnection()
-
- -
- Backward Compatible - Existing code that calls{' '}
-
initConnection() explicitly will continue to work
-
-
-
Before (v1.3.7):
-
-{`// Must call initConnection first
-try await OpenIapModule.shared.initConnection()
-let products = try await OpenIapModule.shared.fetchProducts(request)`}
-
-
After (v1.3.8):
-
-{`// Just call the API directly - connection is handled automatically
-let products = try await OpenIapModule.shared.fetchProducts(request)`}
-
-
- Note: While explicit initConnection() is no longer required,
- you may still want to call it during app startup to pre-initialize the
- StoreKit connection for faster subsequent API calls.
-
-
-
-
- {/* Google 1.3.19 - External Payments */}
-
- Google v1.3.19 - External Payments Program (Japan Only):
-
-
- Google Play Billing Library 8.3.0 introduces the External Payments
- program, which presents a side-by-side choice between Google Play
- Billing and the developer's external payment option directly in the
- purchase flow.
-
-
New APIs:
-
- -
-
BillingProgramAndroid.EXTERNAL_PAYMENTS{' '}
- - New billing program type for external payments
-
- -
-
DeveloperBillingOptionParamsAndroid{' '}
- - Configure external payment option in purchase flow
-
- -
-
DeveloperBillingLaunchModeAndroid{' '}
- - How to launch the external payment link
-
- -
-
DeveloperProvidedBillingDetailsAndroid{' '}
- - Contains externalTransactionToken when user selects developer billing
-
- -
-
developerProvidedBillingListenerAndroid{' '}
- - New listener for when user selects developer billing
-
- -
-
developerBillingOptionAndroid{' '}
- - New field in RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps
-
-
-
New Event:
-
- -
-
IapEvent.DeveloperProvidedBillingAndroid{' '}
- - Fired when user selects developer billing in External Payments flow
-
-
-
Key Differences from User Choice Billing:
-
-
-
- | Feature |
- User Choice Billing |
- External Payments |
-
-
-
-
- | Billing Library |
- 7.0+ |
- 8.3.0+ |
-
-
- | Availability |
- Eligible regions |
- Japan only |
-
-
- | UI |
- Separate dialog |
- Side-by-side in purchase dialog |
-
-
-
-
References:
-
+
+ InitConnectionConfig enhancement, auto connection management for iOS, and External Payments program support.
+
+
+
+
1. GQL v1.3.10 - InitConnectionConfig Enhancement
+
+ Added enableBillingProgramAndroid: BillingProgramAndroid field for easier billing program setup during initConnection().
+
+
+
+
+
2. Apple v1.3.8 - Auto Connection Management
+
+ All API methods now automatically call initConnection() internally. No need to manually call it before using any API. Backward compatible.
+
+
+
+
+
3. Google v1.3.19 - External Payments Program (Japan Only)
+
+ Billing Library 8.3.0 introduces side-by-side choice between Google Play Billing and developer's external payment.
+
+
+ BillingProgramAndroid.EXTERNAL_PAYMENTS - New billing program type
+ DeveloperBillingOptionParamsAndroid - Configure external payment option
+ DeveloperProvidedBillingDetailsAndroid - Contains externalTransactionToken
+ IapEvent.DeveloperProvidedBillingAndroid - New event
+
+
+
+
+ References
+
+
),
},
@@ -774,117 +530,40 @@ let products = try await OpenIapModule.shared.fetchProducts(request)`}
element: (
- 📅 openiap-google v1.3.16 -{' '}
-
- Google Play Billing 8.2.1
-
+ 📅 openiap-google v1.3.16 - Google Play Billing 8.2.1
-
- Billing Library Upgrade: 8.1.0 → 8.2.1
-
-
- Upgraded to Google Play Billing Library 8.2.1 which includes the new
- Billing Programs API and bug fixes.
-
-
- Why 8.2.1 instead of 8.2.0?
-
-
- Version 8.2.0 had a bug in isBillingProgramAvailableAsync{' '}
- and createBillingProgramReportingDetailsAsync. This was
- fixed in 8.2.1 (released 2025-12-15).
-
-
- New APIs for External Content Links and External Offers:
-
-
- -
-
-
enableBillingProgram()
- {' '}
- - Setup BillingClient for billing programs before{' '}
- initConnection()
-
- -
-
-
isBillingProgramAvailableAsync()
- {' '}
- - Determine user eligibility for the billing program
-
- -
-
-
createBillingProgramReportingDetailsAsync()
- {' '}
- - Create external transaction token for reporting
-
- -
-
-
launchExternalLink()
- {' '}
- - Initiate external link to digital content offer or app download
-
-
-
- Deprecated External Offers APIs:
-
-
- -
-
- enableExternalOffer()
- {' '}
- → Use enableBillingProgram(BillingProgramAndroid.ExternalOffer)
-
- -
-
- isExternalOfferAvailableAsync()
- {' '}
- → Use isBillingProgramAvailable(BillingProgramAndroid.ExternalOffer)
-
- -
-
- createExternalOfferReportingDetailsAsync()
- {' '}
- → Use createBillingProgramReportingDetails()
-
- -
-
- showExternalOfferInformationDialog()
- {' '}
- → Use launchExternalLink()
-
-
-
- References:
-
-
+
+
+ Upgraded from 8.1.0 to 8.2.1 with new Billing Programs API. Skipped 8.2.0 due to bugs in isBillingProgramAvailableAsync and createBillingProgramReportingDetailsAsync.
+
+
+
+
1. New APIs
+
+ enableBillingProgram() - Setup BillingClient for billing programs
+ isBillingProgramAvailableAsync() - Determine user eligibility
+ createBillingProgramReportingDetailsAsync() - Create external transaction token
+ launchExternalLink() - Initiate external link
+
+
+
+
+
2. Deprecated APIs
+
+ enableExternalOffer() → enableBillingProgram(BillingProgramAndroid.ExternalOffer)
+ isExternalOfferAvailableAsync() → isBillingProgramAvailable()
+ createExternalOfferReportingDetailsAsync() → createBillingProgramReportingDetails()
+ showExternalOfferInformationDialog() → launchExternalLink()
+
+
+
+
+ References
+
+
),
},
@@ -896,43 +575,18 @@ let products = try await OpenIapModule.shared.fetchProducts(request)`}
element: (
- 📅 openiap-gql v1.3.8
+ 📅 openiap-gql v1.3.8 - Kotlin Null-Safe Casting
-
- Kotlin Type Generation: Null-Safe Casting
-
-
- Fixed potential TypeCastException in generated Kotlin
- types by using safe casts (as?) instead of unsafe casts
- (as).
+
+
+ Fixed potential TypeCastException in generated Kotlin types by using safe casts (as?) instead of unsafe casts (as).
-
- -
- Lists now use
mapNotNull with safe element casting
-
- -
- Non-nullable fields provide sensible defaults (empty string,
- false, 0, emptyList)
-
- -
- Prevents crashes when JSON keys are missing or contain unexpected
- null values
-
+
+
+ - Lists now use
mapNotNull with safe element casting
+ - Non-nullable fields provide sensible defaults (empty string, false, 0, emptyList)
+ - Prevents crashes when JSON keys are missing or contain unexpected null values
-
- Before (unsafe):
-
-
- {`offerTags = (json["offerTags"] as List<*>).map { it as String }
-offerToken = json["offerToken"] as String`}
-
-
- After (null-safe):
-
-
- {`offerTags = (json["offerTags"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList()
-offerToken = json["offerToken"] as? String ?: ""`}
-
),
},
@@ -944,138 +598,34 @@ offerToken = json["offerToken"] as? String ?: ""`}
element: (
- 📅 openiap-gql v1.3.7 / openiap-apple v1.3.7 / openiap-google v1.3.15
+ 📅 openiap-gql v1.3.7 / openiap-apple v1.3.7 / openiap-google v1.3.15 - Advanced Commerce Data
-
- New Feature: Advanced Commerce Data
-
-
- Added support for{' '}
-
- StoreKit 2's Product.PurchaseOption.custom API
- {' '}
- to pass attribution data during purchases.
-
-
- -
-
-
advancedCommerceData
- {' '}
- - New optional field in RequestPurchaseIosProps and{' '}
- RequestSubscriptionIosProps
-
- -
- Enables passing campaign tokens, affiliate IDs, and other
- attribution data to StoreKit during purchase
-
- -
- Data is formatted as JSON:{' '}
-
{`{"signatureInfo": {"token": ""}}`}
-
-
-
- Usage:
-
-
- {`requestPurchase({
- request: {
- apple: {
- sku: 'com.example.premium',
- advancedCommerceData: 'campaign_summer_2025',
- }
- },
- type: 'in-app'
-});`}
-
-
- Use Cases:
-
-
- - Campaign attribution tracking
- - Affiliate marketing integration
- - Promotional code tracking
-
-
- Reference:{' '}
-
- react-native-iap PR #3106
-
-
-
-
- Deprecated:{' '}
-
- requestPurchaseOnPromotedProductIOS()
-
-
-
-
- The{' '}
-
- requestPurchaseOnPromotedProductIOS()
- {' '}
- API is now deprecated. In StoreKit 2, promoted products can be
- purchased directly via the standard requestPurchase(){' '}
- flow.
-
-
- -
- Use
promotedProductListenerIOS to receive the product
- ID when a user taps a promoted product in the App Store
-
- -
- Call
requestPurchase() with the received SKU directly
-
-
-
- {`// Recommended approach
-promotedProductListenerIOS(async (productId) => {
- await requestPurchase({
- request: { apple: { sku: productId } },
- type: 'in-app'
- });
-});`}
-
-
-
- Android: Support for `google` field (openiap-google v1.3.15)
-
-
-
- The Android library now supports the google field in
- request parameters, with fallback to the deprecated{' '}
- android{' '}
- field for backward compatibility.
-
-
- {`// Recommended (new)
-requestPurchase(RequestPurchaseProps(
- request = RequestPurchaseProps.Request.Purchase(
- RequestPurchasePropsByPlatforms(
- google = RequestPurchaseAndroidProps(skus = listOf("sku_id"))
- )
- ),
- type = ProductQueryType.InApp
-))
-
-// Still supported (deprecated)
-requestPurchase(RequestPurchaseProps(
- request = RequestPurchaseProps.Request.Purchase(
- RequestPurchasePropsByPlatforms(
- android = RequestPurchaseAndroidProps(skus = listOf("sku_id"))
- )
- ),
- type = ProductQueryType.InApp
-))`}
-
+
+
+ Added support for StoreKit 2's Product.PurchaseOption.custom API to pass attribution data during purchases.
+
+
+
+
1. advancedCommerceData Field
+
+ - New optional field in
RequestPurchaseIosProps and RequestSubscriptionIosProps
+ - Use cases: Campaign attribution, affiliate marketing, promotional code tracking
+
+
+
+
+
2. Deprecated requestPurchaseOnPromotedProductIOS()
+
+ In StoreKit 2, use promotedProductListenerIOS + requestPurchase() directly.
+
+
+
+
+
3. Android: google Field Support
+
+ Now supports google field with fallback to deprecated android field.
+
+
),
},
@@ -1089,45 +639,16 @@ requestPurchase(RequestPurchaseProps(
📅 openiap-gql v1.3.5 / openiap-apple v1.3.5 - GitHub Release Tag Management Update
-
- GitHub Release Tag Naming Convention:
-
-
- No API changes in this release. This update focuses on GitHub
- release tag management for better Swift Package Manager (SPM)
- compatibility.
+
+
+ No API changes. Updated GitHub release tag management for Swift Package Manager (SPM) compatibility.
-
- -
- Apple (openiap-apple): Uses semantic version tags
- directly (e.g.,
1.3.5) - Required for SPM to
- recognize package versions
-
- -
- GQL (openiap-gql): Uses
gql- prefix
- (e.g., gql-1.3.5)
-
- -
- Google (openiap-google): Uses{' '}
-
google- prefix (e.g., google-1.3.5)
-
+
+
+ - Apple: Uses semver tags directly (e.g.,
1.3.5) - Required for SPM
+ - GQL: Uses
gql- prefix (e.g., gql-1.3.5)
+ - Google: Uses
google- prefix (e.g., google-1.3.5)
-
- Swift Package Manager Integration:
-
-
-
- SPM
- {' '}
- requires semver-only tags (without prefixes) to properly resolve
- package versions. The Apple package now uses direct version tags
- (e.g., 1.3.5) instead of prefixed tags (e.g.,{' '}
- apple-v1.3.5).
-
),
},
@@ -1139,77 +660,21 @@ requestPurchase(RequestPurchaseProps(
element: (
- 📅 openiap-gql v1.3.4 / openiap-google v1.3.14 / openiap-apple v1.3.2 - Platform-Specific Verification Options
+ 📅 openiap-gql v1.3.4 / openiap-google v1.3.14 / openiap-apple v1.3.2 - Platform-Specific Verification
-
- verifyPurchase API Refactored (Breaking Change):
-
-
- The verifyPurchase API now requires platform-specific
- options for Apple, Google, and Meta Horizon stores. The{' '}
- sku field has been moved inside each platform-specific
- options object.
-
-
- -
-
-
VerifyPurchaseAppleOptions
- {' '}
- - Apple App Store verification with sku
-
- -
-
-
VerifyPurchaseGoogleOptions
- {' '}
- - Google Play verification with sku, packageName, purchaseToken,
- and accessToken
-
- -
-
-
VerifyPurchaseHorizonOptions
- {' '}
- - Meta Horizon (Quest) verification via S2S API with sku, userId,
- and accessToken
-
-
-
- New VerifyPurchaseProps Structure:
-
-
- {`// Platform-specific verification
-verifyPurchase({
- apple: { sku: 'premium_monthly' }, // iOS App Store
- google: { // Google Play
- sku: 'premium_monthly',
- packageName: 'com.example.app',
- purchaseToken: 'token...',
- accessToken: 'oauth_token...',
- isSub: true
- },
- horizon: { // Meta Quest
- sku: '50_gems',
- userId: '123456789',
- accessToken: 'OC|app_id|app_secret'
- }
-})`}
-
-
- Breaking Changes:
+
+
+ verifyPurchase API refactored (Breaking Change). Now requires platform-specific options. sku moved inside each platform options.
-
- -
-
sku removed from VerifyPurchaseProps{' '}
- root level → Now inside each platform options
-
- -
-
androidOptions completely removed → Use{' '}
- google instead
-
+
+
+ VerifyPurchaseAppleOptions - Apple App Store verification
+ VerifyPurchaseGoogleOptions - Google Play with packageName, purchaseToken, accessToken
+ VerifyPurchaseHorizonOptions - Meta Horizon (Quest) via S2S API
+ androidOptions → Use google instead
-
- See: verifyPurchase API,{' '}
- VerifyPurchaseProps
-
+
+ See: verifyPurchase API
),
},
@@ -1221,81 +686,20 @@ verifyPurchase({
element: (
- 📅 openiap-google v1.3.12 / openiap-gql v1.3.2 -{' '}
-
- Google Play Billing 8.2.0
- {' '}
- Billing Programs API
+ 📅 openiap-google v1.3.12 / openiap-gql v1.3.2 - Google Play Billing 8.2.0 Billing Programs API
-
- New Billing Programs API (8.2.0+):
-
-
- -
-
-
enableBillingProgram()
- {' '}
- - Enable a billing program before initConnection()
-
- -
-
-
isBillingProgramAvailable()
- {' '}
- - Check if a billing program is available (replaces{' '}
- checkAlternativeBillingAvailability())
-
- -
-
-
createBillingProgramReportingDetails()
- {' '}
- - Create reporting details with token (replaces{' '}
- createAlternativeBillingReportingToken())
-
- -
-
-
launchExternalLink()
- {' '}
- - Launch external link for external offers (replaces{' '}
- showAlternativeBillingInformationDialog())
-
-
-
- Deprecated APIs:
+
+
+ New Billing Programs API (8.2.0+) and deprecated alternative billing APIs.
-
- -
-
- checkAlternativeBillingAvailability()
- {' '}
- → Use isBillingProgramAvailable()
-
- -
-
- showAlternativeBillingInformationDialog()
- {' '}
- → Use launchExternalLink()
-
- -
-
- createAlternativeBillingReportingToken()
- {' '}
- → Use createBillingProgramReportingDetails()
-
+
+
+ enableBillingProgram(), isBillingProgramAvailable(), createBillingProgramReportingDetails(), launchExternalLink()
+ checkAlternativeBillingAvailability() → isBillingProgramAvailable()
+ showAlternativeBillingInformationDialog() → launchExternalLink()
-
- See:{' '}
-
- External Purchase Guide
-
- ,{' '}
-
- Subscription Upgrade/Downgrade
-
-
+
+ See: External Purchase Guide
),
},
@@ -1307,56 +711,18 @@ verifyPurchase({
element: (
- 📅 openiap-google v1.3.11 / openiap-gql v1.3.1 -{' '}
-
- Google Play Billing 8.1.0
- {' '}
- Support
+ 📅 openiap-google v1.3.11 / openiap-gql v1.3.1 - Google Play Billing 8.1.0
-
- Google Play Billing Library Upgrade:
-
-
- -
- Billing Library 8.0.0 → 8.1.0 - Upgraded to
- latest Google Play Billing Library
-
- -
- minSdk 21 → 23 - Minimum SDK increased to Android
- 6.0 (Marshmallow) as required by Billing Library 8.1.0
-
- -
- Kotlin 2.0.21 → 2.2.0 - Upgraded Kotlin version
- for compatibility
-
-
-
- New Features:
+
+
+ Billing Library 8.0.0 → 8.1.0, minSdk 21 → 23, Kotlin 2.0.21 → 2.2.0.
-
- -
- isSuspendedAndroid - New field on{' '}
-
PurchaseAndroid to detect suspended subscriptions due
- to payment failures.
-
- -
- PreorderDetailsAndroid - New type for pre-order
- products.
-
- -
- oneTimePurchaseOfferDetailsAndroid - Changed from
- single object to array type.
-
+
+
+ isSuspendedAndroid - Detect suspended subscriptions due to payment failures
+ PreorderDetailsAndroid - New type for pre-order products
+ oneTimePurchaseOfferDetailsAndroid - Changed to array type
-
- See:{' '}
- Purchase Platform Fields
- , Product Platform Fields
-
),
},
@@ -1370,45 +736,14 @@ verifyPurchase({
📅 openiap v1.3.0 - Platform Props & Store Field Updates
-
- Breaking Changes:
-
-
- -
-
-
- Purchase.platform
- {' '}
- → Purchase.store
- {' '}
- - The{' '}
- platform{' '}
- field is deprecated. Use store instead which returns{' '}
- 'apple' or 'google'.
-
- -
- requestPurchase props - The{' '}
-
ios and{' '}
- android{' '}
- props are deprecated. Use apple and{' '}
- google instead.
-
-
-
- New Feature:
+
+
+ Breaking Changes: Purchase.platform → store, ios/android props → apple/google.
-
- -
- verifyPurchaseWithProvider - New API for purchase
- verification with external providers like IAPKit.
-
+
+
+ - New:
verifyPurchaseWithProvider - Verification with external providers like IAPKit
-
- See:{' '}
-
- verifyPurchaseWithProvider API
-
-
),
},
@@ -1420,38 +755,11 @@ verifyPurchase({
element: (
- 📅 openiap v1.2.6 - validateReceipt → verifyPurchase
+ 📅 openiap v1.2.6 - validateReceipt → verifyPurchase
-
- Starting from openiap v1.2.6, the{' '}
-
- validateReceipt
- {' '}
- API is deprecated in favor of verifyPurchase.
-
-
- Why the change?
-
-
- -
- Terminology alignment - "Receipt Validation" was
- Apple's legacy term from StoreKit 1.
-
- -
- Cross-platform consistency - Android never used
- "receipt" terminology.
-
- -
- Modern API design - Unified interface that works
- consistently across iOS and Android.
-
-
-
- See:{' '}
-
- Purchase Verification
-
- , verifyPurchase API
+
+
+ Terminology alignment with modern StoreKit 2. "Receipt Validation" was Apple's legacy term. Unified interface across iOS and Android.
),
@@ -1466,26 +774,9 @@ verifyPurchase({
📅 openiap v1.2.0 - Version Alignment & Alternative Billing
-
- Version jumped directly from 1.0.12 to{' '}
- 1.2.0 to align with native libraries (iOS/Android)
- that were evolving rapidly.
-
-
- -
- iOS External Purchase - StoreKit External
- Purchase API support
-
- -
- Android Alternative Billing - Google Play
- Alternative Billing support
-
-
-
- See:{' '}
-
- External Purchase Guide
-
+
+
+ Version jumped from 1.0.12 to 1.2.0 to align with native libraries. iOS External Purchase & Android Alternative Billing support.
),
@@ -1500,34 +791,14 @@ verifyPurchase({
📅 openiap-gql 1.0.12 - External Purchase Support
-
- External purchase/alternative billing support for iOS and Android.
+
+
+ iOS External Purchase (iOS 17.4+, 18.2+) and Android Alternative Billing (Billing Library 6.2+/7.0+).
-
- -
- iOS External Purchase - StoreKit External
- Purchase API support (iOS 17.4+, iOS 18.2+ recommended)
-
- -
- Android Alternative Billing - Google Play
- Alternative Billing support (Billing Library 6.2+/7.0+)
-
- -
-
canPresentExternalPurchaseNoticeIOS(),{' '}
- presentExternalPurchaseNoticeSheetIOS(),{' '}
- presentExternalPurchaseLinkIOS() - iOS 18.2+ APIs
-
+
+
+ canPresentExternalPurchaseNoticeIOS(), presentExternalPurchaseNoticeSheetIOS(), presentExternalPurchaseLinkIOS()
-
- See:{' '}
-
- External Purchase Guide
-
- ,{' '}
-
- User Choice Billing Event
-
-
),
},
@@ -1541,28 +812,10 @@ verifyPurchase({
📅 August 2025 - Subscription Status APIs
-
- New standardized APIs for checking subscription status across
- platforms.
+
+
+ New standardized APIs: getActiveSubscriptions(), hasActiveSubscriptions() - automatic detection without requiring product IDs.
-
- -
-
getActiveSubscriptions() - Get detailed information
- about active subscriptions
-
- -
-
hasActiveSubscriptions() - Simple boolean check for
- subscription status
-
- -
- Automatic detection of all active subscriptions without requiring
- product IDs
-
- -
- Platform-specific details (iOS expiration dates, Android
- auto-renewal status)
-
-
),
},
@@ -1576,14 +829,10 @@ verifyPurchase({
📅 August 31, 2024 - Billing Library v5 Deprecated
- All apps must use Google Play Billing Library v6.0.1 or later.
-
- -
- Migration deadline: August 31, 2024 (extended to November 1, 2024)
-
- - New apps must use v6+ immediately
- - Existing apps must update before deadline
-
+
+
+ All apps must use Google Play Billing Library v6.0.1 or later. Deadline extended to November 1, 2024.
+
),
},
diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
index f6618524..e438f206 100644
--- a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
+++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
@@ -1424,6 +1424,12 @@ public data class DiscountOffer(
* Numeric price value
*/
val price: Double,
+ /**
+ * [Android] Purchase option ID for this offer.
+ * Used to identify which purchase option the user selected.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val purchaseOptionIdAndroid: String? = null,
/**
* [Android] Rental details if this is a rental offer.
*/
@@ -1454,6 +1460,7 @@ public data class DiscountOffer(
percentageDiscountAndroid = (json["percentageDiscountAndroid"] as? Number)?.toInt(),
preorderDetailsAndroid = (json["preorderDetailsAndroid"] as? Map)?.let { PreorderDetailsAndroid.fromJson(it) },
price = (json["price"] as? Number)?.toDouble() ?: 0.0,
+ purchaseOptionIdAndroid = json["purchaseOptionIdAndroid"] as? String,
rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) },
type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory,
validTimeWindowAndroid = (json["validTimeWindowAndroid"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) },
@@ -1475,6 +1482,7 @@ public data class DiscountOffer(
"percentageDiscountAndroid" to percentageDiscountAndroid,
"preorderDetailsAndroid" to preorderDetailsAndroid?.toJson(),
"price" to price,
+ "purchaseOptionIdAndroid" to purchaseOptionIdAndroid,
"rentalDetailsAndroid" to rentalDetailsAndroid?.toJson(),
"type" to type.toJson(),
"validTimeWindowAndroid" to validTimeWindowAndroid?.toJson(),
@@ -1745,6 +1753,43 @@ public data class FetchProductsResultProducts(val value: List?) : Fetch
public data class FetchProductsResultSubscriptions(val value: List?) : FetchProductsResult
+/**
+ * Installment plan details for subscription offers (Android)
+ * Contains information about the installment plan commitment.
+ * Available in Google Play Billing Library 7.0+
+ */
+public data class InstallmentPlanDetailsAndroid(
+ /**
+ * Committed payments count after a user signs up for this subscription plan.
+ * For example, for a monthly subscription with commitmentPaymentsCount of 12,
+ * users will be charged monthly for 12 months after signup.
+ */
+ val commitmentPaymentsCount: Int,
+ /**
+ * Subsequent committed payments count after the subscription plan renews.
+ * For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12,
+ * users will be committed to another 12 monthly payments when the plan renews.
+ * Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan).
+ */
+ val subsequentCommitmentPaymentsCount: Int
+) {
+
+ companion object {
+ fun fromJson(json: Map): InstallmentPlanDetailsAndroid {
+ return InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = (json["commitmentPaymentsCount"] as? Number)?.toInt() ?: 0,
+ subsequentCommitmentPaymentsCount = (json["subsequentCommitmentPaymentsCount"] as? Number)?.toInt() ?: 0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "InstallmentPlanDetailsAndroid",
+ "commitmentPaymentsCount" to commitmentPaymentsCount,
+ "subsequentCommitmentPaymentsCount" to subsequentCommitmentPaymentsCount,
+ )
+}
+
/**
* Limited quantity information for one-time purchase offers (Android)
* Available in Google Play Billing Library 7.0+
@@ -1776,6 +1821,42 @@ public data class LimitedQuantityInfoAndroid(
)
}
+/**
+ * Pending purchase update for subscription upgrades/downgrades (Android)
+ * When a user initiates a subscription change (upgrade/downgrade), the new purchase
+ * may be pending until the current billing period ends. This type contains the
+ * details of the pending change.
+ * Available in Google Play Billing Library 5.0+
+ */
+public data class PendingPurchaseUpdateAndroid(
+ /**
+ * Product IDs for the pending purchase update.
+ * These are the new products the user is switching to.
+ */
+ val products: List,
+ /**
+ * Purchase token for the pending transaction.
+ * Use this token to track or manage the pending purchase update.
+ */
+ val purchaseToken: String
+) {
+
+ companion object {
+ fun fromJson(json: Map): PendingPurchaseUpdateAndroid {
+ return PendingPurchaseUpdateAndroid(
+ products = (json["products"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList(),
+ purchaseToken = json["purchaseToken"] as? String ?: "",
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "PendingPurchaseUpdateAndroid",
+ "products" to products,
+ "purchaseToken" to purchaseToken,
+ )
+}
+
/**
* Pre-order details for one-time purchase products (Android)
* Available in Google Play Billing Library 8.1.0+
@@ -1989,6 +2070,12 @@ public data class ProductAndroidOneTimePurchaseOfferDetail(
val preorderDetailsAndroid: PreorderDetailsAndroid? = null,
val priceAmountMicros: String,
val priceCurrencyCode: String,
+ /**
+ * Purchase option ID for this offer (Android)
+ * Used to identify which purchase option the user selected.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val purchaseOptionId: String? = null,
/**
* Rental details for rental offers
*/
@@ -2012,6 +2099,7 @@ public data class ProductAndroidOneTimePurchaseOfferDetail(
preorderDetailsAndroid = (json["preorderDetailsAndroid"] as? Map)?.let { PreorderDetailsAndroid.fromJson(it) },
priceAmountMicros = json["priceAmountMicros"] as? String ?: "",
priceCurrencyCode = json["priceCurrencyCode"] as? String ?: "",
+ purchaseOptionId = json["purchaseOptionId"] as? String,
rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) },
validTimeWindow = (json["validTimeWindow"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) },
)
@@ -2030,6 +2118,7 @@ public data class ProductAndroidOneTimePurchaseOfferDetail(
"preorderDetailsAndroid" to preorderDetailsAndroid?.toJson(),
"priceAmountMicros" to priceAmountMicros,
"priceCurrencyCode" to priceCurrencyCode,
+ "purchaseOptionId" to purchaseOptionId,
"rentalDetailsAndroid" to rentalDetailsAndroid?.toJson(),
"validTimeWindow" to validTimeWindow?.toJson(),
)
@@ -2202,6 +2291,12 @@ public data class ProductSubscriptionAndroid(
*/
public data class ProductSubscriptionAndroidOfferDetails(
val basePlanId: String,
+ /**
+ * Installment plan details for this subscription offer.
+ * Only set for installment subscription plans; null for non-installment plans.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val installmentPlanDetails: InstallmentPlanDetailsAndroid? = null,
val offerId: String? = null,
val offerTags: List,
val offerToken: String,
@@ -2212,6 +2307,7 @@ public data class ProductSubscriptionAndroidOfferDetails(
fun fromJson(json: Map): ProductSubscriptionAndroidOfferDetails {
return ProductSubscriptionAndroidOfferDetails(
basePlanId = json["basePlanId"] as? String ?: "",
+ installmentPlanDetails = (json["installmentPlanDetails"] as? Map)?.let { InstallmentPlanDetailsAndroid.fromJson(it) },
offerId = json["offerId"] as? String,
offerTags = (json["offerTags"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList(),
offerToken = json["offerToken"] as? String ?: "",
@@ -2223,6 +2319,7 @@ public data class ProductSubscriptionAndroidOfferDetails(
fun toJson(): Map = mapOf(
"__typename" to "ProductSubscriptionAndroidOfferDetails",
"basePlanId" to basePlanId,
+ "installmentPlanDetails" to installmentPlanDetails?.toJson(),
"offerId" to offerId,
"offerTags" to offerTags,
"offerToken" to offerToken,
@@ -2348,6 +2445,13 @@ public data class PurchaseAndroid(
val obfuscatedAccountIdAndroid: String? = null,
val obfuscatedProfileIdAndroid: String? = null,
val packageNameAndroid: String? = null,
+ /**
+ * Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ * Contains the new products and purchase token for the pending transaction.
+ * Returns null if no pending update exists.
+ * Available in Google Play Billing Library 5.0+
+ */
+ val pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid? = null,
override val platform: IapPlatform,
override val productId: String,
override val purchaseState: PurchaseState,
@@ -2377,6 +2481,7 @@ public data class PurchaseAndroid(
obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String,
obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String,
packageNameAndroid = json["packageNameAndroid"] as? String,
+ pendingPurchaseUpdateAndroid = (json["pendingPurchaseUpdateAndroid"] as? Map)?.let { PendingPurchaseUpdateAndroid.fromJson(it) },
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
productId = json["productId"] as? String ?: "",
purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Pending,
@@ -2404,6 +2509,7 @@ public data class PurchaseAndroid(
"obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid,
"obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid,
"packageNameAndroid" to packageNameAndroid,
+ "pendingPurchaseUpdateAndroid" to pendingPurchaseUpdateAndroid?.toJson(),
"platform" to platform.toJson(),
"productId" to productId,
"purchaseState" to purchaseState.toJson(),
@@ -2814,6 +2920,12 @@ public data class SubscriptionOffer(
* - Android: offerId from ProductSubscriptionAndroidOfferDetails
*/
val id: String,
+ /**
+ * [Android] Installment plan details for this subscription offer.
+ * Only set for installment subscription plans; null for non-installment plans.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null,
/**
* [iOS] Key identifier for signature validation.
* Used with server-side signature generation for promotional offers.
@@ -2885,6 +2997,7 @@ public data class SubscriptionOffer(
currency = json["currency"] as? String,
displayPrice = json["displayPrice"] as? String ?: "",
id = json["id"] as? String ?: "",
+ installmentPlanDetailsAndroid = (json["installmentPlanDetailsAndroid"] as? Map)?.let { InstallmentPlanDetailsAndroid.fromJson(it) },
keyIdentifierIOS = json["keyIdentifierIOS"] as? String,
localizedPriceIOS = json["localizedPriceIOS"] as? String,
nonceIOS = json["nonceIOS"] as? String,
@@ -2909,6 +3022,7 @@ public data class SubscriptionOffer(
"currency" to currency,
"displayPrice" to displayPrice,
"id" to id,
+ "installmentPlanDetailsAndroid" to installmentPlanDetailsAndroid?.toJson(),
"keyIdentifierIOS" to keyIdentifierIOS,
"localizedPriceIOS" to localizedPriceIOS,
"nonceIOS" to nonceIOS,
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
index b2f6ab66..d0acafd3 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
@@ -7,8 +7,10 @@ import dev.hyo.openiap.DiscountOffer
import dev.hyo.openiap.DiscountOfferType
import dev.hyo.openiap.IapPlatform
import dev.hyo.openiap.IapStore
+import dev.hyo.openiap.InstallmentPlanDetailsAndroid
import dev.hyo.openiap.LimitedQuantityInfoAndroid
import dev.hyo.openiap.PaymentMode
+import dev.hyo.openiap.PendingPurchaseUpdateAndroid
import dev.hyo.openiap.PricingPhaseAndroid
import dev.hyo.openiap.PricingPhasesAndroid
import dev.hyo.openiap.Product
@@ -103,6 +105,9 @@ internal object BillingConverters {
)
}
+ // Extract purchase option ID if available (Billing Library 7.0+)
+ val purchaseOptId = runCatching { purchaseOptionId }.getOrNull()
+
return ProductAndroidOneTimePurchaseOfferDetail(
offerId = runCatching { offerId }.getOrNull(),
offerToken = offerToken ?: "",
@@ -115,7 +120,8 @@ internal object BillingConverters {
validTimeWindow = timeWindow,
limitedQuantityInfo = quantityInfo,
preorderDetailsAndroid = preorder,
- rentalDetailsAndroid = rental
+ rentalDetailsAndroid = rental,
+ purchaseOptionId = purchaseOptId
)
}
@@ -161,7 +167,8 @@ internal object BillingConverters {
rentalPeriod = details.rentalPeriod,
rentalExpirationPeriod = runCatching { details.rentalExpirationPeriod }.getOrNull()
)
- }
+ },
+ purchaseOptionIdAndroid = runCatching { purchaseOptionId }.getOrNull()
)
}
@@ -239,6 +246,14 @@ internal object BillingConverters {
else -> DiscountOfferType.Introductory
}
+ // Extract installment plan details if available (Billing Library 7.0+)
+ val installmentDetails = runCatching { installmentPlanDetails }?.getOrNull()?.let { details ->
+ InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = details.installmentPlanCommitmentPaymentsCount,
+ subsequentCommitmentPaymentsCount = details.subsequentInstallmentPlanCommitmentPaymentsCount
+ )
+ }
+
return SubscriptionOffer(
id = offerId ?: basePlanId,
displayPrice = displayPrice,
@@ -262,7 +277,8 @@ internal object BillingConverters {
recurrenceMode = phase.recurrenceMode
)
}
- )
+ ),
+ installmentPlanDetailsAndroid = installmentDetails
)
}
@@ -320,6 +336,14 @@ internal object BillingConverters {
// Convert to deprecated format (for backwards compatibility)
val pricingDetails = offers.map { offer ->
+ // Extract installment plan details if available (Billing Library 7.0+)
+ val installmentDetails = runCatching { offer.installmentPlanDetails }?.getOrNull()?.let { details ->
+ InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = details.installmentPlanCommitmentPaymentsCount,
+ subsequentCommitmentPaymentsCount = details.subsequentInstallmentPlanCommitmentPaymentsCount
+ )
+ }
+
ProductSubscriptionAndroidOfferDetails(
basePlanId = offer.basePlanId,
offerId = offer.offerId,
@@ -336,7 +360,8 @@ internal object BillingConverters {
recurrenceMode = phase.recurrenceMode
)
}
- )
+ ),
+ installmentPlanDetails = installmentDetails
)
}
@@ -393,6 +418,14 @@ internal object BillingConverters {
null
}
+ // Extract pending purchase update for subscription upgrades/downgrades (Billing Library 5.0+)
+ val pendingUpdate = runCatching { pendingPurchaseUpdate }?.getOrNull()?.let { update ->
+ PendingPurchaseUpdateAndroid(
+ products = update.products,
+ purchaseToken = update.purchaseToken
+ )
+ }
+
return PurchaseAndroid(
autoRenewingAndroid = isAutoRenewing,
currentPlanId = basePlanId,
@@ -403,6 +436,7 @@ internal object BillingConverters {
isAcknowledgedAndroid = isAcknowledged,
isAutoRenewing = isAutoRenewing,
isSuspendedAndroid = isSuspended,
+ pendingPurchaseUpdateAndroid = pendingUpdate,
obfuscatedAccountIdAndroid = accountIdentifiers?.obfuscatedAccountId,
obfuscatedProfileIdAndroid = accountIdentifiers?.obfuscatedProfileId,
packageNameAndroid = packageName,
diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt
index c77a5787..d96d04fa 100644
--- a/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt
+++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/StandardizedOfferTypesTest.kt
@@ -1,6 +1,7 @@
package dev.hyo.openiap
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
@@ -523,4 +524,489 @@ class StandardizedOfferTypesTest {
assertEquals("consumable_gems", purchaseProps.skus.first())
assertEquals("flash_sale_token", purchaseProps.offerToken)
}
+
+ // MARK: - purchaseOptionIdAndroid Tests (Issue #77)
+
+ @Test
+ fun `DiscountOffer supports purchaseOptionIdAndroid`() {
+ val offer = DiscountOffer(
+ id = "option_offer",
+ displayPrice = "$4.99",
+ price = 4.99,
+ currency = "USD",
+ type = DiscountOfferType.OneTime,
+ offerTokenAndroid = "token_abc",
+ purchaseOptionIdAndroid = "purchase_option_001"
+ )
+
+ assertEquals("purchase_option_001", offer.purchaseOptionIdAndroid)
+ }
+
+ @Test
+ fun `DiscountOffer toJson includes purchaseOptionIdAndroid`() {
+ val offer = DiscountOffer(
+ id = "test_offer",
+ displayPrice = "$2.99",
+ price = 2.99,
+ currency = "USD",
+ type = DiscountOfferType.OneTime,
+ purchaseOptionIdAndroid = "option_xyz"
+ )
+
+ val json = offer.toJson()
+ assertEquals("option_xyz", json["purchaseOptionIdAndroid"])
+ }
+
+ @Test
+ fun `DiscountOffer fromJson parses purchaseOptionIdAndroid`() {
+ val json = mapOf(
+ "id" to "offer_100",
+ "displayPrice" to "$1.99",
+ "price" to 1.99,
+ "currency" to "USD",
+ "type" to "one-time",
+ "purchaseOptionIdAndroid" to "parsed_option_id"
+ )
+
+ val offer = DiscountOffer.fromJson(json)
+ assertEquals("parsed_option_id", offer.purchaseOptionIdAndroid)
+ }
+
+ @Test
+ fun `DiscountOffer allows null purchaseOptionIdAndroid`() {
+ val offer = DiscountOffer(
+ id = "basic_offer",
+ displayPrice = "$4.99",
+ price = 4.99,
+ currency = "USD",
+ type = DiscountOfferType.OneTime
+ )
+
+ assertNull(offer.purchaseOptionIdAndroid)
+ }
+
+ // MARK: - InstallmentPlanDetailsAndroid Tests
+
+ @Test
+ fun `InstallmentPlanDetailsAndroid creation and toJson`() {
+ val details = InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = 12,
+ subsequentCommitmentPaymentsCount = 12
+ )
+
+ assertEquals(12, details.commitmentPaymentsCount)
+ assertEquals(12, details.subsequentCommitmentPaymentsCount)
+
+ val json = details.toJson()
+ assertEquals(12, json["commitmentPaymentsCount"])
+ assertEquals(12, json["subsequentCommitmentPaymentsCount"])
+ }
+
+ @Test
+ fun `InstallmentPlanDetailsAndroid fromJson`() {
+ val json = mapOf(
+ "commitmentPaymentsCount" to 6,
+ "subsequentCommitmentPaymentsCount" to 0
+ )
+
+ val details = InstallmentPlanDetailsAndroid.fromJson(json)
+ assertEquals(6, details.commitmentPaymentsCount)
+ assertEquals(0, details.subsequentCommitmentPaymentsCount)
+ }
+
+ @Test
+ fun `InstallmentPlanDetailsAndroid zero subsequent means revert to normal plan`() {
+ val details = InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = 12,
+ subsequentCommitmentPaymentsCount = 0
+ )
+
+ // subsequentCommitmentPaymentsCount = 0 means plan reverts to normal upon renewal
+ assertEquals(0, details.subsequentCommitmentPaymentsCount)
+ }
+
+ // MARK: - SubscriptionOffer installmentPlanDetailsAndroid Tests
+
+ @Test
+ fun `SubscriptionOffer supports installmentPlanDetailsAndroid`() {
+ val installmentDetails = InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = 12,
+ subsequentCommitmentPaymentsCount = 12
+ )
+
+ val offer = SubscriptionOffer(
+ id = "installment_sub",
+ displayPrice = "$9.99/month",
+ price = 9.99,
+ currency = "USD",
+ type = DiscountOfferType.Introductory,
+ basePlanIdAndroid = "monthly_installment",
+ offerTokenAndroid = "install_token",
+ installmentPlanDetailsAndroid = installmentDetails
+ )
+
+ assertEquals(12, offer.installmentPlanDetailsAndroid?.commitmentPaymentsCount)
+ assertEquals(12, offer.installmentPlanDetailsAndroid?.subsequentCommitmentPaymentsCount)
+ }
+
+ @Test
+ fun `SubscriptionOffer toJson includes installmentPlanDetailsAndroid`() {
+ val offer = SubscriptionOffer(
+ id = "sub_with_installment",
+ displayPrice = "$5.99",
+ price = 5.99,
+ currency = "USD",
+ type = DiscountOfferType.Promotional,
+ installmentPlanDetailsAndroid = InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = 6,
+ subsequentCommitmentPaymentsCount = 6
+ )
+ )
+
+ val json = offer.toJson()
+ @Suppress("UNCHECKED_CAST")
+ val installmentJson = json["installmentPlanDetailsAndroid"] as? Map
+ assertEquals(6, installmentJson?.get("commitmentPaymentsCount"))
+ assertEquals(6, installmentJson?.get("subsequentCommitmentPaymentsCount"))
+ }
+
+ @Test
+ fun `SubscriptionOffer fromJson parses installmentPlanDetailsAndroid`() {
+ val json = mapOf(
+ "id" to "parsed_installment_offer",
+ "displayPrice" to "$7.99",
+ "price" to 7.99,
+ "currency" to "USD",
+ "type" to "introductory",
+ "installmentPlanDetailsAndroid" to mapOf(
+ "commitmentPaymentsCount" to 24,
+ "subsequentCommitmentPaymentsCount" to 12
+ )
+ )
+
+ val offer = SubscriptionOffer.fromJson(json)
+ assertEquals(24, offer.installmentPlanDetailsAndroid?.commitmentPaymentsCount)
+ assertEquals(12, offer.installmentPlanDetailsAndroid?.subsequentCommitmentPaymentsCount)
+ }
+
+ @Test
+ fun `SubscriptionOffer allows null installmentPlanDetailsAndroid`() {
+ val offer = SubscriptionOffer(
+ id = "regular_sub",
+ displayPrice = "$9.99",
+ price = 9.99,
+ currency = "USD",
+ type = DiscountOfferType.Introductory
+ )
+
+ assertNull(offer.installmentPlanDetailsAndroid)
+ }
+
+ // MARK: - ProductAndroidOneTimePurchaseOfferDetail purchaseOptionId Tests
+
+ @Test
+ fun `ProductAndroidOneTimePurchaseOfferDetail supports purchaseOptionId`() {
+ val offerDetail = ProductAndroidOneTimePurchaseOfferDetail(
+ offerId = "offer_001",
+ offerToken = "token_abc",
+ offerTags = listOf("sale"),
+ formattedPrice = "$4.99",
+ priceAmountMicros = "4990000",
+ priceCurrencyCode = "USD",
+ purchaseOptionId = "purchase_opt_xyz"
+ )
+
+ assertEquals("purchase_opt_xyz", offerDetail.purchaseOptionId)
+ }
+
+ @Test
+ fun `ProductAndroidOneTimePurchaseOfferDetail toJson includes purchaseOptionId`() {
+ val offerDetail = ProductAndroidOneTimePurchaseOfferDetail(
+ offerId = "offer_002",
+ offerToken = "token_def",
+ offerTags = emptyList(),
+ formattedPrice = "$2.99",
+ priceAmountMicros = "2990000",
+ priceCurrencyCode = "USD",
+ purchaseOptionId = "opt_id_123"
+ )
+
+ val json = offerDetail.toJson()
+ assertEquals("opt_id_123", json["purchaseOptionId"])
+ }
+
+ @Test
+ fun `ProductAndroidOneTimePurchaseOfferDetail fromJson parses purchaseOptionId`() {
+ val json = mapOf(
+ "offerId" to "parsed_offer",
+ "offerToken" to "parsed_token",
+ "offerTags" to listOf("tag1"),
+ "formattedPrice" to "$1.99",
+ "priceAmountMicros" to "1990000",
+ "priceCurrencyCode" to "EUR",
+ "purchaseOptionId" to "parsed_purchase_option"
+ )
+
+ val offerDetail = ProductAndroidOneTimePurchaseOfferDetail.fromJson(json)
+ assertEquals("parsed_purchase_option", offerDetail.purchaseOptionId)
+ }
+
+ // MARK: - ProductSubscriptionAndroidOfferDetails installmentPlanDetails Tests
+
+ @Test
+ fun `ProductSubscriptionAndroidOfferDetails supports installmentPlanDetails`() {
+ val pricingPhases = PricingPhasesAndroid(
+ pricingPhaseList = listOf(
+ PricingPhaseAndroid(
+ billingCycleCount = 0,
+ billingPeriod = "P1M",
+ formattedPrice = "$9.99",
+ priceAmountMicros = "9990000",
+ priceCurrencyCode = "USD",
+ recurrenceMode = 1
+ )
+ )
+ )
+
+ val offerDetails = ProductSubscriptionAndroidOfferDetails(
+ basePlanId = "monthly_installment",
+ offerId = null,
+ offerToken = "install_token",
+ offerTags = listOf("installment"),
+ pricingPhases = pricingPhases,
+ installmentPlanDetails = InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = 12,
+ subsequentCommitmentPaymentsCount = 0
+ )
+ )
+
+ assertEquals(12, offerDetails.installmentPlanDetails?.commitmentPaymentsCount)
+ assertEquals(0, offerDetails.installmentPlanDetails?.subsequentCommitmentPaymentsCount)
+ }
+
+ @Test
+ fun `ProductSubscriptionAndroidOfferDetails toJson includes installmentPlanDetails`() {
+ val pricingPhases = PricingPhasesAndroid(
+ pricingPhaseList = listOf(
+ PricingPhaseAndroid(
+ billingCycleCount = 0,
+ billingPeriod = "P1M",
+ formattedPrice = "$7.99",
+ priceAmountMicros = "7990000",
+ priceCurrencyCode = "USD",
+ recurrenceMode = 1
+ )
+ )
+ )
+
+ val offerDetails = ProductSubscriptionAndroidOfferDetails(
+ basePlanId = "yearly_base",
+ offerId = "promo_offer",
+ offerToken = "promo_token",
+ offerTags = emptyList(),
+ pricingPhases = pricingPhases,
+ installmentPlanDetails = InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = 6,
+ subsequentCommitmentPaymentsCount = 6
+ )
+ )
+
+ val json = offerDetails.toJson()
+ @Suppress("UNCHECKED_CAST")
+ val installmentJson = json["installmentPlanDetails"] as? Map
+ assertEquals(6, installmentJson?.get("commitmentPaymentsCount"))
+ assertEquals(6, installmentJson?.get("subsequentCommitmentPaymentsCount"))
+ }
+
+ @Test
+ fun `ProductSubscriptionAndroidOfferDetails fromJson parses installmentPlanDetails`() {
+ val json = mapOf(
+ "basePlanId" to "parsed_base",
+ "offerId" to null,
+ "offerToken" to "parsed_token",
+ "offerTags" to listOf("monthly"),
+ "pricingPhases" to mapOf(
+ "pricingPhaseList" to listOf(
+ mapOf(
+ "billingCycleCount" to 0,
+ "billingPeriod" to "P1M",
+ "formattedPrice" to "$9.99",
+ "priceAmountMicros" to "9990000",
+ "priceCurrencyCode" to "USD",
+ "recurrenceMode" to 1
+ )
+ )
+ ),
+ "installmentPlanDetails" to mapOf(
+ "commitmentPaymentsCount" to 24,
+ "subsequentCommitmentPaymentsCount" to 12
+ )
+ )
+
+ val offerDetails = ProductSubscriptionAndroidOfferDetails.fromJson(json)
+ assertEquals(24, offerDetails.installmentPlanDetails?.commitmentPaymentsCount)
+ assertEquals(12, offerDetails.installmentPlanDetails?.subsequentCommitmentPaymentsCount)
+ }
+
+ // MARK: - PendingPurchaseUpdateAndroid Tests
+
+ @Test
+ fun `PendingPurchaseUpdateAndroid creation and toJson`() {
+ val pendingUpdate = PendingPurchaseUpdateAndroid(
+ products = listOf("premium_monthly", "premium_yearly"),
+ purchaseToken = "pending_token_abc123"
+ )
+
+ assertEquals(listOf("premium_monthly", "premium_yearly"), pendingUpdate.products)
+ assertEquals("pending_token_abc123", pendingUpdate.purchaseToken)
+
+ val json = pendingUpdate.toJson()
+ @Suppress("UNCHECKED_CAST")
+ assertEquals(listOf("premium_monthly", "premium_yearly"), json["products"] as List)
+ assertEquals("pending_token_abc123", json["purchaseToken"])
+ }
+
+ @Test
+ fun `PendingPurchaseUpdateAndroid fromJson`() {
+ val json = mapOf(
+ "products" to listOf("basic_plan", "pro_plan"),
+ "purchaseToken" to "token_xyz789"
+ )
+
+ val pendingUpdate = PendingPurchaseUpdateAndroid.fromJson(json)
+ assertEquals(listOf("basic_plan", "pro_plan"), pendingUpdate.products)
+ assertEquals("token_xyz789", pendingUpdate.purchaseToken)
+ }
+
+ @Test
+ fun `PendingPurchaseUpdateAndroid single product upgrade`() {
+ val pendingUpdate = PendingPurchaseUpdateAndroid(
+ products = listOf("premium_yearly"),
+ purchaseToken = "upgrade_token"
+ )
+
+ // Single product upgrade scenario
+ assertEquals(1, pendingUpdate.products.size)
+ assertEquals("premium_yearly", pendingUpdate.products.first())
+ }
+
+ // MARK: - PurchaseAndroid with pendingPurchaseUpdateAndroid Tests
+
+ @Test
+ fun `PurchaseAndroid supports pendingPurchaseUpdateAndroid`() {
+ val pendingUpdate = PendingPurchaseUpdateAndroid(
+ products = listOf("premium_yearly"),
+ purchaseToken = "pending_upgrade_token"
+ )
+
+ val purchase = PurchaseAndroid(
+ id = "order_123",
+ productId = "premium_monthly",
+ transactionDate = 1700000000000.0,
+ purchaseToken = "current_token",
+ store = IapStore.Google,
+ platform = IapPlatform.Android,
+ quantity = 1,
+ purchaseState = PurchaseState.Purchased,
+ isAutoRenewing = true,
+ pendingPurchaseUpdateAndroid = pendingUpdate
+ )
+
+ assertNotNull(purchase.pendingPurchaseUpdateAndroid)
+ assertEquals(listOf("premium_yearly"), purchase.pendingPurchaseUpdateAndroid?.products)
+ assertEquals("pending_upgrade_token", purchase.pendingPurchaseUpdateAndroid?.purchaseToken)
+ }
+
+ @Test
+ fun `PurchaseAndroid toJson includes pendingPurchaseUpdateAndroid`() {
+ val purchase = PurchaseAndroid(
+ id = "order_456",
+ productId = "basic_plan",
+ transactionDate = 1700000000000.0,
+ store = IapStore.Google,
+ platform = IapPlatform.Android,
+ quantity = 1,
+ purchaseState = PurchaseState.Purchased,
+ isAutoRenewing = true,
+ pendingPurchaseUpdateAndroid = PendingPurchaseUpdateAndroid(
+ products = listOf("pro_plan"),
+ purchaseToken = "upgrade_token_789"
+ )
+ )
+
+ val json = purchase.toJson()
+ @Suppress("UNCHECKED_CAST")
+ val pendingJson = json["pendingPurchaseUpdateAndroid"] as? Map
+ assertNotNull(pendingJson)
+ assertEquals(listOf("pro_plan"), pendingJson?.get("products"))
+ assertEquals("upgrade_token_789", pendingJson?.get("purchaseToken"))
+ }
+
+ @Test
+ fun `PurchaseAndroid fromJson parses pendingPurchaseUpdateAndroid`() {
+ val json = mapOf(
+ "id" to "order_789",
+ "productId" to "starter_plan",
+ "transactionDate" to 1700000000000.0,
+ "store" to "google",
+ "platform" to "android",
+ "quantity" to 1,
+ "purchaseState" to "purchased",
+ "isAutoRenewing" to true,
+ "pendingPurchaseUpdateAndroid" to mapOf(
+ "products" to listOf("enterprise_plan"),
+ "purchaseToken" to "enterprise_upgrade_token"
+ )
+ )
+
+ val purchase = PurchaseAndroid.fromJson(json)
+ assertNotNull(purchase.pendingPurchaseUpdateAndroid)
+ assertEquals(listOf("enterprise_plan"), purchase.pendingPurchaseUpdateAndroid?.products)
+ assertEquals("enterprise_upgrade_token", purchase.pendingPurchaseUpdateAndroid?.purchaseToken)
+ }
+
+ @Test
+ fun `PurchaseAndroid allows null pendingPurchaseUpdateAndroid`() {
+ val purchase = PurchaseAndroid(
+ id = "order_no_pending",
+ productId = "regular_product",
+ transactionDate = 1700000000000.0,
+ store = IapStore.Google,
+ platform = IapPlatform.Android,
+ quantity = 1,
+ purchaseState = PurchaseState.Purchased,
+ isAutoRenewing = false
+ )
+
+ assertNull(purchase.pendingPurchaseUpdateAndroid)
+ }
+
+ @Test
+ fun `PurchaseAndroid pendingPurchaseUpdateAndroid use case - subscription downgrade`() {
+ // Scenario: User on yearly plan downgrades to monthly
+ // The downgrade is pending until the yearly period ends
+ val purchase = PurchaseAndroid(
+ id = "yearly_order",
+ productId = "premium_yearly",
+ transactionDate = 1700000000000.0,
+ store = IapStore.Google,
+ platform = IapPlatform.Android,
+ quantity = 1,
+ purchaseState = PurchaseState.Purchased,
+ isAutoRenewing = true,
+ currentPlanId = "yearly",
+ pendingPurchaseUpdateAndroid = PendingPurchaseUpdateAndroid(
+ products = listOf("premium_monthly"),
+ purchaseToken = "downgrade_pending_token"
+ )
+ )
+
+ // Current purchase is still yearly
+ assertEquals("premium_yearly", purchase.productId)
+ assertEquals("yearly", purchase.currentPlanId)
+
+ // But there's a pending downgrade to monthly
+ assertNotNull(purchase.pendingPurchaseUpdateAndroid)
+ assertEquals("premium_monthly", purchase.pendingPurchaseUpdateAndroid?.products?.first())
+ }
}
diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt
index 68e159ab..7f235187 100644
--- a/packages/gql/src/generated/Types.kt
+++ b/packages/gql/src/generated/Types.kt
@@ -1510,6 +1510,12 @@ public data class DiscountOffer(
* Numeric price value
*/
val price: Double,
+ /**
+ * [Android] Purchase option ID for this offer.
+ * Used to identify which purchase option the user selected.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val purchaseOptionIdAndroid: String? = null,
/**
* [Android] Rental details if this is a rental offer.
*/
@@ -1540,6 +1546,7 @@ public data class DiscountOffer(
percentageDiscountAndroid = (json["percentageDiscountAndroid"] as? Number)?.toInt(),
preorderDetailsAndroid = (json["preorderDetailsAndroid"] as? Map)?.let { PreorderDetailsAndroid.fromJson(it) },
price = (json["price"] as? Number)?.toDouble() ?: 0.0,
+ purchaseOptionIdAndroid = json["purchaseOptionIdAndroid"] as? String,
rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) },
type = (json["type"] as? String)?.let { DiscountOfferType.fromJson(it) } ?: DiscountOfferType.Introductory,
validTimeWindowAndroid = (json["validTimeWindowAndroid"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) },
@@ -1561,6 +1568,7 @@ public data class DiscountOffer(
"percentageDiscountAndroid" to percentageDiscountAndroid,
"preorderDetailsAndroid" to preorderDetailsAndroid?.toJson(),
"price" to price,
+ "purchaseOptionIdAndroid" to purchaseOptionIdAndroid,
"rentalDetailsAndroid" to rentalDetailsAndroid?.toJson(),
"type" to type.toJson(),
"validTimeWindowAndroid" to validTimeWindowAndroid?.toJson(),
@@ -1831,6 +1839,43 @@ public data class FetchProductsResultProducts(val value: List?) : Fetch
public data class FetchProductsResultSubscriptions(val value: List?) : FetchProductsResult
+/**
+ * Installment plan details for subscription offers (Android)
+ * Contains information about the installment plan commitment.
+ * Available in Google Play Billing Library 7.0+
+ */
+public data class InstallmentPlanDetailsAndroid(
+ /**
+ * Committed payments count after a user signs up for this subscription plan.
+ * For example, for a monthly subscription with commitmentPaymentsCount of 12,
+ * users will be charged monthly for 12 months after signup.
+ */
+ val commitmentPaymentsCount: Int,
+ /**
+ * Subsequent committed payments count after the subscription plan renews.
+ * For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12,
+ * users will be committed to another 12 monthly payments when the plan renews.
+ * Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan).
+ */
+ val subsequentCommitmentPaymentsCount: Int
+) {
+
+ companion object {
+ fun fromJson(json: Map): InstallmentPlanDetailsAndroid {
+ return InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount = (json["commitmentPaymentsCount"] as? Number)?.toInt() ?: 0,
+ subsequentCommitmentPaymentsCount = (json["subsequentCommitmentPaymentsCount"] as? Number)?.toInt() ?: 0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "InstallmentPlanDetailsAndroid",
+ "commitmentPaymentsCount" to commitmentPaymentsCount,
+ "subsequentCommitmentPaymentsCount" to subsequentCommitmentPaymentsCount,
+ )
+}
+
/**
* Limited quantity information for one-time purchase offers (Android)
* Available in Google Play Billing Library 7.0+
@@ -1862,6 +1907,42 @@ public data class LimitedQuantityInfoAndroid(
)
}
+/**
+ * Pending purchase update for subscription upgrades/downgrades (Android)
+ * When a user initiates a subscription change (upgrade/downgrade), the new purchase
+ * may be pending until the current billing period ends. This type contains the
+ * details of the pending change.
+ * Available in Google Play Billing Library 5.0+
+ */
+public data class PendingPurchaseUpdateAndroid(
+ /**
+ * Product IDs for the pending purchase update.
+ * These are the new products the user is switching to.
+ */
+ val products: List,
+ /**
+ * Purchase token for the pending transaction.
+ * Use this token to track or manage the pending purchase update.
+ */
+ val purchaseToken: String
+) {
+
+ companion object {
+ fun fromJson(json: Map): PendingPurchaseUpdateAndroid {
+ return PendingPurchaseUpdateAndroid(
+ products = (json["products"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList(),
+ purchaseToken = json["purchaseToken"] as? String ?: "",
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "PendingPurchaseUpdateAndroid",
+ "products" to products,
+ "purchaseToken" to purchaseToken,
+ )
+}
+
/**
* Pre-order details for one-time purchase products (Android)
* Available in Google Play Billing Library 8.1.0+
@@ -2075,6 +2156,12 @@ public data class ProductAndroidOneTimePurchaseOfferDetail(
val preorderDetailsAndroid: PreorderDetailsAndroid? = null,
val priceAmountMicros: String,
val priceCurrencyCode: String,
+ /**
+ * Purchase option ID for this offer (Android)
+ * Used to identify which purchase option the user selected.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val purchaseOptionId: String? = null,
/**
* Rental details for rental offers
*/
@@ -2098,6 +2185,7 @@ public data class ProductAndroidOneTimePurchaseOfferDetail(
preorderDetailsAndroid = (json["preorderDetailsAndroid"] as? Map)?.let { PreorderDetailsAndroid.fromJson(it) },
priceAmountMicros = json["priceAmountMicros"] as? String ?: "",
priceCurrencyCode = json["priceCurrencyCode"] as? String ?: "",
+ purchaseOptionId = json["purchaseOptionId"] as? String,
rentalDetailsAndroid = (json["rentalDetailsAndroid"] as? Map)?.let { RentalDetailsAndroid.fromJson(it) },
validTimeWindow = (json["validTimeWindow"] as? Map)?.let { ValidTimeWindowAndroid.fromJson(it) },
)
@@ -2116,6 +2204,7 @@ public data class ProductAndroidOneTimePurchaseOfferDetail(
"preorderDetailsAndroid" to preorderDetailsAndroid?.toJson(),
"priceAmountMicros" to priceAmountMicros,
"priceCurrencyCode" to priceCurrencyCode,
+ "purchaseOptionId" to purchaseOptionId,
"rentalDetailsAndroid" to rentalDetailsAndroid?.toJson(),
"validTimeWindow" to validTimeWindow?.toJson(),
)
@@ -2288,6 +2377,12 @@ public data class ProductSubscriptionAndroid(
*/
public data class ProductSubscriptionAndroidOfferDetails(
val basePlanId: String,
+ /**
+ * Installment plan details for this subscription offer.
+ * Only set for installment subscription plans; null for non-installment plans.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val installmentPlanDetails: InstallmentPlanDetailsAndroid? = null,
val offerId: String? = null,
val offerTags: List,
val offerToken: String,
@@ -2298,6 +2393,7 @@ public data class ProductSubscriptionAndroidOfferDetails(
fun fromJson(json: Map): ProductSubscriptionAndroidOfferDetails {
return ProductSubscriptionAndroidOfferDetails(
basePlanId = json["basePlanId"] as? String ?: "",
+ installmentPlanDetails = (json["installmentPlanDetails"] as? Map)?.let { InstallmentPlanDetailsAndroid.fromJson(it) },
offerId = json["offerId"] as? String,
offerTags = (json["offerTags"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList(),
offerToken = json["offerToken"] as? String ?: "",
@@ -2309,6 +2405,7 @@ public data class ProductSubscriptionAndroidOfferDetails(
fun toJson(): Map = mapOf(
"__typename" to "ProductSubscriptionAndroidOfferDetails",
"basePlanId" to basePlanId,
+ "installmentPlanDetails" to installmentPlanDetails?.toJson(),
"offerId" to offerId,
"offerTags" to offerTags,
"offerToken" to offerToken,
@@ -2434,6 +2531,13 @@ public data class PurchaseAndroid(
val obfuscatedAccountIdAndroid: String? = null,
val obfuscatedProfileIdAndroid: String? = null,
val packageNameAndroid: String? = null,
+ /**
+ * Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ * Contains the new products and purchase token for the pending transaction.
+ * Returns null if no pending update exists.
+ * Available in Google Play Billing Library 5.0+
+ */
+ val pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid? = null,
override val platform: IapPlatform,
override val productId: String,
override val purchaseState: PurchaseState,
@@ -2463,6 +2567,7 @@ public data class PurchaseAndroid(
obfuscatedAccountIdAndroid = json["obfuscatedAccountIdAndroid"] as? String,
obfuscatedProfileIdAndroid = json["obfuscatedProfileIdAndroid"] as? String,
packageNameAndroid = json["packageNameAndroid"] as? String,
+ pendingPurchaseUpdateAndroid = (json["pendingPurchaseUpdateAndroid"] as? Map)?.let { PendingPurchaseUpdateAndroid.fromJson(it) },
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
productId = json["productId"] as? String ?: "",
purchaseState = (json["purchaseState"] as? String)?.let { PurchaseState.fromJson(it) } ?: PurchaseState.Pending,
@@ -2490,6 +2595,7 @@ public data class PurchaseAndroid(
"obfuscatedAccountIdAndroid" to obfuscatedAccountIdAndroid,
"obfuscatedProfileIdAndroid" to obfuscatedProfileIdAndroid,
"packageNameAndroid" to packageNameAndroid,
+ "pendingPurchaseUpdateAndroid" to pendingPurchaseUpdateAndroid?.toJson(),
"platform" to platform.toJson(),
"productId" to productId,
"purchaseState" to purchaseState.toJson(),
@@ -2900,6 +3006,12 @@ public data class SubscriptionOffer(
* - Android: offerId from ProductSubscriptionAndroidOfferDetails
*/
val id: String,
+ /**
+ * [Android] Installment plan details for this subscription offer.
+ * Only set for installment subscription plans; null for non-installment plans.
+ * Available in Google Play Billing Library 7.0+
+ */
+ val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null,
/**
* [iOS] Key identifier for signature validation.
* Used with server-side signature generation for promotional offers.
@@ -2971,6 +3083,7 @@ public data class SubscriptionOffer(
currency = json["currency"] as? String,
displayPrice = json["displayPrice"] as? String ?: "",
id = json["id"] as? String ?: "",
+ installmentPlanDetailsAndroid = (json["installmentPlanDetailsAndroid"] as? Map)?.let { InstallmentPlanDetailsAndroid.fromJson(it) },
keyIdentifierIOS = json["keyIdentifierIOS"] as? String,
localizedPriceIOS = json["localizedPriceIOS"] as? String,
nonceIOS = json["nonceIOS"] as? String,
@@ -2995,6 +3108,7 @@ public data class SubscriptionOffer(
"currency" to currency,
"displayPrice" to displayPrice,
"id" to id,
+ "installmentPlanDetailsAndroid" to installmentPlanDetailsAndroid?.toJson(),
"keyIdentifierIOS" to keyIdentifierIOS,
"localizedPriceIOS" to localizedPriceIOS,
"nonceIOS" to nonceIOS,
diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift
index 9ab84e4b..182e96a5 100644
--- a/packages/gql/src/generated/Types.swift
+++ b/packages/gql/src/generated/Types.swift
@@ -610,6 +610,10 @@ public struct DiscountOffer: Codable {
public var preorderDetailsAndroid: PreorderDetailsAndroid?
/// Numeric price value
public var price: Double
+ /// [Android] Purchase option ID for this offer.
+ /// Used to identify which purchase option the user selected.
+ /// Available in Google Play Billing Library 7.0+
+ public var purchaseOptionIdAndroid: String?
/// [Android] Rental details if this is a rental offer.
public var rentalDetailsAndroid: RentalDetailsAndroid?
/// Type of discount offer
@@ -701,6 +705,21 @@ public enum FetchProductsResult {
case subscriptions([ProductSubscription]?)
}
+/// Installment plan details for subscription offers (Android)
+/// Contains information about the installment plan commitment.
+/// Available in Google Play Billing Library 7.0+
+public struct InstallmentPlanDetailsAndroid: Codable {
+ /// Committed payments count after a user signs up for this subscription plan.
+ /// For example, for a monthly subscription with commitmentPaymentsCount of 12,
+ /// users will be charged monthly for 12 months after signup.
+ public var commitmentPaymentsCount: Int
+ /// Subsequent committed payments count after the subscription plan renews.
+ /// For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12,
+ /// users will be committed to another 12 monthly payments when the plan renews.
+ /// Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan).
+ public var subsequentCommitmentPaymentsCount: Int
+}
+
/// Limited quantity information for one-time purchase offers (Android)
/// Available in Google Play Billing Library 7.0+
public struct LimitedQuantityInfoAndroid: Codable {
@@ -710,6 +729,20 @@ public struct LimitedQuantityInfoAndroid: Codable {
public var remainingQuantity: Int
}
+/// Pending purchase update for subscription upgrades/downgrades (Android)
+/// When a user initiates a subscription change (upgrade/downgrade), the new purchase
+/// may be pending until the current billing period ends. This type contains the
+/// details of the pending change.
+/// Available in Google Play Billing Library 5.0+
+public struct PendingPurchaseUpdateAndroid: Codable {
+ /// Product IDs for the pending purchase update.
+ /// These are the new products the user is switching to.
+ public var products: [String]
+ /// Purchase token for the pending transaction.
+ /// Use this token to track or manage the pending purchase update.
+ public var purchaseToken: String
+}
+
/// Pre-order details for one-time purchase products (Android)
/// Available in Google Play Billing Library 8.1.0+
public struct PreorderDetailsAndroid: Codable {
@@ -793,6 +826,10 @@ public struct ProductAndroidOneTimePurchaseOfferDetail: Codable {
public var preorderDetailsAndroid: PreorderDetailsAndroid?
public var priceAmountMicros: String
public var priceCurrencyCode: String
+ /// Purchase option ID for this offer (Android)
+ /// Used to identify which purchase option the user selected.
+ /// Available in Google Play Billing Library 7.0+
+ public var purchaseOptionId: String?
/// Rental details for rental offers
public var rentalDetailsAndroid: RentalDetailsAndroid?
/// Valid time window for the offer
@@ -862,6 +899,10 @@ public struct ProductSubscriptionAndroid: Codable, ProductCommon {
/// @see https://openiap.dev/docs/types#subscription-offer
public struct ProductSubscriptionAndroidOfferDetails: Codable {
public var basePlanId: String
+ /// Installment plan details for this subscription offer.
+ /// Only set for installment subscription plans; null for non-installment plans.
+ /// Available in Google Play Billing Library 7.0+
+ public var installmentPlanDetails: InstallmentPlanDetailsAndroid?
public var offerId: String?
public var offerTags: [String]
public var offerToken: String
@@ -918,6 +959,11 @@ public struct PurchaseAndroid: Codable, PurchaseCommon {
public var obfuscatedAccountIdAndroid: String?
public var obfuscatedProfileIdAndroid: String?
public var packageNameAndroid: String?
+ /// Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ /// Contains the new products and purchase token for the pending transaction.
+ /// Returns null if no pending update exists.
+ /// Available in Google Play Billing Library 5.0+
+ public var pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid?
public var platform: IapPlatform
public var productId: String
public var purchaseState: PurchaseState
@@ -1067,6 +1113,10 @@ public struct SubscriptionOffer: Codable {
/// - iOS: Discount identifier from App Store Connect
/// - Android: offerId from ProductSubscriptionAndroidOfferDetails
public var id: String
+ /// [Android] Installment plan details for this subscription offer.
+ /// Only set for installment subscription plans; null for non-installment plans.
+ /// Available in Google Play Billing Library 7.0+
+ public var installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid?
/// [iOS] Key identifier for signature validation.
/// Used with server-side signature generation for promotional offers.
public var keyIdentifierIOS: String?
diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart
index a1edb3fc..027ec81d 100644
--- a/packages/gql/src/generated/types.dart
+++ b/packages/gql/src/generated/types.dart
@@ -1359,6 +1359,7 @@ class DiscountOffer {
this.percentageDiscountAndroid,
this.preorderDetailsAndroid,
required this.price,
+ this.purchaseOptionIdAndroid,
this.rentalDetailsAndroid,
required this.type,
this.validTimeWindowAndroid,
@@ -1397,6 +1398,10 @@ class DiscountOffer {
final PreorderDetailsAndroid? preorderDetailsAndroid;
/// Numeric price value
final double price;
+ /// [Android] Purchase option ID for this offer.
+ /// Used to identify which purchase option the user selected.
+ /// Available in Google Play Billing Library 7.0+
+ final String? purchaseOptionIdAndroid;
/// [Android] Rental details if this is a rental offer.
final RentalDetailsAndroid? rentalDetailsAndroid;
/// Type of discount offer
@@ -1419,6 +1424,7 @@ class DiscountOffer {
percentageDiscountAndroid: json['percentageDiscountAndroid'] as int?,
preorderDetailsAndroid: json['preorderDetailsAndroid'] != null ? PreorderDetailsAndroid.fromJson(json['preorderDetailsAndroid'] as Map) : null,
price: (json['price'] as num).toDouble(),
+ purchaseOptionIdAndroid: json['purchaseOptionIdAndroid'] as String?,
rentalDetailsAndroid: json['rentalDetailsAndroid'] != null ? RentalDetailsAndroid.fromJson(json['rentalDetailsAndroid'] as Map) : null,
type: DiscountOfferType.fromJson(json['type'] as String),
validTimeWindowAndroid: json['validTimeWindowAndroid'] != null ? ValidTimeWindowAndroid.fromJson(json['validTimeWindowAndroid'] as Map) : null,
@@ -1440,6 +1446,7 @@ class DiscountOffer {
'percentageDiscountAndroid': percentageDiscountAndroid,
'preorderDetailsAndroid': preorderDetailsAndroid?.toJson(),
'price': price,
+ 'purchaseOptionIdAndroid': purchaseOptionIdAndroid,
'rentalDetailsAndroid': rentalDetailsAndroid?.toJson(),
'type': type.toJson(),
'validTimeWindowAndroid': validTimeWindowAndroid?.toJson(),
@@ -1711,6 +1718,41 @@ class FetchProductsResultSubscriptions extends FetchProductsResult {
final List? value;
}
+/// Installment plan details for subscription offers (Android)
+/// Contains information about the installment plan commitment.
+/// Available in Google Play Billing Library 7.0+
+class InstallmentPlanDetailsAndroid {
+ const InstallmentPlanDetailsAndroid({
+ required this.commitmentPaymentsCount,
+ required this.subsequentCommitmentPaymentsCount,
+ });
+
+ /// Committed payments count after a user signs up for this subscription plan.
+ /// For example, for a monthly subscription with commitmentPaymentsCount of 12,
+ /// users will be charged monthly for 12 months after signup.
+ final int commitmentPaymentsCount;
+ /// Subsequent committed payments count after the subscription plan renews.
+ /// For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12,
+ /// users will be committed to another 12 monthly payments when the plan renews.
+ /// Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan).
+ final int subsequentCommitmentPaymentsCount;
+
+ factory InstallmentPlanDetailsAndroid.fromJson(Map json) {
+ return InstallmentPlanDetailsAndroid(
+ commitmentPaymentsCount: json['commitmentPaymentsCount'] as int,
+ subsequentCommitmentPaymentsCount: json['subsequentCommitmentPaymentsCount'] as int,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'InstallmentPlanDetailsAndroid',
+ 'commitmentPaymentsCount': commitmentPaymentsCount,
+ 'subsequentCommitmentPaymentsCount': subsequentCommitmentPaymentsCount,
+ };
+ }
+}
+
/// Limited quantity information for one-time purchase offers (Android)
/// Available in Google Play Billing Library 7.0+
class LimitedQuantityInfoAndroid {
@@ -1740,6 +1782,40 @@ class LimitedQuantityInfoAndroid {
}
}
+/// Pending purchase update for subscription upgrades/downgrades (Android)
+/// When a user initiates a subscription change (upgrade/downgrade), the new purchase
+/// may be pending until the current billing period ends. This type contains the
+/// details of the pending change.
+/// Available in Google Play Billing Library 5.0+
+class PendingPurchaseUpdateAndroid {
+ const PendingPurchaseUpdateAndroid({
+ required this.products,
+ required this.purchaseToken,
+ });
+
+ /// Product IDs for the pending purchase update.
+ /// These are the new products the user is switching to.
+ final List products;
+ /// Purchase token for the pending transaction.
+ /// Use this token to track or manage the pending purchase update.
+ final String purchaseToken;
+
+ factory PendingPurchaseUpdateAndroid.fromJson(Map json) {
+ return PendingPurchaseUpdateAndroid(
+ products: (json['products'] as List).map((e) => e as String).toList(),
+ purchaseToken: json['purchaseToken'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'PendingPurchaseUpdateAndroid',
+ 'products': products,
+ 'purchaseToken': purchaseToken,
+ };
+ }
+}
+
/// Pre-order details for one-time purchase products (Android)
/// Available in Google Play Billing Library 8.1.0+
class PreorderDetailsAndroid {
@@ -1946,6 +2022,7 @@ class ProductAndroidOneTimePurchaseOfferDetail {
this.preorderDetailsAndroid,
required this.priceAmountMicros,
required this.priceCurrencyCode,
+ this.purchaseOptionId,
this.rentalDetailsAndroid,
this.validTimeWindow,
});
@@ -1970,6 +2047,10 @@ class ProductAndroidOneTimePurchaseOfferDetail {
final PreorderDetailsAndroid? preorderDetailsAndroid;
final String priceAmountMicros;
final String priceCurrencyCode;
+ /// Purchase option ID for this offer (Android)
+ /// Used to identify which purchase option the user selected.
+ /// Available in Google Play Billing Library 7.0+
+ final String? purchaseOptionId;
/// Rental details for rental offers
final RentalDetailsAndroid? rentalDetailsAndroid;
/// Valid time window for the offer
@@ -1987,6 +2068,7 @@ class ProductAndroidOneTimePurchaseOfferDetail {
preorderDetailsAndroid: json['preorderDetailsAndroid'] != null ? PreorderDetailsAndroid.fromJson(json['preorderDetailsAndroid'] as Map) : null,
priceAmountMicros: json['priceAmountMicros'] as String,
priceCurrencyCode: json['priceCurrencyCode'] as String,
+ purchaseOptionId: json['purchaseOptionId'] as String?,
rentalDetailsAndroid: json['rentalDetailsAndroid'] != null ? RentalDetailsAndroid.fromJson(json['rentalDetailsAndroid'] as Map) : null,
validTimeWindow: json['validTimeWindow'] != null ? ValidTimeWindowAndroid.fromJson(json['validTimeWindow'] as Map) : null,
);
@@ -2005,6 +2087,7 @@ class ProductAndroidOneTimePurchaseOfferDetail {
'preorderDetailsAndroid': preorderDetailsAndroid?.toJson(),
'priceAmountMicros': priceAmountMicros,
'priceCurrencyCode': priceCurrencyCode,
+ 'purchaseOptionId': purchaseOptionId,
'rentalDetailsAndroid': rentalDetailsAndroid?.toJson(),
'validTimeWindow': validTimeWindow?.toJson(),
};
@@ -2201,6 +2284,7 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC
class ProductSubscriptionAndroidOfferDetails {
const ProductSubscriptionAndroidOfferDetails({
required this.basePlanId,
+ this.installmentPlanDetails,
this.offerId,
required this.offerTags,
required this.offerToken,
@@ -2208,6 +2292,10 @@ class ProductSubscriptionAndroidOfferDetails {
});
final String basePlanId;
+ /// Installment plan details for this subscription offer.
+ /// Only set for installment subscription plans; null for non-installment plans.
+ /// Available in Google Play Billing Library 7.0+
+ final InstallmentPlanDetailsAndroid? installmentPlanDetails;
final String? offerId;
final List offerTags;
final String offerToken;
@@ -2216,6 +2304,7 @@ class ProductSubscriptionAndroidOfferDetails {
factory ProductSubscriptionAndroidOfferDetails.fromJson(Map json) {
return ProductSubscriptionAndroidOfferDetails(
basePlanId: json['basePlanId'] as String,
+ installmentPlanDetails: json['installmentPlanDetails'] != null ? InstallmentPlanDetailsAndroid.fromJson(json['installmentPlanDetails'] as Map) : null,
offerId: json['offerId'] as String?,
offerTags: (json['offerTags'] as List).map((e) => e as String).toList(),
offerToken: json['offerToken'] as String,
@@ -2227,6 +2316,7 @@ class ProductSubscriptionAndroidOfferDetails {
return {
'__typename': 'ProductSubscriptionAndroidOfferDetails',
'basePlanId': basePlanId,
+ 'installmentPlanDetails': installmentPlanDetails?.toJson(),
'offerId': offerId,
'offerTags': offerTags,
'offerToken': offerToken,
@@ -2368,6 +2458,7 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon {
this.obfuscatedAccountIdAndroid,
this.obfuscatedProfileIdAndroid,
this.packageNameAndroid,
+ this.pendingPurchaseUpdateAndroid,
required this.platform,
required this.productId,
required this.purchaseState,
@@ -2397,6 +2488,11 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon {
final String? obfuscatedAccountIdAndroid;
final String? obfuscatedProfileIdAndroid;
final String? packageNameAndroid;
+ /// Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ /// Contains the new products and purchase token for the pending transaction.
+ /// Returns null if no pending update exists.
+ /// Available in Google Play Billing Library 5.0+
+ final PendingPurchaseUpdateAndroid? pendingPurchaseUpdateAndroid;
final IapPlatform platform;
final String productId;
final PurchaseState purchaseState;
@@ -2423,6 +2519,7 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon {
obfuscatedAccountIdAndroid: json['obfuscatedAccountIdAndroid'] as String?,
obfuscatedProfileIdAndroid: json['obfuscatedProfileIdAndroid'] as String?,
packageNameAndroid: json['packageNameAndroid'] as String?,
+ pendingPurchaseUpdateAndroid: json['pendingPurchaseUpdateAndroid'] != null ? PendingPurchaseUpdateAndroid.fromJson(json['pendingPurchaseUpdateAndroid'] as Map) : null,
platform: IapPlatform.fromJson(json['platform'] as String),
productId: json['productId'] as String,
purchaseState: PurchaseState.fromJson(json['purchaseState'] as String),
@@ -2452,6 +2549,7 @@ class PurchaseAndroid extends Purchase implements PurchaseCommon {
'obfuscatedAccountIdAndroid': obfuscatedAccountIdAndroid,
'obfuscatedProfileIdAndroid': obfuscatedProfileIdAndroid,
'packageNameAndroid': packageNameAndroid,
+ 'pendingPurchaseUpdateAndroid': pendingPurchaseUpdateAndroid?.toJson(),
'platform': platform.toJson(),
'productId': productId,
'purchaseState': purchaseState.toJson(),
@@ -2909,6 +3007,7 @@ class SubscriptionOffer {
this.currency,
required this.displayPrice,
required this.id,
+ this.installmentPlanDetailsAndroid,
this.keyIdentifierIOS,
this.localizedPriceIOS,
this.nonceIOS,
@@ -2936,6 +3035,10 @@ class SubscriptionOffer {
/// - iOS: Discount identifier from App Store Connect
/// - Android: offerId from ProductSubscriptionAndroidOfferDetails
final String id;
+ /// [Android] Installment plan details for this subscription offer.
+ /// Only set for installment subscription plans; null for non-installment plans.
+ /// Available in Google Play Billing Library 7.0+
+ final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid;
/// [iOS] Key identifier for signature validation.
/// Used with server-side signature generation for promotional offers.
final String? keyIdentifierIOS;
@@ -2977,6 +3080,7 @@ class SubscriptionOffer {
currency: json['currency'] as String?,
displayPrice: json['displayPrice'] as String,
id: json['id'] as String,
+ installmentPlanDetailsAndroid: json['installmentPlanDetailsAndroid'] != null ? InstallmentPlanDetailsAndroid.fromJson(json['installmentPlanDetailsAndroid'] as Map) : null,
keyIdentifierIOS: json['keyIdentifierIOS'] as String?,
localizedPriceIOS: json['localizedPriceIOS'] as String?,
nonceIOS: json['nonceIOS'] as String?,
@@ -3001,6 +3105,7 @@ class SubscriptionOffer {
'currency': currency,
'displayPrice': displayPrice,
'id': id,
+ 'installmentPlanDetailsAndroid': installmentPlanDetailsAndroid?.toJson(),
'keyIdentifierIOS': keyIdentifierIOS,
'localizedPriceIOS': localizedPriceIOS,
'nonceIOS': nonceIOS,
diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd
index 3fd884f3..121abf7e 100644
--- a/packages/gql/src/generated/types.gd
+++ b/packages/gql/src/generated/types.gd
@@ -668,6 +668,8 @@ class DiscountOffer:
var preorder_details_android: PreorderDetailsAndroid
## [Android] Rental details if this is a rental offer.
var rental_details_android: RentalDetailsAndroid
+ ## [Android] Purchase option ID for this offer.
+ var purchase_option_id_android: String
static func from_dict(data: Dictionary) -> DiscountOffer:
var obj = DiscountOffer.new()
@@ -717,6 +719,8 @@ class DiscountOffer:
obj.rental_details_android = RentalDetailsAndroid.from_dict(data["rentalDetailsAndroid"])
else:
obj.rental_details_android = data["rentalDetailsAndroid"]
+ if data.has("purchaseOptionIdAndroid") and data["purchaseOptionIdAndroid"] != null:
+ obj.purchase_option_id_android = data["purchaseOptionIdAndroid"]
return obj
func to_dict() -> Dictionary:
@@ -751,6 +755,7 @@ class DiscountOffer:
dict["rentalDetailsAndroid"] = rental_details_android.to_dict()
else:
dict["rentalDetailsAndroid"] = rental_details_android
+ dict["purchaseOptionIdAndroid"] = purchase_option_id_android
return dict
## iOS DiscountOffer (output type). @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer
@@ -939,6 +944,27 @@ class ExternalPurchaseNoticeResultIOS:
dict["externalPurchaseToken"] = external_purchase_token
return dict
+## Installment plan details for subscription offers (Android) Contains information about the installment plan commitment. Available in Google Play Billing Library 7.0+
+class InstallmentPlanDetailsAndroid:
+ ## Committed payments count after a user signs up for this subscription plan.
+ var commitment_payments_count: int
+ ## Subsequent committed payments count after the subscription plan renews.
+ var subsequent_commitment_payments_count: int
+
+ static func from_dict(data: Dictionary) -> InstallmentPlanDetailsAndroid:
+ var obj = InstallmentPlanDetailsAndroid.new()
+ if data.has("commitmentPaymentsCount") and data["commitmentPaymentsCount"] != null:
+ obj.commitment_payments_count = data["commitmentPaymentsCount"]
+ if data.has("subsequentCommitmentPaymentsCount") and data["subsequentCommitmentPaymentsCount"] != null:
+ obj.subsequent_commitment_payments_count = data["subsequentCommitmentPaymentsCount"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["commitmentPaymentsCount"] = commitment_payments_count
+ dict["subsequentCommitmentPaymentsCount"] = subsequent_commitment_payments_count
+ return dict
+
## Limited quantity information for one-time purchase offers (Android) Available in Google Play Billing Library 7.0+
class LimitedQuantityInfoAndroid:
## Maximum quantity a user can purchase
@@ -960,6 +986,27 @@ class LimitedQuantityInfoAndroid:
dict["remainingQuantity"] = remaining_quantity
return dict
+## Pending purchase update for subscription upgrades/downgrades (Android) When a user initiates a subscription change (upgrade/downgrade), the new purchase may be pending until the current billing period ends. This type contains the details of the pending change. Available in Google Play Billing Library 5.0+
+class PendingPurchaseUpdateAndroid:
+ ## Product IDs for the pending purchase update.
+ var products: Array[String]
+ ## Purchase token for the pending transaction.
+ var purchase_token: String
+
+ static func from_dict(data: Dictionary) -> PendingPurchaseUpdateAndroid:
+ var obj = PendingPurchaseUpdateAndroid.new()
+ if data.has("products") and data["products"] != null:
+ obj.products = data["products"]
+ if data.has("purchaseToken") and data["purchaseToken"] != null:
+ obj.purchase_token = data["purchaseToken"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["products"] = products
+ dict["purchaseToken"] = purchase_token
+ return dict
+
## Pre-order details for one-time purchase products (Android) Available in Google Play Billing Library 8.1.0+
class PreorderDetailsAndroid:
## Pre-order presale end time in milliseconds since epoch.
@@ -1227,6 +1274,8 @@ class ProductAndroidOneTimePurchaseOfferDetail:
var preorder_details_android: PreorderDetailsAndroid
## Rental details for rental offers
var rental_details_android: RentalDetailsAndroid
+ ## Purchase option ID for this offer (Android)
+ var purchase_option_id: String
static func from_dict(data: Dictionary) -> ProductAndroidOneTimePurchaseOfferDetail:
var obj = ProductAndroidOneTimePurchaseOfferDetail.new()
@@ -1269,6 +1318,8 @@ class ProductAndroidOneTimePurchaseOfferDetail:
obj.rental_details_android = RentalDetailsAndroid.from_dict(data["rentalDetailsAndroid"])
else:
obj.rental_details_android = data["rentalDetailsAndroid"]
+ if data.has("purchaseOptionId") and data["purchaseOptionId"] != null:
+ obj.purchase_option_id = data["purchaseOptionId"]
return obj
func to_dict() -> Dictionary:
@@ -1300,6 +1351,7 @@ class ProductAndroidOneTimePurchaseOfferDetail:
dict["rentalDetailsAndroid"] = rental_details_android.to_dict()
else:
dict["rentalDetailsAndroid"] = rental_details_android
+ dict["purchaseOptionId"] = purchase_option_id
return dict
class ProductIOS:
@@ -1587,6 +1639,8 @@ class ProductSubscriptionAndroidOfferDetails:
var offer_token: String
var offer_tags: Array[String]
var pricing_phases: PricingPhasesAndroid
+ ## Installment plan details for this subscription offer.
+ var installment_plan_details: InstallmentPlanDetailsAndroid
static func from_dict(data: Dictionary) -> ProductSubscriptionAndroidOfferDetails:
var obj = ProductSubscriptionAndroidOfferDetails.new()
@@ -1603,6 +1657,11 @@ class ProductSubscriptionAndroidOfferDetails:
obj.pricing_phases = PricingPhasesAndroid.from_dict(data["pricingPhases"])
else:
obj.pricing_phases = data["pricingPhases"]
+ if data.has("installmentPlanDetails") and data["installmentPlanDetails"] != null:
+ if data["installmentPlanDetails"] is Dictionary:
+ obj.installment_plan_details = InstallmentPlanDetailsAndroid.from_dict(data["installmentPlanDetails"])
+ else:
+ obj.installment_plan_details = data["installmentPlanDetails"]
return obj
func to_dict() -> Dictionary:
@@ -1615,6 +1674,10 @@ class ProductSubscriptionAndroidOfferDetails:
dict["pricingPhases"] = pricing_phases.to_dict()
else:
dict["pricingPhases"] = pricing_phases
+ if installment_plan_details != null and installment_plan_details.has_method("to_dict"):
+ dict["installmentPlanDetails"] = installment_plan_details.to_dict()
+ else:
+ dict["installmentPlanDetails"] = installment_plan_details
return dict
class ProductSubscriptionIOS:
@@ -1828,6 +1891,8 @@ class PurchaseAndroid:
var obfuscated_profile_id_android: String
## Whether the subscription is suspended (Android)
var is_suspended_android: bool
+ ## Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ var pending_purchase_update_android: PendingPurchaseUpdateAndroid
static func from_dict(data: Dictionary) -> PurchaseAndroid:
var obj = PurchaseAndroid.new()
@@ -1885,6 +1950,11 @@ class PurchaseAndroid:
obj.obfuscated_profile_id_android = data["obfuscatedProfileIdAndroid"]
if data.has("isSuspendedAndroid") and data["isSuspendedAndroid"] != null:
obj.is_suspended_android = data["isSuspendedAndroid"]
+ if data.has("pendingPurchaseUpdateAndroid") and data["pendingPurchaseUpdateAndroid"] != null:
+ if data["pendingPurchaseUpdateAndroid"] is Dictionary:
+ obj.pending_purchase_update_android = PendingPurchaseUpdateAndroid.from_dict(data["pendingPurchaseUpdateAndroid"])
+ else:
+ obj.pending_purchase_update_android = data["pendingPurchaseUpdateAndroid"]
return obj
func to_dict() -> Dictionary:
@@ -1919,6 +1989,10 @@ class PurchaseAndroid:
dict["obfuscatedAccountIdAndroid"] = obfuscated_account_id_android
dict["obfuscatedProfileIdAndroid"] = obfuscated_profile_id_android
dict["isSuspendedAndroid"] = is_suspended_android
+ if pending_purchase_update_android != null and pending_purchase_update_android.has_method("to_dict"):
+ dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android.to_dict()
+ else:
+ dict["pendingPurchaseUpdateAndroid"] = pending_purchase_update_android
return dict
class PurchaseError:
@@ -2383,6 +2457,8 @@ class SubscriptionOffer:
var offer_tags_android: Array[String]
## [Android] Pricing phases for this subscription offer.
var pricing_phases_android: PricingPhasesAndroid
+ ## [Android] Installment plan details for this subscription offer.
+ var installment_plan_details_android: InstallmentPlanDetailsAndroid
static func from_dict(data: Dictionary) -> SubscriptionOffer:
var obj = SubscriptionOffer.new()
@@ -2436,6 +2512,11 @@ class SubscriptionOffer:
obj.pricing_phases_android = PricingPhasesAndroid.from_dict(data["pricingPhasesAndroid"])
else:
obj.pricing_phases_android = data["pricingPhasesAndroid"]
+ if data.has("installmentPlanDetailsAndroid") and data["installmentPlanDetailsAndroid"] != null:
+ if data["installmentPlanDetailsAndroid"] is Dictionary:
+ obj.installment_plan_details_android = InstallmentPlanDetailsAndroid.from_dict(data["installmentPlanDetailsAndroid"])
+ else:
+ obj.installment_plan_details_android = data["installmentPlanDetailsAndroid"]
return obj
func to_dict() -> Dictionary:
@@ -2470,6 +2551,10 @@ class SubscriptionOffer:
dict["pricingPhasesAndroid"] = pricing_phases_android.to_dict()
else:
dict["pricingPhasesAndroid"] = pricing_phases_android
+ if installment_plan_details_android != null and installment_plan_details_android.has_method("to_dict"):
+ dict["installmentPlanDetailsAndroid"] = installment_plan_details_android.to_dict()
+ else:
+ dict["installmentPlanDetailsAndroid"] = installment_plan_details_android
return dict
## iOS subscription offer details. @deprecated Use the standardized SubscriptionOffer type instead for cross-platform compatibility. @see https://openiap.dev/docs/types#subscription-offer
diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts
index baa44e9c..7f133545 100644
--- a/packages/gql/src/generated/types.ts
+++ b/packages/gql/src/generated/types.ts
@@ -258,6 +258,12 @@ export interface DiscountOffer {
preorderDetailsAndroid?: (PreorderDetailsAndroid | null);
/** Numeric price value */
price: number;
+ /**
+ * [Android] Purchase option ID for this offer.
+ * Used to identify which purchase option the user selected.
+ * Available in Google Play Billing Library 7.0+
+ */
+ purchaseOptionIdAndroid?: (string | null);
/** [Android] Rental details if this is a rental offer. */
rentalDetailsAndroid?: (RentalDetailsAndroid | null);
/** Type of discount offer */
@@ -478,6 +484,27 @@ export interface InitConnectionConfig {
enableBillingProgramAndroid?: (BillingProgramAndroid | null);
}
+/**
+ * Installment plan details for subscription offers (Android)
+ * Contains information about the installment plan commitment.
+ * Available in Google Play Billing Library 7.0+
+ */
+export interface InstallmentPlanDetailsAndroid {
+ /**
+ * Committed payments count after a user signs up for this subscription plan.
+ * For example, for a monthly subscription with commitmentPaymentsCount of 12,
+ * users will be charged monthly for 12 months after signup.
+ */
+ commitmentPaymentsCount: number;
+ /**
+ * Subsequent committed payments count after the subscription plan renews.
+ * For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12,
+ * users will be committed to another 12 monthly payments when the plan renews.
+ * Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan).
+ */
+ subsequentCommitmentPaymentsCount: number;
+}
+
/**
* Parameters for launching an external link (Android)
* Used with launchExternalLink to initiate external offer or app install flows
@@ -680,6 +707,26 @@ export type PaymentMode = 'free-trial' | 'pay-as-you-go' | 'pay-up-front' | 'unk
export type PaymentModeIOS = 'empty' | 'free-trial' | 'pay-as-you-go' | 'pay-up-front';
+/**
+ * Pending purchase update for subscription upgrades/downgrades (Android)
+ * When a user initiates a subscription change (upgrade/downgrade), the new purchase
+ * may be pending until the current billing period ends. This type contains the
+ * details of the pending change.
+ * Available in Google Play Billing Library 5.0+
+ */
+export interface PendingPurchaseUpdateAndroid {
+ /**
+ * Product IDs for the pending purchase update.
+ * These are the new products the user is switching to.
+ */
+ products: string[];
+ /**
+ * Purchase token for the pending transaction.
+ * Use this token to track or manage the pending purchase update.
+ */
+ purchaseToken: string;
+}
+
/**
* Pre-order details for one-time purchase products (Android)
* Available in Google Play Billing Library 8.1.0+
@@ -791,6 +838,12 @@ export interface ProductAndroidOneTimePurchaseOfferDetail {
preorderDetailsAndroid?: (PreorderDetailsAndroid | null);
priceAmountMicros: string;
priceCurrencyCode: string;
+ /**
+ * Purchase option ID for this offer (Android)
+ * Used to identify which purchase option the user selected.
+ * Available in Google Play Billing Library 7.0+
+ */
+ purchaseOptionId?: (string | null);
/** Rental details for rental offers */
rentalDetailsAndroid?: (RentalDetailsAndroid | null);
/** Valid time window for the offer */
@@ -911,6 +964,12 @@ export interface ProductSubscriptionAndroid extends ProductCommon {
*/
export interface ProductSubscriptionAndroidOfferDetails {
basePlanId: string;
+ /**
+ * Installment plan details for this subscription offer.
+ * Only set for installment subscription plans; null for non-installment plans.
+ * Available in Google Play Billing Library 7.0+
+ */
+ installmentPlanDetails?: (InstallmentPlanDetailsAndroid | null);
offerId?: (string | null);
offerTags: string[];
offerToken: string;
@@ -1000,6 +1059,13 @@ export interface PurchaseAndroid extends PurchaseCommon {
obfuscatedAccountIdAndroid?: (string | null);
obfuscatedProfileIdAndroid?: (string | null);
packageNameAndroid?: (string | null);
+ /**
+ * Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ * Contains the new products and purchase token for the pending transaction.
+ * Returns null if no pending update exists.
+ * Available in Google Play Billing Library 5.0+
+ */
+ pendingPurchaseUpdateAndroid?: (PendingPurchaseUpdateAndroid | null);
/** @deprecated Use store instead */
platform: IapPlatform;
productId: string;
@@ -1533,6 +1599,12 @@ export interface SubscriptionOffer {
* - Android: offerId from ProductSubscriptionAndroidOfferDetails
*/
id: string;
+ /**
+ * [Android] Installment plan details for this subscription offer.
+ * Only set for installment subscription plans; null for non-installment plans.
+ * Available in Google Play Billing Library 7.0+
+ */
+ installmentPlanDetailsAndroid?: (InstallmentPlanDetailsAndroid | null);
/**
* [iOS] Key identifier for signature validation.
* Used with server-side signature generation for promotional offers.
diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql
index 01ce9611..339a835f 100644
--- a/packages/gql/src/type-android.graphql
+++ b/packages/gql/src/type-android.graphql
@@ -185,6 +185,33 @@ type ProductAndroidOneTimePurchaseOfferDetail
Rental details for rental offers
"""
rentalDetailsAndroid: RentalDetailsAndroid
+ """
+ Purchase option ID for this offer (Android)
+ Used to identify which purchase option the user selected.
+ Available in Google Play Billing Library 7.0+
+ """
+ purchaseOptionId: String
+}
+
+"""
+Installment plan details for subscription offers (Android)
+Contains information about the installment plan commitment.
+Available in Google Play Billing Library 7.0+
+"""
+type InstallmentPlanDetailsAndroid {
+ """
+ Committed payments count after a user signs up for this subscription plan.
+ For example, for a monthly subscription with commitmentPaymentsCount of 12,
+ users will be charged monthly for 12 months after signup.
+ """
+ commitmentPaymentsCount: Int!
+ """
+ Subsequent committed payments count after the subscription plan renews.
+ For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12,
+ users will be committed to another 12 monthly payments when the plan renews.
+ Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan).
+ """
+ subsequentCommitmentPaymentsCount: Int!
}
"""
@@ -199,6 +226,12 @@ type ProductSubscriptionAndroidOfferDetails
offerToken: String!
offerTags: [String!]!
pricingPhases: PricingPhasesAndroid!
+ """
+ Installment plan details for this subscription offer.
+ Only set for installment subscription plans; null for non-installment plans.
+ Available in Google Play Billing Library 7.0+
+ """
+ installmentPlanDetails: InstallmentPlanDetailsAndroid
}
type ProductAndroid implements ProductCommon {
@@ -344,6 +377,33 @@ type PurchaseAndroid implements PurchaseCommon {
Available in Google Play Billing Library 8.1.0+
"""
isSuspendedAndroid: Boolean
+ """
+ Pending purchase update for uncommitted subscription upgrade/downgrade (Android)
+ Contains the new products and purchase token for the pending transaction.
+ Returns null if no pending update exists.
+ Available in Google Play Billing Library 5.0+
+ """
+ pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid
+}
+
+"""
+Pending purchase update for subscription upgrades/downgrades (Android)
+When a user initiates a subscription change (upgrade/downgrade), the new purchase
+may be pending until the current billing period ends. This type contains the
+details of the pending change.
+Available in Google Play Billing Library 5.0+
+"""
+type PendingPurchaseUpdateAndroid {
+ """
+ Product IDs for the pending purchase update.
+ These are the new products the user is switching to.
+ """
+ products: [String!]!
+ """
+ Purchase token for the pending transaction.
+ Use this token to track or manage the pending purchase update.
+ """
+ purchaseToken: String!
}
# Android inputs
diff --git a/packages/gql/src/type.graphql b/packages/gql/src/type.graphql
index 4c7acd76..bfefeec8 100644
--- a/packages/gql/src/type.graphql
+++ b/packages/gql/src/type.graphql
@@ -598,6 +598,13 @@ type DiscountOffer {
[Android] Rental details if this is a rental offer.
"""
rentalDetailsAndroid: RentalDetailsAndroid
+
+ """
+ [Android] Purchase option ID for this offer.
+ Used to identify which purchase option the user selected.
+ Available in Google Play Billing Library 7.0+
+ """
+ purchaseOptionIdAndroid: String
}
"""
@@ -718,6 +725,13 @@ type SubscriptionOffer {
Contains detailed pricing information for each phase (trial, intro, regular).
"""
pricingPhasesAndroid: PricingPhasesAndroid
+
+ """
+ [Android] Installment plan details for this subscription offer.
+ Only set for installment subscription plans; null for non-installment plans.
+ Available in Google Play Billing Library 7.0+
+ """
+ installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid
}
# Initialization configuration