From 5a2e852c06f2c7e821484abaf80666c52ee98e14 Mon Sep 17 00:00:00 2001 From: Hyo Date: Wed, 11 Feb 2026 06:08:33 +0900 Subject: [PATCH 1/5] feat(gql): add BillingClient 5.0+ and 7.0+ fields for Issue #77 Android BillingClient enhancements: - purchaseOptionId on ProductAndroidOneTimePurchaseOfferDetail (7.0+) - InstallmentPlanDetailsAndroid type for subscription installments (7.0+) - installmentPlanDetails on ProductSubscriptionAndroidOfferDetails - PendingPurchaseUpdateAndroid for subscription upgrades/downgrades (5.0+) - pendingPurchaseUpdateAndroid on PurchaseAndroid Standardized offer types: - purchaseOptionIdAndroid on DiscountOffer - installmentPlanDetailsAndroid on SubscriptionOffer Closes #77 Co-Authored-By: Claude Opus 4.5 --- packages/gql/src/type-android.graphql | 60 +++++++++++++++++++++++++++ packages/gql/src/type.graphql | 14 +++++++ 2 files changed, 74 insertions(+) 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 From e898ac3a53d2345bf1d6bb5af760a3b88eb92627 Mon Sep 17 00:00:00 2001 From: Hyo Date: Wed, 11 Feb 2026 06:08:37 +0900 Subject: [PATCH 2/5] chore(gql): regenerate types for all platforms Regenerate TypeScript, Swift, Kotlin, Dart, GDScript types from updated GraphQL schema with BillingClient 5.0+ and 7.0+ fields. Co-Authored-By: Claude Opus 4.5 --- packages/gql/src/generated/Types.kt | 114 +++++++++++++++++++++++++ packages/gql/src/generated/Types.swift | 50 +++++++++++ packages/gql/src/generated/types.dart | 105 +++++++++++++++++++++++ packages/gql/src/generated/types.gd | 85 ++++++++++++++++++ packages/gql/src/generated/types.ts | 72 ++++++++++++++++ 5 files changed, 426 insertions(+) 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. From 19413ccab27757a09f645c99576c22eb04f986ec Mon Sep 17 00:00:00 2001 From: Hyo Date: Wed, 11 Feb 2026 06:08:43 +0900 Subject: [PATCH 3/5] feat(google): implement BillingClient 5.0+ and 7.0+ field extraction BillingConverters: - Extract purchaseOptionId from OneTimePurchaseOfferDetails (7.0+) - Extract installmentPlanDetails from SubscriptionOfferDetails (7.0+) - Extract pendingPurchaseUpdate from Purchase (5.0+) Types.kt: - Add purchaseOptionIdAndroid to DiscountOffer - Add installmentPlanDetailsAndroid to SubscriptionOffer - Add pendingPurchaseUpdateAndroid to PurchaseAndroid Tests: - Add comprehensive tests for all new fields - Test toJson/fromJson serialization - Test use cases like subscription downgrades Co-Authored-By: Claude Opus 4.5 --- packages/apple/Sources/Models/Types.swift | 50 ++ .../src/main/java/dev/hyo/openiap/Types.kt | 114 ++++ .../hyo/openiap/utils/BillingConverters.kt | 42 +- .../hyo/openiap/StandardizedOfferTypesTest.kt | 486 ++++++++++++++++++ 4 files changed, 688 insertions(+), 4 deletions(-) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 9ab84e4b..182e96a5 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/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/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()) + } } From 734c6a814bef187b7aa85b117fa36ca2ccc96625 Mon Sep 17 00:00:00 2001 From: Hyo Date: Wed, 11 Feb 2026 06:08:50 +0900 Subject: [PATCH 4/5] docs: add v1.3.17 release notes and update type documentation Release notes (notes.tsx): - Add v1.3.17 release entry for Android BillingClient Enhancement - Convert all References sections to accordion format - Add proper ul indentation for numbered headings Type documentation: - Add purchaseOptionIdAndroid to DiscountOffer (offer.tsx) - Add installmentPlanDetailsAndroid to SubscriptionOffer (offer.tsx) - Add pendingPurchaseUpdateAndroid to PurchaseAndroid (purchase.tsx) - Add PendingPurchaseUpdateAndroid type section (purchase.tsx) Co-Authored-By: Claude Opus 4.5 --- .../docs/src/pages/docs/types/android.tsx | 9 + packages/docs/src/pages/docs/types/offer.tsx | 63 +- .../docs/src/pages/docs/types/purchase.tsx | 73 + .../docs/src/pages/docs/updates/notes.tsx | 1821 +++++------------ 4 files changed, 678 insertions(+), 1288 deletions(-) diff --git a/packages/docs/src/pages/docs/types/android.tsx b/packages/docs/src/pages/docs/types/android.tsx index 7160a1cc..7c23ccb6 100644 --- a/packages/docs/src/pages/docs/types/android.tsx +++ b/packages/docs/src/pages/docs/types/android.tsx @@ -188,6 +188,15 @@ function TypesAndroid() { Quantity-limited offer availability + + + purchaseOptionId + + + string | null + + Purchase option ID to identify which option was selected (7.0+) + diff --git a/packages/docs/src/pages/docs/types/offer.tsx b/packages/docs/src/pages/docs/types/offer.tsx index 0871b6fb..7f378eb0 100644 --- a/packages/docs/src/pages/docs/types/offer.tsx +++ b/packages/docs/src/pages/docs/types/offer.tsx @@ -235,6 +235,15 @@ function TypesOffer() { Rental offer details + + + purchaseOptionIdAndroid + + + String + + Purchase option ID for identifying which purchase option was selected (7.0+) + @@ -263,6 +272,7 @@ function TypesOffer() { limitedQuantityInfoAndroid?: LimitedQuantityInfoAndroid; preorderDetailsAndroid?: PreorderDetailsAndroid; rentalDetailsAndroid?: RentalDetailsAndroid; + purchaseOptionIdAndroid?: string; } enum DiscountOfferType { @@ -292,6 +302,7 @@ enum DiscountOfferType { let limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? let preorderDetailsAndroid: PreorderDetailsAndroid? let rentalDetailsAndroid: RentalDetailsAndroid? + let purchaseOptionIdAndroid: String? } enum DiscountOfferType: String, Codable { @@ -320,7 +331,8 @@ enum DiscountOfferType: String, Codable { val validTimeWindowAndroid: ValidTimeWindowAndroid? = null, val limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? = null, val preorderDetailsAndroid: PreorderDetailsAndroid? = null, - val rentalDetailsAndroid: RentalDetailsAndroid? = null + val rentalDetailsAndroid: RentalDetailsAndroid? = null, + val purchaseOptionIdAndroid: String? = null ) enum class DiscountOfferType { @@ -350,6 +362,7 @@ enum class DiscountOfferType { final LimitedQuantityInfoAndroid? limitedQuantityInfoAndroid; final PreorderDetailsAndroid? preorderDetailsAndroid; final RentalDetailsAndroid? rentalDetailsAndroid; + final String? purchaseOptionIdAndroid; DiscountOffer({ this.id, @@ -367,6 +380,7 @@ enum class DiscountOfferType { this.limitedQuantityInfoAndroid, this.preorderDetailsAndroid, this.rentalDetailsAndroid, + this.purchaseOptionIdAndroid, }); } @@ -398,6 +412,7 @@ var valid_time_window_android: ValidTimeWindowAndroid var limited_quantity_info_android: LimitedQuantityInfoAndroid var preorder_details_android: PreorderDetailsAndroid var rental_details_android: RentalDetailsAndroid +var purchase_option_id_android: String enum DiscountOfferType { INTRODUCTORY, @@ -627,6 +642,15 @@ enum DiscountOfferType { Pricing phases (trial, intro, regular) + + + installmentPlanDetailsAndroid + + + InstallmentPlanDetailsAndroid + + Installment plan details for subscription commitments (7.0+) + @@ -660,6 +684,12 @@ enum DiscountOfferType { offerTokenAndroid?: string; offerTagsAndroid?: string[]; pricingPhasesAndroid?: PricingPhasesAndroid; + installmentPlanDetailsAndroid?: InstallmentPlanDetailsAndroid; +} + +interface InstallmentPlanDetailsAndroid { + commitmentPaymentsCount: number; + subsequentCommitmentPaymentsCount: number; } interface SubscriptionPeriod { @@ -707,6 +737,12 @@ enum PaymentMode { let offerTokenAndroid: String? let offerTagsAndroid: [String]? let pricingPhasesAndroid: PricingPhasesAndroid? + let installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? +} + +struct InstallmentPlanDetailsAndroid: Codable { + let commitmentPaymentsCount: Int + let subsequentCommitmentPaymentsCount: Int } struct SubscriptionPeriod: Codable { @@ -753,7 +789,13 @@ enum PaymentMode: String, Codable { val basePlanIdAndroid: String? = null, val offerTokenAndroid: String? = null, val offerTagsAndroid: List? = null, - val pricingPhasesAndroid: PricingPhasesAndroid? = null + val pricingPhasesAndroid: PricingPhasesAndroid? = null, + val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null +) + +data class InstallmentPlanDetailsAndroid( + val commitmentPaymentsCount: Int, + val subsequentCommitmentPaymentsCount: Int ) data class SubscriptionPeriod( @@ -794,6 +836,7 @@ enum class PaymentMode { final String? offerTokenAndroid; final List? 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. +

+ + + + + + + + + + + + + + + + + +
NameSummary
+ 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:

- - - - - - - - - - - - -
MethodDescription
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:

- - - - - - - - - - - - -
APIiOS VersionUse Case
ExternalPurchase17.4+Basic external purchase notice
ExternalPurchaseCustomLink18.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 -

- - - - - - - - - - - - - - -
ModeBefore (Wrong)After (Correct)
CHARGE_FULL_PRICE54
DEFERRED65
KEEP_EXISTING76
- -

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.

+
+ +
+
2. Known Issue - introductoryOfferEligibility API (#68)
+

+ Current field uses Boolean type, but Apple's introductoryOfferEligibility(compactJWS:) API requires a JWS string. Will be corrected in future release. +

+
+ +
+
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
+
    +
  • ProductAndroidOneTimePurchaseOfferDetailDiscountOffer
  • +
  • ProductSubscriptionAndroidOfferDetailsSubscriptionOffer
  • +
  • oneTimePurchaseOfferDetailsAndroiddiscountOffers
  • +
  • subscriptionOfferDetailsAndroidsubscriptionOffers
  • +
+
+ +
+ 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_CHOICEenableBillingProgramAndroid: USER_CHOICE_BILLING
alternativeBillingModeAndroid: ALTERNATIVE_ONLYenableBillingProgramAndroid: 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_CHOICEenableBillingProgramAndroid: USER_CHOICE_BILLING
  • +
  • alternativeBillingModeAndroid: ALTERNATIVE_ONLYenableBillingProgramAndroid: 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:

- - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureUser Choice BillingExternal Payments
Billing Library7.0+8.3.0+
AvailabilityEligible regionsJapan only
UISeparate dialogSide-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.platformstore, 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. +

        ), }, From ffe85a800ba1df61dcad5acde31fff5187c681fd Mon Sep 17 00:00:00 2001 From: Hyo Date: Wed, 11 Feb 2026 06:08:54 +0900 Subject: [PATCH 5/5] chore: add VSCode launch configurations for development Add debug/run configurations: - GQL: Generate Types - Docs: Dev Server - Test: Apple (Swift) - Test: Google (Android) Co-Authored-By: Claude Opus 4.5 --- .vscode/launch.json | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e66ec3d3..91dd4a72 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,18 +21,22 @@ "console": "integratedTerminal" }, { - "type": "node-terminal", - "request": "launch", "name": "📝 GQL: Generate Types", - "command": "bun run generate", - "cwd": "${workspaceFolder}/packages/gql" + "type": "node", + "request": "launch", + "runtimeExecutable": "bash", + "runtimeArgs": ["-lc", "bun run generate"], + "cwd": "${workspaceFolder}/packages/gql", + "console": "integratedTerminal" }, { - "type": "node-terminal", - "request": "launch", "name": "📚 Docs: Dev Server", - "command": "bun run dev", - "cwd": "${workspaceFolder}/packages/docs" + "type": "node", + "request": "launch", + "runtimeExecutable": "bash", + "runtimeArgs": ["-lc", "bun run dev"], + "cwd": "${workspaceFolder}/packages/docs", + "console": "integratedTerminal" }, { "type": "node-terminal",