From 1dc8900a22e7ea64387e1bbc6c9571a4c466078e Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:06:34 +0900 Subject: [PATCH 1/7] fix(gql): clarify advanced commerce availability --- libraries/expo-iap/src/types.ts | 7 ++++++- libraries/flutter_inapp_purchase/lib/types.dart | 5 ++++- libraries/godot-iap/addons/godot-iap/types.gd | 2 +- .../kotlin/io/github/hyochan/kmpiap/openiap/Types.kt | 5 ++++- libraries/maui-iap/src/OpenIap.Maui/Types.cs | 5 ++++- libraries/react-native-iap/src/types.ts | 7 ++++++- packages/apple/Sources/Models/Types.swift | 5 ++++- .../google/openiap/src/main/java/dev/hyo/openiap/Types.kt | 5 ++++- packages/gql/src/generated/Types.cs | 5 ++++- packages/gql/src/generated/Types.kt | 5 ++++- packages/gql/src/generated/Types.swift | 5 ++++- packages/gql/src/generated/types.dart | 5 ++++- packages/gql/src/generated/types.gd | 2 +- packages/gql/src/generated/types.ts | 7 ++++++- packages/gql/src/type-ios.graphql | 5 ++++- 15 files changed, 60 insertions(+), 15 deletions(-) diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index 056e0c007..5934056f1 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -46,7 +46,12 @@ export interface AdvancedCommerceInfoIOS { estimatedTax?: (string | null); /** The items purchased as part of this transaction */ items: AdvancedCommerceItemIOS[]; - /** Subscription period for this transaction */ + /** + * Subscription period for this transaction. + * Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + * (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + * or visionOS 2.4+). + */ period?: (SubscriptionPeriodValueIOS | null); /** Request reference identifier for tracking */ requestReferenceId?: (string | null); diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 50f99a113..06fd9d24a 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -1238,7 +1238,10 @@ class AdvancedCommerceInfoIOS { final String? estimatedTax; /// The items purchased as part of this transaction final List items; - /// Subscription period for this transaction + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). final SubscriptionPeriodValueIOS? period; /// Request reference identifier for tracking final String? requestReferenceId; diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index f59061cfd..b03a5e6b2 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -439,7 +439,7 @@ class ActiveSubscription: class AdvancedCommerceInfoIOS: ## The items purchased as part of this transaction var items: Array[AdvancedCommerceItemIOS] = [] - ## Subscription period for this transaction + ## Subscription period for this transaction. Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, or visionOS 2.4+). var period: SubscriptionPeriodValueIOS ## Request reference identifier for tracking var request_reference_id: Variant = null diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt index 43f3c9131..8c7849bd4 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt @@ -1387,7 +1387,10 @@ public data class AdvancedCommerceInfoIOS( */ val items: List, /** - * Subscription period for this transaction + * Subscription period for this transaction. + * Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + * (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + * or visionOS 2.4+). */ val period: SubscriptionPeriodValueIOS? = null, /** diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index 258d3b23c..eff98837c 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -2245,7 +2245,10 @@ public sealed record AdvancedCommerceInfoIOS /// The items purchased as part of this transaction [JsonPropertyName("items")] public required IReadOnlyList Items { get; init; } - /// Subscription period for this transaction + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). [JsonPropertyName("period")] public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index 056e0c007..5934056f1 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -46,7 +46,12 @@ export interface AdvancedCommerceInfoIOS { estimatedTax?: (string | null); /** The items purchased as part of this transaction */ items: AdvancedCommerceItemIOS[]; - /** Subscription period for this transaction */ + /** + * Subscription period for this transaction. + * Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + * (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + * or visionOS 2.4+). + */ period?: (SubscriptionPeriodValueIOS | null); /** Request reference identifier for tracking */ requestReferenceId?: (string | null); diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index ce8355084..d3bfac2ef 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -557,7 +557,10 @@ public struct AdvancedCommerceInfoIOS: Codable { public var estimatedTax: String? = nil /// The items purchased as part of this transaction public var items: [AdvancedCommerceItemIOS] - /// Subscription period for this transaction + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). public var period: SubscriptionPeriodValueIOS? = nil /// Request reference identifier for tracking public var requestReferenceId: String? = nil 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 6b1f9b81c..5ec22d2a9 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 @@ -1439,7 +1439,10 @@ public data class AdvancedCommerceInfoIOS( */ val items: List, /** - * Subscription period for this transaction + * Subscription period for this transaction. + * Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + * (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + * or visionOS 2.4+). */ val period: SubscriptionPeriodValueIOS? = null, /** diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index 258d3b23c..eff98837c 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -2245,7 +2245,10 @@ public sealed record AdvancedCommerceInfoIOS /// The items purchased as part of this transaction [JsonPropertyName("items")] public required IReadOnlyList Items { get; init; } - /// Subscription period for this transaction + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). [JsonPropertyName("period")] public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index e614239c9..ac6a88039 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -1385,7 +1385,10 @@ public data class AdvancedCommerceInfoIOS( */ val items: List, /** - * Subscription period for this transaction + * Subscription period for this transaction. + * Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + * (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + * or visionOS 2.4+). */ val period: SubscriptionPeriodValueIOS? = null, /** diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index ce8355084..d3bfac2ef 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -557,7 +557,10 @@ public struct AdvancedCommerceInfoIOS: Codable { public var estimatedTax: String? = nil /// The items purchased as part of this transaction public var items: [AdvancedCommerceItemIOS] - /// Subscription period for this transaction + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). public var period: SubscriptionPeriodValueIOS? = nil /// Request reference identifier for tracking public var requestReferenceId: String? = nil diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 50f99a113..06fd9d24a 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -1238,7 +1238,10 @@ class AdvancedCommerceInfoIOS { final String? estimatedTax; /// The items purchased as part of this transaction final List items; - /// Subscription period for this transaction + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). final SubscriptionPeriodValueIOS? period; /// Request reference identifier for tracking final String? requestReferenceId; diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index f59061cfd..b03a5e6b2 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -439,7 +439,7 @@ class ActiveSubscription: class AdvancedCommerceInfoIOS: ## The items purchased as part of this transaction var items: Array[AdvancedCommerceItemIOS] = [] - ## Subscription period for this transaction + ## Subscription period for this transaction. Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, or visionOS 2.4+). var period: SubscriptionPeriodValueIOS ## Request reference identifier for tracking var request_reference_id: Variant = null diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 056e0c007..5934056f1 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -46,7 +46,12 @@ export interface AdvancedCommerceInfoIOS { estimatedTax?: (string | null); /** The items purchased as part of this transaction */ items: AdvancedCommerceItemIOS[]; - /** Subscription period for this transaction */ + /** + * Subscription period for this transaction. + * Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + * (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + * or visionOS 2.4+). + */ period?: (SubscriptionPeriodValueIOS | null); /** Request reference identifier for tracking */ requestReferenceId?: (string | null); diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index 186ea30d8..feebc2834 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -728,7 +728,10 @@ type AdvancedCommerceInfoIOS { """ items: [AdvancedCommerceItemIOS!]! """ - Subscription period for this transaction + Subscription period for this transaction. + Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + or visionOS 2.4+). """ period: SubscriptionPeriodValueIOS """ From 84d6dd687dd82ac2dc5ecac9abc10fab6db81180 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:07:56 +0900 Subject: [PATCH 2/7] fix(apple): preserve promoted intent offers --- packages/apple/Sources/Helpers/IapState.swift | 40 +++++++++++-- packages/apple/Sources/OpenIapModule.swift | 56 +++++++++++++++++-- packages/apple/Tests/OpenIapTests.swift | 41 ++++++++++++-- 3 files changed, 122 insertions(+), 15 deletions(-) diff --git a/packages/apple/Sources/Helpers/IapState.swift b/packages/apple/Sources/Helpers/IapState.swift index 9a1be0ec0..78dbe6ddd 100644 --- a/packages/apple/Sources/Helpers/IapState.swift +++ b/packages/apple/Sources/Helpers/IapState.swift @@ -254,13 +254,45 @@ actor IapState { /// app starts the matching purchase. The generic form makes the one-shot, /// product-scoped behavior testable without constructing StoreKit offers. actor PromotedPurchaseIntentOfferStore { - private var offersByProductId: [String: Offer] = [:] + struct Lease: Sendable { + fileprivate let id: UInt64 + let offer: Offer + } + + private enum Slot: Sendable { + case available(id: UInt64, offer: Offer) + case leased(id: UInt64) + } + + private var nextId: UInt64 = 0 + private var slotsByProductId: [String: Slot] = [:] func record(_ offer: Offer?, for productId: String) { - offersByProductId[productId] = offer + nextId &+= 1 + guard let offer else { + slotsByProductId.removeValue(forKey: productId) + return + } + slotsByProductId[productId] = .available(id: nextId, offer: offer) + } + + func lease(for productId: String) -> Lease? { + guard case let .available(id, offer) = slotsByProductId[productId] else { + return nil + } + slotsByProductId[productId] = .leased(id: id) + return Lease(id: id, offer: offer) + } + + func release(_ lease: Lease, for productId: String) { + guard case let .leased(id) = slotsByProductId[productId], + id == lease.id else { return } + slotsByProductId[productId] = .available(id: lease.id, offer: lease.offer) } - func take(for productId: String) -> Offer? { - offersByProductId.removeValue(forKey: productId) + func consume(_ lease: Lease, for productId: String) { + guard case let .leased(id) = slotsByProductId[productId], + id == lease.id else { return } + slotsByProductId.removeValue(forKey: productId) } } diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 36ce3d215..d748ace67 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -383,15 +383,39 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { let sku = iosProps.sku let product = try await storeProduct(for: sku) #if os(iOS) - let purchaseIntentOffer = await promotedPurchaseIntentOffers.take(for: sku) + let canUsePurchaseIntentOffer: Bool + if let subscriptionProps = iosProps as? RequestSubscriptionIosProps { + canUsePurchaseIntentOffer = subscriptionProps.winBackOffer == nil && + subscriptionProps.withOffer == nil && + subscriptionProps.promotionalOfferJWS == nil + } else { + canUsePurchaseIntentOffer = false + } + let purchaseIntentOfferLease = canUsePurchaseIntentOffer + ? await promotedPurchaseIntentOffers.lease(for: sku) + : nil + let purchaseIntentOffer = purchaseIntentOfferLease?.offer #else let purchaseIntentOffer: StoreKit.Product.SubscriptionOffer? = nil #endif - let options = try StoreKitTypesBridge.purchaseOptionsIOS( - from: iosProps, - product: product, - purchaseIntentOffer: purchaseIntentOffer - ) + let options: Set + do { + options = try StoreKitTypesBridge.purchaseOptionsIOS( + from: iosProps, + product: product, + purchaseIntentOffer: purchaseIntentOffer + ) + } catch { + #if os(iOS) + if let purchaseIntentOfferLease { + await promotedPurchaseIntentOffers.release( + purchaseIntentOfferLease, + for: sku + ) + } + #endif + throw error + } if StoreKitTypesBridge.isAutoRenewingSubscriptionProductType(product.type) { await preflightInactiveUnfinishedSubscriptions(productId: sku) @@ -426,6 +450,14 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { result = try await product.purchase(options: options) #endif } catch { + #if os(iOS) + if let purchaseIntentOfferLease { + await promotedPurchaseIntentOffers.release( + purchaseIntentOfferLease, + for: sku + ) + } + #endif // Enhanced error handling for promotional offers if iosProps.withOffer != nil { OpenIapLog.error("Purchase with promotional offer failed: \(error.localizedDescription)") @@ -452,6 +484,18 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { throw purchaseError } + // Keep the intent offer available across local validation and + // presentation-context failures. Consume only after StoreKit has + // accepted the matching purchase attempt and returned a result. + #if os(iOS) + if let purchaseIntentOfferLease { + await promotedPurchaseIntentOffers.consume( + purchaseIntentOfferLease, + for: sku + ) + } + #endif + switch result { case .success(let verification): let transaction = try checkVerified(verification) diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift index cb25bcf68..626cefe2a 100644 --- a/packages/apple/Tests/OpenIapTests.swift +++ b/packages/apple/Tests/OpenIapTests.swift @@ -67,17 +67,48 @@ final class OpenIapTests: XCTestCase { let store = PromotedPurchaseIntentOfferStore() await store.record("win-back", for: "subscription") - let unrelated = await store.take(for: "other") - let matching = await store.take(for: "subscription") - let consumed = await store.take(for: "subscription") + let unrelated = await store.lease(for: "other") + let matching = await store.lease(for: "subscription") + let reserved = await store.lease(for: "subscription") XCTAssertNil(unrelated) - XCTAssertEqual(matching, "win-back") + XCTAssertEqual(matching?.offer, "win-back") + XCTAssertNil(reserved) + + if let matching { + await store.release(matching, for: "subscription") + await store.consume(matching, for: "subscription") + } + let retry = await store.lease(for: "subscription") + XCTAssertEqual(retry?.offer, "win-back") + + if let retry { + await store.consume(retry, for: "subscription") + } + let consumed = await store.lease(for: "subscription") XCTAssertNil(consumed) + await store.record("first", for: "subscription") + let staleLease = await store.lease(for: "subscription") + await store.record("replacement", for: "subscription") + if let staleLease { + await store.release(staleLease, for: "subscription") + } + let replacement = await store.lease(for: "subscription") + XCTAssertEqual(replacement?.offer, "replacement") + + await store.record("first", for: "subscription") + let consumedStaleLease = await store.lease(for: "subscription") + await store.record("newest", for: "subscription") + if let consumedStaleLease { + await store.consume(consumedStaleLease, for: "subscription") + } + let newest = await store.lease(for: "subscription") + XCTAssertEqual(newest?.offer, "newest") + await store.record("stale", for: "subscription") await store.record(nil, for: "subscription") - let cleared = await store.take(for: "subscription") + let cleared = await store.lease(for: "subscription") XCTAssertNil(cleared) } From c809fc89c7d98ab9a354c714149d7df1cc060365 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:08:15 +0900 Subject: [PATCH 3/7] fix(google): bind Horizon activity lifecycle --- .../main/java/dev/hyo/martie/MainActivity.kt | 1 + .../hyo/martie/screens/AllProductsScreen.kt | 46 +++------- .../screens/AvailablePurchasesScreen.kt | 29 +----- .../dev/hyo/martie/screens/OfferCodeScreen.kt | 9 +- .../hyo/martie/screens/OpenIapStoreContext.kt | 12 +++ .../hyo/martie/screens/PurchaseFlowScreen.kt | 37 +------- .../martie/screens/SubscriptionFlowScreen.kt | 41 +-------- .../main/java/dev/hyo/openiap/IapContext.kt | 53 ++++++++++- .../java/dev/hyo/openiap/OpenIapViewModel.kt | 91 ++++++++++++++++++- .../dev/hyo/openiap/store/OpenIapStore.kt | 41 ++++++++- .../dev/hyo/openiap/utils/ActivityUtils.kt | 28 ++++++ .../openiap/utils/OwnerScopedValueBinding.kt | 39 ++++++++ .../utils/OwnerScopedValueBindingTest.kt | 62 +++++++++++++ .../hyo/openiap/utils/ActivityUtilsTest.kt | 27 ++++++ 14 files changed, 367 insertions(+), 149 deletions(-) create mode 100644 packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt create mode 100644 packages/google/openiap/src/main/java/dev/hyo/openiap/utils/ActivityUtils.kt create mode 100644 packages/google/openiap/src/main/java/dev/hyo/openiap/utils/OwnerScopedValueBinding.kt create mode 100644 packages/google/openiap/src/test/java/dev/hyo/openiap/utils/OwnerScopedValueBindingTest.kt create mode 100644 packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/utils/ActivityUtilsTest.kt diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt b/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt index 83cc3fca3..561efa846 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt @@ -43,6 +43,7 @@ class MainActivity : ComponentActivity() { iapStore.endConnection() } } + iapStore.clear() super.onDestroy() } } diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt index f1bc3cf8f..68f2ccee4 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt @@ -14,15 +14,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* import dev.hyo.martie.IapConstants -import dev.hyo.martie.util.findActivity -import dev.hyo.openiap.IapContext import dev.hyo.openiap.Product import dev.hyo.openiap.ProductAndroid import dev.hyo.openiap.ProductQueryType @@ -39,10 +36,7 @@ fun AllProductsScreen( navController: NavController, storeParam: OpenIapStore? = null ) { - val context = LocalContext.current - val activity = remember(context) { context.findActivity() } - val appContext = remember(context) { context.applicationContext } - val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) } + val iapStore = currentOpenIapStore(storeParam) val products by iapStore.products.collectAsState() val subscriptions by iapStore.subscriptions.collectAsState() val status by iapStore.status.collectAsState() @@ -55,31 +49,20 @@ fun AllProductsScreen( val scope = rememberCoroutineScope() - // Initialize and connect on first composition - val startupScope = rememberCoroutineScope() - DisposableEffect(Unit) { - startupScope.launch { - try { - val connected = iapStore.initConnection() - if (connected) { - iapStore.setActivity(activity) - // Fetch all products at once using ProductQueryType.All - // This fetches both in-app and subscription products in a single call - val request = ProductRequest( - skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS, - type = ProductQueryType.All - ) - iapStore.fetchProducts(request) - } - } catch (_: Exception) { } - } - onDispose { - // End connection when screen leaves - startupScope.launch { - runCatching { iapStore.endConnection() } - runCatching { iapStore.clear() } + // The Activity owns and closes the shared store. + LaunchedEffect(iapStore) { + try { + val connected = iapStore.initConnection() + if (connected) { + // Fetch all products at once using ProductQueryType.All + // This fetches both in-app and subscription products in a single call + val request = ProductRequest( + skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS, + type = ProductQueryType.All + ) + iapStore.fetchProducts(request) } - } + } catch (_: Exception) { } } Scaffold( @@ -140,7 +123,6 @@ fun AllProductsScreen( try { val connected = iapStore.initConnection() if (connected) { - iapStore.setActivity(activity) // Fetch all products after reconnecting using ProductQueryType.All val request = ProductRequest( skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS, diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt index 302522dcc..25b39b36b 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -22,15 +21,10 @@ import dev.hyo.martie.BuildConfig import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* import dev.hyo.martie.util.PREMIUM_SUBSCRIPTION_PRODUCT_ID -import dev.hyo.openiap.IapContext import dev.hyo.openiap.PurchaseAndroid import dev.hyo.openiap.PurchaseState import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -39,9 +33,7 @@ fun AvailablePurchasesScreen( navController: NavController, storeParam: OpenIapStore? = null ) { - val context = LocalContext.current - val iapStore = storeParam ?: (IapContext.LocalOpenIapStore.current - ?: IapContext.rememberOpenIapStore()) + val iapStore = currentOpenIapStore(storeParam) val purchases by iapStore.availablePurchases.collectAsState() val status by iapStore.status.collectAsState() val connectionStatus by iapStore.isConnected.collectAsState() @@ -61,15 +53,6 @@ fun AvailablePurchasesScreen( var isInitializing by remember { mutableStateOf(true) } var initError by remember { mutableStateOf(null) } - // Use a dedicated scope for cleanup that won't be cancelled with composition - val cleanupScope = remember { CoroutineScope(Dispatchers.Main + SupervisorJob()) } - - DisposableEffect(cleanupScope) { - onDispose { - cleanupScope.cancel() - } - } - // Initialize and connect on first composition (spec-aligned names) LaunchedEffect(Unit) { try { @@ -86,16 +69,6 @@ fun AvailablePurchasesScreen( } } - DisposableEffect(Unit) { - onDispose { - // Use dedicated cleanup scope to avoid cancellation race - cleanupScope.launch { - runCatching { iapStore.endConnection() } - runCatching { iapStore.clear() } - } - } - } - Scaffold( topBar = { TopAppBar( diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt index e5c76355c..d7be98f04 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt @@ -21,7 +21,6 @@ import androidx.navigation.NavController import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* import dev.hyo.martie.util.findActivity -import dev.hyo.openiap.IapContext import dev.hyo.openiap.store.OpenIapStore import kotlinx.coroutines.launch @@ -33,8 +32,7 @@ fun OfferCodeScreen( ) { val context = LocalContext.current val activity = remember(context) { context.findActivity() } - val iapStore = storeParam ?: (IapContext.LocalOpenIapStore.current - ?: IapContext.rememberOpenIapStore()) + val iapStore = currentOpenIapStore(storeParam) var showResult by remember { mutableStateOf(false) } var resultMessage by remember { mutableStateOf("") } @@ -42,9 +40,8 @@ fun OfferCodeScreen( // Initialize and connect on first composition (spec-aligned names) val startupScope = rememberCoroutineScope() - DisposableEffect(Unit) { - startupScope.launch { runCatching { iapStore.initConnection() } } - onDispose { startupScope.launch { runCatching { iapStore.endConnection() } } } + LaunchedEffect(iapStore) { + runCatching { iapStore.initConnection() } } val lifecycleOwner = LocalLifecycleOwner.current diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt new file mode 100644 index 000000000..d3c67f496 --- /dev/null +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt @@ -0,0 +1,12 @@ +package dev.hyo.martie.screens + +import androidx.compose.runtime.Composable +import dev.hyo.openiap.IapContext +import dev.hyo.openiap.store.OpenIapStore + +/** Uses an injected store or the Activity-owned store provided by AppNavigation. */ +@Composable +internal fun currentOpenIapStore(storeParam: OpenIapStore?): OpenIapStore = + storeParam ?: requireNotNull(IapContext.LocalOpenIapStore.current) { + "OpenIapStore must be provided by IapContext.OpenIapProvider" + } diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt index 145a67560..8ddf7d989 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt @@ -1,6 +1,5 @@ package dev.hyo.martie.screens -import android.app.Activity import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -13,21 +12,15 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import dev.hyo.martie.IapConstants import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* -import dev.hyo.openiap.IapContext import dev.hyo.openiap.IapkitPurchaseState import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -49,7 +42,6 @@ import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitGoogleProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitResult import dev.hyo.openiap.utils.verifyPurchaseWithIapkit -import dev.hyo.martie.util.findActivity import dev.hyo.martie.BuildConfig enum class VerificationMethod(val displayName: String) { @@ -64,13 +56,8 @@ fun PurchaseFlowScreen( navController: NavController, storeParam: OpenIapStore? = null ) { - val context = LocalContext.current - val activity = remember(context) { context.findActivity() } val uiScope = rememberCoroutineScope() - val appContext = remember(context) { context.applicationContext } - val iapStore = storeParam ?: remember(appContext) { - OpenIapStore(appContext) - } + val iapStore = currentOpenIapStore(storeParam) val products by iapStore.products.collectAsState() val purchases by iapStore.availablePurchases.collectAsState() val androidProducts = remember(products) { products.filterIsInstance() } @@ -130,7 +117,6 @@ fun PurchaseFlowScreen( fun launchPurchase(product: ProductAndroid) { uiScope.launch { - iapStore.setActivity(activity) try { iapStore.requestPurchase(purchasePropsFor(product)) } catch (e: Exception) { @@ -143,15 +129,6 @@ fun PurchaseFlowScreen( } } - // Use a dedicated scope for cleanup that won't be cancelled with composition - val cleanupScope = remember { CoroutineScope(Dispatchers.Main + SupervisorJob()) } - - DisposableEffect(cleanupScope) { - onDispose { - cleanupScope.cancel() - } - } - // Initialize and connect on first composition (spec-aligned names) LaunchedEffect(Unit) { // Enable OpenIapLog for debugging @@ -160,7 +137,6 @@ fun PurchaseFlowScreen( try { val connected = iapStore.initConnection() if (connected) { - iapStore.setActivity(activity) val request = ProductRequest( skus = IapConstants.INAPP_SKUS, type = ProductQueryType.InApp @@ -183,16 +159,6 @@ fun PurchaseFlowScreen( } } - DisposableEffect(Unit) { - onDispose { - // Use dedicated cleanup scope to avoid cancellation race - cleanupScope.launch { - runCatching { iapStore.endConnection() } - runCatching { iapStore.clear() } - } - } - } - Scaffold( topBar = { TopAppBar( @@ -208,7 +174,6 @@ fun PurchaseFlowScreen( onClick = { scope.launch { try { - iapStore.setActivity(activity) val request = ProductRequest( skus = IapConstants.INAPP_SKUS, type = ProductQueryType.InApp diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt index 901794a68..2b53eef2c 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import android.app.Activity import android.content.Context import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -24,7 +23,6 @@ import dev.hyo.martie.models.AppColors import dev.hyo.martie.IapConstants import dev.hyo.martie.BuildConfig import dev.hyo.martie.screens.uis.* -import dev.hyo.openiap.IapContext import dev.hyo.openiap.ProductAndroid import dev.hyo.openiap.ProductQueryType import dev.hyo.openiap.ProductType @@ -46,13 +44,9 @@ import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid import dev.hyo.openiap.SubscriptionReplacementModeAndroid import dev.hyo.openiap.utils.verifyPurchaseWithIapkit -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import dev.hyo.martie.util.findActivity import dev.hyo.martie.util.PREMIUM_SUBSCRIPTION_PRODUCT_ID import dev.hyo.martie.util.SUBSCRIPTION_PREFS_NAME import dev.hyo.martie.util.resolvePremiumOfferInfo @@ -94,15 +88,12 @@ fun SubscriptionFlowScreen( storeParam: OpenIapStore? = null ) { val context = LocalContext.current - val activity = remember(context) { context.findActivity() } val uiScope = rememberCoroutineScope() val appContext = remember(context) { context.applicationContext } // SharedPreferences to track current offer (necessary since Google doesn't provide offer info) val prefs = remember { context.getSharedPreferences(SUBSCRIPTION_PREFS_NAME, Context.MODE_PRIVATE) } - val iapStore = storeParam ?: remember(appContext) { - OpenIapStore(appContext) - } + val iapStore = currentOpenIapStore(storeParam) val products by iapStore.products.collectAsState() val subscriptions by iapStore.subscriptions.collectAsState() val purchases by iapStore.availablePurchases.collectAsState() @@ -157,15 +148,6 @@ fun SubscriptionFlowScreen( runCatching { BuildConfig.IAPKIT_API_KEY.takeIf { it.isNotBlank() } }.getOrNull() } - // Use a dedicated scope for cleanup that won't be cancelled with composition - val cleanupScope = remember { CoroutineScope(Dispatchers.Main + SupervisorJob()) } - - DisposableEffect(cleanupScope) { - onDispose { - cleanupScope.cancel() - } - } - // Load subscription data on screen entry LaunchedEffect(Unit) { // Enable OpenIapLog for debugging @@ -178,7 +160,6 @@ fun SubscriptionFlowScreen( val connected = iapStore.initConnection() if (connected) { - iapStore.setActivity(activity) // TEST: Use getActiveSubscriptions instead of getAvailablePurchases for example usage println("SubscriptionFlow: Testing getActiveSubscriptions...") @@ -246,16 +227,6 @@ fun SubscriptionFlowScreen( } } - DisposableEffect(Unit) { - onDispose { - // Use dedicated cleanup scope to avoid cancellation race - cleanupScope.launch { - runCatching { iapStore.endConnection() } - runCatching { iapStore.clear() } - } - } - } - // Cross-platform subscriptionBillingIssue listener: fires when Play Billing 8.1+ // reports isSuspended == true on any active subscription. No-op on Horizon flavor. DisposableEffect(iapStore) { @@ -356,7 +327,6 @@ fun SubscriptionFlowScreen( onClick = { scope.launch { try { - iapStore.setActivity(activity) val request = ProductRequest( skus = subscriptionSkus, type = ProductQueryType.Subs @@ -475,7 +445,7 @@ fun SubscriptionFlowScreen( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Button(onClick = { - cleanupScope.launch { + uiScope.launch { runCatching { iapStore.deepLinkToSubscriptions( dev.hyo.openiap.DeepLinkOptions( @@ -851,8 +821,6 @@ fun SubscriptionFlowScreen( onClick = { scope.launch { try { - iapStore.setActivity(activity) - val purchaseToken = subscription.purchaseToken if (purchaseToken == null) { iapStore.postStatusMessage( @@ -1080,8 +1048,6 @@ fun SubscriptionFlowScreen( onClick = { scope.launch { try { - iapStore.setActivity(activity) - val purchaseToken = subscription.purchaseToken if (purchaseToken == null) { iapStore.postStatusMessage( @@ -1217,8 +1183,6 @@ fun SubscriptionFlowScreen( } scope.launch { - iapStore.setActivity(activity) - val props = if (product.type == ProductType.Subs) { // Determine if this is an upgrade or downgrade val purchaseToken = otherPremiumSubscription?.purchaseToken @@ -1558,7 +1522,6 @@ fun SubscriptionFlowScreen( onDismiss = { selectedProduct = null }, onPurchase = { uiScope.launch { - iapStore.setActivity(activity) val props = if (product.type == ProductType.Subs) { RequestPurchaseProps( request = RequestPurchaseProps.Request.Subscription( diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/IapContext.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/IapContext.kt index 6cefc84d7..39bdf5632 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/IapContext.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/IapContext.kt @@ -3,11 +3,17 @@ package dev.hyo.openiap import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import dev.hyo.openiap.store.OpenIapStore +import dev.hyo.openiap.utils.activityBindingState +import dev.hyo.openiap.utils.findActivity /** * Compose context helpers for providing OpenIapStore to UI tree @@ -18,9 +24,16 @@ object IapContext { val LocalOpenIapStore: ProvidableCompositionLocal = compositionLocalOf { null } - /** Remember an OpenIapStore bound to application context */ + /** Remember an OpenIapStore and bind the current foreground Activity. */ @Composable fun rememberOpenIapStore(context: Context = LocalContext.current): OpenIapStore { + val store = rememberUnboundOpenIapStore(context) + BindActivity(store, context) + return store + } + + @Composable + private fun rememberUnboundOpenIapStore(context: Context = LocalContext.current): OpenIapStore { val appContext = context.applicationContext return remember(appContext) { OpenIapStore(appContext) } } @@ -28,12 +41,46 @@ object IapContext { /** Provider to attach OpenIapStore to the composition */ @Composable fun OpenIapProvider( - store: OpenIapStore = rememberOpenIapStore(), + store: OpenIapStore = rememberUnboundOpenIapStore(), content: @Composable () -> Unit ) { + BindActivity(store, LocalContext.current) CompositionLocalProvider(LocalOpenIapStore provides store) { content() } } -} + @Composable + private fun BindActivity(store: OpenIapStore, context: Context) { + val activity = remember(context) { context.findActivity() } + val lifecycleOwner = activity as? LifecycleOwner + val bindingOwner = remember(store) { Any() } + DisposableEffect(store, activity, lifecycleOwner, bindingOwner) { + if (activity == null) { + onDispose { } + } else if (lifecycleOwner == null) { + store.bindActivity(bindingOwner, activity) + onDispose { store.unbindActivity(bindingOwner) } + } else { + val observer = LifecycleEventObserver { _, event -> + when (event.activityBindingState()) { + true -> store.bindActivity(bindingOwner, activity) + false -> store.unbindActivity(bindingOwner) + null -> Unit + } + } + lifecycleOwner.lifecycle.addObserver(observer) + // setContent commonly runs from onCreate before ON_START. Bind + // the live Activity now so first-frame initialization can use + // Horizon, then let pause/stop/destroy remove the binding. + if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) { + store.bindActivity(bindingOwner, activity) + } + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + store.unbindActivity(bindingOwner) + } + } + } + } +} diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapViewModel.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapViewModel.kt index 5f94705c7..7a1c5cef1 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapViewModel.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapViewModel.kt @@ -1,9 +1,18 @@ package dev.hyo.openiap +import android.app.Activity +import android.app.Application import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope -import android.app.Application import dev.hyo.openiap.store.OpenIapStore +import dev.hyo.openiap.utils.activityBindingState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -12,15 +21,95 @@ import kotlinx.coroutines.launch */ class OpenIapViewModel(app: Application) : AndroidViewModel(app) { private val store = OpenIapStore(app.applicationContext) + private var activityBindingOwner: Any? = null + private var activityLifecycleOwner: LifecycleOwner? = null + private var activityLifecycleObserver: LifecycleEventObserver? = null + private val cleanupScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) val isConnected: StateFlow = store.isConnected val products = store.products val availablePurchases = store.availablePurchases val status = store.status + /** Supplies the foreground Activity required by Horizon billing. */ + fun setActivity(activity: Activity?) { + if (activity == null) { + clearActivityBinding() + } else { + bindActivity(activity) + } + } + + /** + * Initializes without an Activity. This remains valid for Play and Amazon; + * Horizon callers must use [initConnection] with an Activity. + */ + @Deprecated("Horizon callers must use initConnection(activity, config)") fun initConnection(config: InitConnectionConfig? = null) { viewModelScope.launch { runCatching { store.initConnection(config) } } } + + /** Supplies the foreground Activity and initializes the store connection. */ + fun initConnection(activity: Activity, config: InitConnectionConfig? = null) { + if (!bindActivity(activity)) return + viewModelScope.launch { runCatching { store.initConnection(config) } } + } + + private fun bindActivity(activity: Activity): Boolean { + clearActivityBinding() + + val lifecycleOwner = activity as? LifecycleOwner + if (lifecycleOwner?.lifecycle?.currentState == Lifecycle.State.DESTROYED) { + return false + } + + val bindingOwner = Any() + activityBindingOwner = bindingOwner + store.bindActivity(bindingOwner, activity) + + if (lifecycleOwner != null) { + val observer = LifecycleEventObserver { _, event -> + if (activityBindingOwner === bindingOwner) { + when (event.activityBindingState()) { + true -> store.bindActivity(bindingOwner, activity) + false -> store.unbindActivity(bindingOwner) + null -> Unit + } + if (event == Lifecycle.Event.ON_DESTROY) { + clearActivityBinding() + } + } + } + activityLifecycleOwner = lifecycleOwner + activityLifecycleObserver = observer + lifecycleOwner.lifecycle.addObserver(observer) + } + return true + } + + private fun clearActivityBinding() { + val bindingOwner = activityBindingOwner + activityLifecycleObserver?.let { observer -> + activityLifecycleOwner?.lifecycle?.removeObserver(observer) + } + activityLifecycleObserver = null + activityLifecycleOwner = null + activityBindingOwner = null + if (bindingOwner != null) { + store.unbindActivity(bindingOwner) + } + } + + override fun onCleared() { + clearActivityBinding() + store.clear() + cleanupScope.launch { + runCatching { store.endConnection() } + cleanupScope.cancel() + } + super.onCleared() + } + fun endConnection() { viewModelScope.launch { runCatching { store.endConnection() } } } fun fetchProducts(skus: List, type: ProductQueryType = ProductQueryType.All) { diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt index d19a41735..c3b5b14f7 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt @@ -46,6 +46,7 @@ import dev.hyo.openiap.InAppMessageResultAndroid import dev.hyo.openiap.LaunchExternalLinkParamsAndroid import android.app.Activity import android.content.Context +import dev.hyo.openiap.IapContext import dev.hyo.openiap.OpenIapError import dev.hyo.openiap.OpenIapLog // OpenIapModule is loaded via reflection to support both Play and Horizon flavors @@ -53,6 +54,8 @@ import dev.hyo.openiap.OpenIapProtocol import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener import dev.hyo.openiap.utils.toProduct +import dev.hyo.openiap.utils.findActivity +import dev.hyo.openiap.utils.OwnerScopedValueBinding import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -77,13 +80,28 @@ internal object OpenIapStorePurchaseRequestResolver { * and exposes suspend APIs with observable StateFlows for UI layers to consume. */ class OpenIapStore(private val module: OpenIapProtocol) { + private val manualActivityOwner = Any() + private val activityBindings = OwnerScopedValueBinding(module::setActivity) + init { OpenIapLog.info("Initialized with module: ${module.javaClass.simpleName}", "OpenIapStore") } - constructor(context: Context) : this(buildModule(context, null, null)) - constructor(context: Context, store: String?) : this(buildModule(context, store, null)) - constructor(context: Context, store: String?, appId: String?) : this(buildModule(context, store, appId)) + constructor(context: Context) : this(buildModule(context.applicationContext, null, null)) { + setActivity(context.findActivity()) + } + + constructor(context: Context, store: String?) : this( + buildModule(context.applicationContext, store, null) + ) { + setActivity(context.findActivity()) + } + + constructor(context: Context, store: String?, appId: String?) : this( + buildModule(context.applicationContext, store, appId) + ) { + setActivity(context.findActivity()) + } // Play-specific alternative billing constructors moved to play/store/OpenIapStoreExtensions.kt @@ -188,7 +206,19 @@ class OpenIapStore(private val module: OpenIapProtocol) { // Expose a way to set the current Activity for purchase flows fun setActivity(activity: Activity?) { - module.setActivity(activity) + activityBindings.set(manualActivityOwner, activity) + } + + /** Binds an Activity without allowing another lifecycle owner to clear it. */ + internal fun bindActivity(owner: Any, activity: Activity) { + // A managed lifecycle supersedes the constructor/manual bootstrap value. + activityBindings.set(manualActivityOwner, null) + activityBindings.set(owner, activity) + } + + /** Clears only the Activity registered by [owner]. */ + internal fun unbindActivity(owner: Any) { + activityBindings.set(owner, null) } init { @@ -202,6 +232,7 @@ class OpenIapStore(private val module: OpenIapProtocol) { fun clear() { module.removePurchaseUpdateListener(purchaseUpdateListener) module.removePurchaseErrorListener(purchaseErrorListener) + activityBindings.clear() processedPurchaseTokens.clear() pendingRequestProductId = null storeScope.cancel() @@ -217,6 +248,8 @@ class OpenIapStore(private val module: OpenIapProtocol) { * @param config Optional [InitConnectionConfig]. Use `enableBillingProgramAndroid` to * opt in to External Payments / similar Play Billing programs. Pass `null` for default. * @return `true` once the Play Billing client is connected. + * @throws OpenIapError.MissingCurrentActivity when Horizon has no Activity supplied by + * the constructor, [setActivity], or [IapContext.OpenIapProvider]. * @throws OpenIapError.InitConnection when the billing client fails to initialize * (e.g. Play Store missing, version too old). * diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/ActivityUtils.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/ActivityUtils.kt new file mode 100644 index 000000000..3ae96f48c --- /dev/null +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/ActivityUtils.kt @@ -0,0 +1,28 @@ +package dev.hyo.openiap.utils + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import androidx.lifecycle.Lifecycle + +/** Finds the Activity carried by a UI context without retaining it. */ +internal fun Context.findActivity(): Activity? { + var current: Context = this + while (current is ContextWrapper) { + if (current is Activity) return current + val base = current.baseContext + if (base === current) return null + current = base + } + return current as? Activity +} + +/** Maps lifecycle transitions to foreground Activity binding changes. */ +internal fun Lifecycle.Event.activityBindingState(): Boolean? = when (this) { + Lifecycle.Event.ON_START, + Lifecycle.Event.ON_RESUME -> true + Lifecycle.Event.ON_PAUSE, + Lifecycle.Event.ON_STOP, + Lifecycle.Event.ON_DESTROY -> false + else -> null +} diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/OwnerScopedValueBinding.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/OwnerScopedValueBinding.kt new file mode 100644 index 000000000..9f0248863 --- /dev/null +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/utils/OwnerScopedValueBinding.kt @@ -0,0 +1,39 @@ +package dev.hyo.openiap.utils + +import java.lang.ref.WeakReference + +/** + * Selects the most recently bound live value without letting one owner clear + * another owner's binding. + */ +internal class OwnerScopedValueBinding( + private val onSelectedValueChanged: (T?) -> Unit +) { + private val valuesByOwner = LinkedHashMap>() + + @Synchronized + fun set(owner: Any, value: T?) { + valuesByOwner.remove(owner) + if (value != null) { + valuesByOwner[owner] = WeakReference(value) + } + + var selected: T? = null + val iterator = valuesByOwner.iterator() + while (iterator.hasNext()) { + val valueForOwner = iterator.next().value.get() + if (valueForOwner == null) { + iterator.remove() + } else { + selected = valueForOwner + } + } + onSelectedValueChanged(selected) + } + + @Synchronized + fun clear() { + valuesByOwner.clear() + onSelectedValueChanged(null) + } +} diff --git a/packages/google/openiap/src/test/java/dev/hyo/openiap/utils/OwnerScopedValueBindingTest.kt b/packages/google/openiap/src/test/java/dev/hyo/openiap/utils/OwnerScopedValueBindingTest.kt new file mode 100644 index 000000000..193cb9a84 --- /dev/null +++ b/packages/google/openiap/src/test/java/dev/hyo/openiap/utils/OwnerScopedValueBindingTest.kt @@ -0,0 +1,62 @@ +package dev.hyo.openiap.utils + +import androidx.lifecycle.Lifecycle +import org.junit.Assert.assertEquals +import org.junit.Test + +class OwnerScopedValueBindingTest { + @Test + fun `activity binding state follows foreground lifecycle transitions`() { + assertEquals(true, Lifecycle.Event.ON_START.activityBindingState()) + assertEquals(true, Lifecycle.Event.ON_RESUME.activityBindingState()) + assertEquals(false, Lifecycle.Event.ON_PAUSE.activityBindingState()) + assertEquals(false, Lifecycle.Event.ON_STOP.activityBindingState()) + assertEquals(false, Lifecycle.Event.ON_DESTROY.activityBindingState()) + assertEquals(null, Lifecycle.Event.ON_CREATE.activityBindingState()) + } + + @Test + fun `clearing one owner restores the newest remaining binding`() { + val selections = mutableListOf() + val bindings = OwnerScopedValueBinding(selections::add) + val firstOwner = Any() + val secondOwner = Any() + + bindings.set(firstOwner, "first") + bindings.set(secondOwner, "second") + bindings.set(firstOwner, null) + bindings.set(secondOwner, null) + + assertEquals(listOf("first", "second", "second", null), selections) + } + + @Test + fun `rebinding an owner makes it the current selection`() { + val selections = mutableListOf() + val bindings = OwnerScopedValueBinding(selections::add) + val firstOwner = Any() + val secondOwner = Any() + + bindings.set(firstOwner, "first") + bindings.set(secondOwner, "second") + bindings.set(firstOwner, "first-resumed") + bindings.set(firstOwner, null) + + assertEquals( + listOf("first", "second", "first-resumed", "second"), + selections + ) + } + + @Test + fun `clear removes every owner and selected value`() { + val selections = mutableListOf() + val bindings = OwnerScopedValueBinding(selections::add) + + bindings.set(Any(), "first") + bindings.set(Any(), "second") + bindings.clear() + + assertEquals(listOf("first", "second", null), selections) + } +} diff --git a/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/utils/ActivityUtilsTest.kt b/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/utils/ActivityUtilsTest.kt new file mode 100644 index 000000000..0b5e7915e --- /dev/null +++ b/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/utils/ActivityUtilsTest.kt @@ -0,0 +1,27 @@ +package dev.hyo.openiap.utils + +import android.app.Activity +import android.content.ContextWrapper +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ActivityUtilsTest { + @Test + fun `findActivity unwraps nested UI contexts`() { + val activity = Robolectric.buildActivity(Activity::class.java).create().get() + val wrapped = ContextWrapper(ContextWrapper(activity)) + + assertSame(activity, wrapped.findActivity()) + } + + @Test + fun `findActivity rejects application context`() { + assertNull(ApplicationProvider.getApplicationContext().findActivity()) + } +} From eb88c6b2356a3bef34a2f19509cea287a256f07a Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:08:32 +0900 Subject: [PATCH 4/7] docs: document store API follow-up --- .../ios/promoted-product-listener-ios.tsx | 155 +++++++++++----- .../docs/src/pages/docs/updates/releases.tsx | 172 +++++++++++++++++- 2 files changed, 275 insertions(+), 52 deletions(-) diff --git a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx index 6026183d2..6d27637be 100644 --- a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -67,6 +67,7 @@ IObservable promotedProducts = OpenIapClient.Instance.PromotedProductIOS {{ typescript: ( {`import { + fetchProducts, promotedProductListenerIOS, requestPurchase } from 'expo-iap'; @@ -75,14 +76,26 @@ const subscription = promotedProductListenerIOS(async (product) => { const productId = product.id; console.log('Promoted product tapped:', productId); - // expo-iap and react-native-iap deliver the fetched Product object. - const confirmed = await showPurchaseConfirmation(product); + // Refetch as "all" because the listener's legacy Product payload does not + // distinguish promoted subscriptions at the type level. + const items = (await fetchProducts({ skus: [productId], type: 'all' })) ?? []; + const item = items.find((candidate) => candidate.id === productId); + if (!item) return; + + const confirmed = await showPurchaseConfirmation(item); if (confirmed) { - await requestPurchase({ - request: { apple: { sku: productId } }, - type: 'in-app' - }); + if (item.type === 'subs') { + await requestPurchase({ + request: { apple: { sku: productId } }, + type: 'subs' + }); + } else { + await requestPurchase({ + request: { apple: { sku: productId } }, + type: 'in-app' + }); + } } }); @@ -98,21 +111,35 @@ let subscription = OpenIapModule.shared.promotedProductListenerIOS { productId i do { let result = try await OpenIapModule.shared.fetchProducts( - ProductRequest(skus: [productId], type: .inApp) + ProductRequest(skus: [productId], type: .all) ) - guard case let .products(products) = result, - let product = products?.first, - await showPurchaseConfirmation(product) else { return } + guard case let .all(items) = result, + let item = items?.first else { return } - try await OpenIapModule.shared.requestPurchase( - RequestPurchaseProps( - request: .purchase( - RequestPurchasePropsByPlatforms( - apple: RequestPurchaseIosProps(sku: productId) + switch item { + case let .product(product): + guard await showPurchaseConfirmation(product) else { return } + try await OpenIapModule.shared.requestPurchase( + RequestPurchaseProps( + request: .purchase( + RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: productId) + ) ) ) ) - ) + case let .productSubscription(subscription): + guard await showPurchaseConfirmation(subscription) else { return } + try await OpenIapModule.shared.requestPurchase( + RequestPurchaseProps( + request: .subscription( + RequestSubscriptionPropsByPlatforms( + apple: RequestSubscriptionIosProps(sku: productId) + ) + ) + ) + ) + } } catch { print("Promoted purchase failed: \\(error.localizedDescription)") } @@ -135,23 +162,40 @@ scope.launch { productId ?: return@collect val result = iap.fetchProducts( - ProductRequest(skus = listOf(productId), type = ProductQueryType.InApp) + ProductRequest(skus = listOf(productId), type = ProductQueryType.All) ) - val product = (result as? FetchProductsResultProducts) + val item = (result as? FetchProductsResultAll) ?.value ?.firstOrNull() - if (product != null && showPurchaseConfirmation(product)) { - iap.requestPurchase( - RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - apple = RequestPurchaseIosProps(sku = productId) - ) - ), - type = ProductQueryType.InApp + when (item) { + is ProductOrSubscription.ProductItem -> { + if (!showPurchaseConfirmation(item.value)) return@collect + iap.requestPurchase( + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + apple = RequestPurchaseIosProps(sku = productId) + ) + ), + type = ProductQueryType.InApp + ) ) - ) + } + is ProductOrSubscription.ProductSubscriptionItem -> { + if (!showPurchaseConfirmation(item.value)) return@collect + iap.requestPurchase( + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Subscription( + RequestSubscriptionPropsByPlatforms( + apple = RequestSubscriptionIosProps(sku = productId) + ) + ), + type = ProductQueryType.Subs + ) + ) + } + null -> Unit } } }`} @@ -166,9 +210,9 @@ final subscription = iap.purchasePromoted.listen((productId) async { print('Promoted product tapped: $productId'); // Fetch product details - final products = await iap.fetchProducts( + final products = await iap.fetchProducts( skus: [productId], - type: ProductQueryType.InApp, + type: ProductQueryType.All, ); if (products.isNotEmpty) { @@ -176,13 +220,21 @@ final subscription = iap.purchasePromoted.listen((productId) async { final confirmed = await showPurchaseConfirmation(products.first); if (confirmed) { - // Purchase directly using requestPurchase with the received SKU - await iap.requestPurchase( - RequestPurchaseProps.inApp(( - apple: RequestPurchaseIosProps(sku: productId), - google: null, - )), - ); + if (products.first.type == ProductType.Subs) { + await iap.requestPurchase( + RequestPurchaseProps.subs(( + apple: RequestSubscriptionIosProps(sku: productId), + google: null, + )), + ); + } else { + await iap.requestPurchase( + RequestPurchaseProps.inApp(( + apple: RequestPurchaseIosProps(sku: productId), + google: null, + )), + ); + } } } }); @@ -203,20 +255,31 @@ using var subscription = iap.PromotedProductIOS.Subscribe(async productId => var result = await query.FetchProductsAsync(new ProductRequest { Skus = new[] { productId }, - Type = ProductQueryType.InApp, + Type = ProductQueryType.All, }); - var product = (result as FetchProductsResultProducts)?.Value?.FirstOrDefault(); + var product = (result as FetchProductsResultAll)?.Value?.FirstOrDefault(); if (product is null || !await ShowPurchaseConfirmationAsync(product)) return; - await mutate.RequestPurchaseAsync(new RequestPurchaseProps - { - RequestPurchase = new RequestPurchasePropsByPlatforms + var props = product is ProductSubscription + ? new RequestPurchaseProps { - Apple = new RequestPurchaseIosProps { Sku = productId }, - }, - Type = ProductQueryType.InApp, - }); + RequestSubscription = new RequestSubscriptionPropsByPlatforms + { + Apple = new RequestSubscriptionIosProps { Sku = productId }, + }, + Type = ProductQueryType.Subs, + } + : new RequestPurchaseProps + { + RequestPurchase = new RequestPurchasePropsByPlatforms + { + Apple = new RequestPurchaseIosProps { Sku = productId }, + }, + Type = ProductQueryType.InApp, + }; + + await mutate.RequestPurchaseAsync(props); });`} ), }} diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 212faa843..69859f72c 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -25,6 +25,7 @@ interface Note { } const storeApiModernizationReleases = [ + ['OpenIAP Spec 3.1.0', 'docs-3.1.0'], ['openiap-apple 3.1.0', '3.1.0'], ['openiap-google 3.2.0', 'google-3.2.0'], ['react-native-iap 16.2.0', 'react-native-iap-16.2.0'], @@ -35,6 +36,18 @@ const storeApiModernizationReleases = [ ['OpenIap.Maui 2.2.0', 'maui-iap-2.2.0'], ] as const; +const storeApiFollowupReleases = [ + ['OpenIAP Spec 3.1.1', 'docs-3.1.1'], + ['openiap-apple 3.1.1', '3.1.1'], + ['openiap-google 3.2.1', 'google-3.2.1'], + ['react-native-iap 16.2.1', 'react-native-iap-16.2.1'], + ['expo-iap 5.2.1', 'expo-iap-5.2.1'], + ['flutter_inapp_purchase 10.2.1', 'flutter-iap-10.2.1'], + ['godot-iap 3.2.1', 'godot-iap-3.2.1'], + ['kmp-iap 3.2.1', 'kmp-iap-3.2.1'], + ['OpenIap.Maui 2.2.1', 'maui-iap-2.2.1'], +] as const; + const dependencyModernizationReleases = [ ['openiap-google 3.1.0', 'google-3.1.0'], ['react-native-iap 16.1.0', 'react-native-iap-16.1.0'], @@ -179,6 +192,153 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // August 10, 2026 - Store API follow-up hardening + { + id: 'store-api-follow-up-hardening-2026-08-10', + date: new Date('2026-08-10'), + element: ( +
+ + August 10, 2026 - Store API follow-up hardening + + +

+ Publishes a coordinated patch train from the final cross-SDK + self-review of the Store API modernization release. It closes two + runtime edge cases without removing or renaming public APIs. +

+ +
Common changes
+
    +
  • + Promoted-product examples now refetch mixed product details and + select the subscription request branch when the promoted item is a + subscription. The Advanced Commerce period field also carries its + precise OpenIAP and Apple availability in every generated SDK. +
  • +
+ +
+ Shared spec and native packages +
+
    +
  • + OpenIAP Spec 3.1.1 - synchronizes the Advanced + Commerce period availability clarification used by every generated + SDK. +
  • +
  • + openiap-apple 3.1.1 - exclusively reserves an + externally redeemed win-back offer for one matching promoted + purchase attempt. Local validation and presentation failures + release the reservation for a safe retry, while newer purchase + intents cannot be overwritten by a stale attempt. +
  • +
  • + openiap-google 3.2.1 - makes native convenience + Activity binding lifecycle-aware and owner-scoped. A paused or + disposed Compose owner no longer clears another active owner, and + Horizon ViewModel callers have an explicit Activity-based + initialization path. +
  • +
+ +
Framework libraries
+
    +
  • + react-native-iap 16.2.1 - consumes the corrected + native behavior and synchronized generated contract comments. +
  • +
  • + expo-iap 5.2.1 - consumes the corrected native + behavior and synchronized generated contract comments. +
  • +
  • + flutter_inapp_purchase 10.2.1 - synchronizes the + generated Apple availability contract. +
  • +
  • + godot-iap 3.2.1 - synchronizes the generated + Apple availability contract. +
  • +
  • + kmp-iap 3.2.1 - synchronizes the generated Apple + availability contract. +
  • +
  • + OpenIap.Maui 2.2.1 - synchronizes the generated + Apple availability contract. +
  • +
+ +
Integration notes
+

+ Framework API call sites remain unchanged. Native Android ViewModel + users targeting Horizon should migrate from the deprecated + initConnection(config) overload to{' '} + initConnection(activity, config); Compose users receive + lifecycle binding automatically. Upgrade all coordinated packages to + the versions below so the native fixes and generated contracts stay + aligned. +

+ +
+
Package Releases
+
    + {storeApiFollowupReleases.map(([label, tag]) => ( +
  • + + {label} + +
  • + ))} +
+
+
+ ), + }, + // August 10, 2026 - Store API contract modernization { id: 'store-api-contract-modernization-2026-08-10', @@ -225,12 +385,12 @@ function Releases() { }} >
  • - openiap-apple 3.1.0 preserves externally redeemed - win-back offers until the matching promoted purchase is completed, - selects the newest verified current entitlement deterministically, - and returns a verified redemption purchase on Apple 27+ when built - with Xcode 27+. Older supported system-sheet paths continue to - return null after presentation. + openiap-apple 3.1.0 captures externally redeemed + win-back offers for matching promoted purchases, selects the + newest verified current entitlement deterministically, and returns + a verified redemption purchase on Apple 27+ when built with Xcode + 27+. Older supported system-sheet paths continue to return{' '} + null after presentation.
  • openiap-google 3.2.0 requests suspended From 1bdd0175f9c578b7a1d2c7eecca929a1d25542f3 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:09:56 +0900 Subject: [PATCH 5/7] chore(skills): enforce KISS and SSOT reviews --- .claude/commands/audit-code.md | 5 ++- .claude/commands/review-pr.md | 2 + .claude/skills/review-self/SKILL.md | 20 ++++----- .codex/skills/openiap-workflows/SKILL.md | 2 + .codex/skills/review-self/SKILL.md | 46 +++++++++++--------- .codex/skills/review-self/agents/openai.yaml | 4 +- AGENTS.md | 9 +++- 7 files changed, 54 insertions(+), 34 deletions(-) diff --git a/.claude/commands/audit-code.md b/.claude/commands/audit-code.md index d829146bd..eec5a8ad0 100644 --- a/.claude/commands/audit-code.md +++ b/.claude/commands/audit-code.md @@ -148,6 +148,8 @@ Compare current implementation against latest platform APIs: **Internal Rules Compliance:** +- [ ] Canonical KISS/SSOT rules in `03-coding-style.md` pass + packages/apple (Swift): - [ ] iOS-specific functions end with `IOS` suffix @@ -192,7 +194,8 @@ After identifying issues: 1. Read the relevant knowledge file for the rule 2. Read the violating code file -3. Fix the code to comply with the rule +3. Fix the code to comply with the rule, simplifying or consolidating before + adding another layer 4. For missing features: add to roadmap or implement ### 7. Update Documentation diff --git a/.claude/commands/review-pr.md b/.claude/commands/review-pr.md index cef53cac3..e2c5fc2f3 100644 --- a/.claude/commands/review-pr.md +++ b/.claude/commands/review-pr.md @@ -31,6 +31,8 @@ Based on changed files, run these checks BEFORE committing: When reviewing, check these project-specific rules: +- **KISS/SSOT**: Enforce the canonical release rules in + `knowledge/internal/03-coding-style.md` - **iOS functions**: Must end with `IOS` suffix (e.g., `syncIOS`) - **Android functions in packages/google**: NO `Android` suffix (it's Android-only) - **Generated files**: Do NOT edit `packages/apple/Sources/Models/Types.swift` or `packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt` diff --git a/.claude/skills/review-self/SKILL.md b/.claude/skills/review-self/SKILL.md index 2aef49208..d1f540ac6 100644 --- a/.claude/skills/review-self/SKILL.md +++ b/.claude/skills/review-self/SKILL.md @@ -1,26 +1,26 @@ --- name: review-self -description: Independently review and improve the agent's current implementation, working-tree changes, or pull request; fix actionable in-scope gaps; rerun relevant verification; and recheck at five-minute intervals until the work is stable or genuinely blocked. Use when the user says "review-self", asks Claude to review its own changes, requests a self-review loop, or wants current work monitored for new issues after implementation. +description: Independently review and simplify the agent's current implementation, working-tree changes, or pull request; enforce KISS and repository SSOT rules, fix actionable in-scope gaps, rerun relevant verification, and recheck at the user-requested interval (five minutes by default) until stable or genuinely blocked. Use when the user says "review-self", asks Claude to review its own changes, requests a self-review loop, or wants current work monitored for new issues after implementation. --- # Review Self (Claude Code) The canonical loop definition lives in `.codex/skills/review-self/SKILL.md`. Read it and follow every section — authority and scope preservation, target -establishment, the review round, related OpenIAP workflows, the five-minute -recheck contract, safe stopping conditions, and per-round communication are -agent-agnostic and apply as written. +establishment, the KISS/SSOT review round, related OpenIAP workflows, the +requested-interval recheck contract, safe stopping conditions, and per-round +communication are agent-agnostic and apply as written. ## Claude Code Notes - Where the canonical file routes through `$openiap-workflows`, read the matching `.claude/commands/*.md` file directly (or use the `.claude/skills/openiap-workflows` skill). -- For the five-minute recheck, use Claude's real wake-up mechanism for the - current surface (for example a scheduled reminder / wake-up tool in Cowork - or the Agent SDK). If no such mechanism is available in the current session, - complete the current round and report that automatic re-entry could not be - scheduled — never emulate the loop with `sleep 300`, `while true`, or an - abandoned background process. +- For interval rechecks, use Claude's real wake-up mechanism for the current + surface (for example a scheduled reminder / wake-up tool in Cowork or the + Agent SDK). Honor the user's interval, defaulting to five minutes. If no such + mechanism is available in the current session, complete the current round and + report that automatic re-entry could not be scheduled — never emulate the + loop with `sleep`, `while true`, or an abandoned background process. - Use read-only subagents (Task/Agent tool with an Explore-style agent) for independent review lenses on large or cross-cutting diffs. diff --git a/.codex/skills/openiap-workflows/SKILL.md b/.codex/skills/openiap-workflows/SKILL.md index f7fec9faa..f7b4a8ff7 100644 --- a/.codex/skills/openiap-workflows/SKILL.md +++ b/.codex/skills/openiap-workflows/SKILL.md @@ -69,6 +69,8 @@ appropriate labels before merging. ## Non-Negotiables +- Apply the canonical KISS/SSOT release criteria in + `knowledge/internal/03-coding-style.md`. - Before any public GitHub write, apply the English-only communication guard in `knowledge/internal/06-git-deployment.md`. Private maintainer conversation language must never leak into issue, PR, review, release, or commit prose. diff --git a/.codex/skills/review-self/SKILL.md b/.codex/skills/review-self/SKILL.md index 02b4377e1..de00ba451 100644 --- a/.codex/skills/review-self/SKILL.md +++ b/.codex/skills/review-self/SKILL.md @@ -1,12 +1,13 @@ --- name: review-self -description: Independently review and improve Codex's current implementation, working-tree changes, or pull request; fix actionable in-scope gaps; rerun relevant verification; and recheck at five-minute intervals until the work is stable or genuinely blocked. Use when the user says "review-self", asks Codex to review its own changes, requests a self-review loop, or wants current work monitored for new issues after implementation. +description: Independently review and simplify Codex's current implementation, working-tree changes, or pull request; enforce KISS and repository SSOT rules, fix actionable in-scope gaps, rerun relevant verification, and recheck at the user-requested interval (five minutes by default) until stable or genuinely blocked. Use when the user says "review-self", asks Codex to review its own changes, requests a self-review loop, or wants current work monitored for new issues after implementation. --- # Review Self -Review the current work immediately, fix validated gaps, and use five-minute -confirmation rounds until the result is stable. +Review the current work immediately, fix validated gaps, and use confirmation +rounds at the user's requested interval, or five minutes by default, until the +result is stable. ## Preserve Authority And Scope @@ -51,15 +52,18 @@ confirmation rounds until the result is stable. - public contracts, naming, compatibility, generated-file rules, and cross-package or SDK parity; - missing or weak tests, documentation, examples, migrations, and operational - safeguards required by the change. + safeguards required by the change; + - the canonical KISS/SSOT release rules in + `knowledge/internal/03-coding-style.md`. 4. Use independent read-only subagents for separate review lenses when the diff is large or cross-cutting. Give them the raw target and request, not suspected findings or an expected answer. 5. Validate every finding against the current code and applicable instructions. Reject pure taste, cosmetic churn, duplicate findings, and unrelated nice-to-have work. -6. Fix all validated in-scope findings in one coherent batch. Regenerate generated - files only through their documented generator and preserve unrelated edits. +6. Fix all validated in-scope findings in one coherent batch under those + canonical KISS/SSOT rules. Regenerate generated files only through their + documented generator and preserve unrelated edits. 7. Re-read the resulting diff, then run the path-specific lint, typecheck, tests, builds, audits, and `git diff --check` required by the repository. Do not rerun an expensive unchanged check fingerprint unless new state can affect it. @@ -105,7 +109,7 @@ the current PR head: requirements, and acceptance criteria. Preserve the commit/push and external write authority supplied by the calling workflow. - Do not re-enter `review-pr`, request external reviewers, handle its trigger - comments, invoke this fallback again, or schedule this skill's five-minute + comments, invoke this fallback again, or schedule this skill's recurring loop. `review-pr` remains the sole thread-handling and polling owner. - Return the reviewed head and working-tree fingerprints, validated findings and fixes, checks run, and a clean or blocked result. The caller may cache a clean @@ -114,22 +118,23 @@ the current PR head: This single-round override prevents nested polling loops while still replacing the missing external review coverage with the full self-review procedure. -## Recheck Every Five Minutes +## Recheck At The Requested Interval - Run the first round immediately; never wait before the initial review. - If the user explicitly requests one pass, finish after that round and do not schedule a confirmation. - Use the product's real recurring-monitor or wake-up mechanism to schedule the - next round for 300 seconds after the current round. Make the scheduled prompt - explicitly invoke `$review-self`; re-enter this skill rather than trying to - perform semantic review inside a shell loop. + next round after the user's explicit interval, or 300 seconds by default. + Make the scheduled prompt explicitly invoke `$review-self`; re-enter this + skill rather than trying to perform semantic review inside a shell loop. - Keep at most one outstanding wake-up for the same target. Cancel or supersede stale duplicate wake-ups when the mechanism supports it. - Carry a compact state capsule in the scheduled prompt or monitor state. Include the original goal and acceptance criteria, target and base, head/tree and working-tree fingerprints, seen feedback and check IDs, poll count, clean - count, start time, finding fingerprints with fix attempts, and the existing - commit/push authority. + count, start time, the current pending-state fingerprint and first-seen time, + finding fingerprints with fix attempts, and the existing commit/push + authority. - Keep loop state out of tracked repository files. Revalidate it against disk and remote state on every wake-up. - Increment the clean count only after a complete round has no actionable @@ -137,15 +142,16 @@ the missing external review coverage with the full self-review procedure. successful or explicitly allowed to skip, no actionable feedback remains, and the final diff has been reread. Reset it when any material state or finding changes, then allow the fully clean post-change result to start a new count. -- Finish successfully after two consecutive clean snapshots separated by a - five-minute interval. Continue without a fixed round cap while new findings or +- Finish successfully after two consecutive clean snapshots separated by the + active interval. Continue without a fixed round cap while new findings or state changes lead to meaningful progress. -- Treat pending CI or review automation as neither clean nor failed. Poll it every - five minutes without repeating expensive local checks on an unchanged - fingerprint. +- Treat pending CI or review automation as neither clean nor failed. Poll it at + the active interval without repeating expensive local checks on an unchanged + fingerprint. Reset its first-seen time whenever the pending-state fingerprint + changes. - If no recurring mechanism is available, complete the current round and report that automatic re-entry could not be scheduled. Do not emulate it with - `sleep 300`, `while true`, `nohup`, or an abandoned background process, and + `sleep`, `while true`, `nohup`, or an abandoned background process, and never claim a future pass was scheduled unless the mechanism actually accepted it. @@ -161,7 +167,7 @@ Stop the loop and report the exact state when any of these conditions holds: - the same root finding remains after two fix attempts; - the same authentication, rate-limit, tool, or environment failure blocks three consecutive rounds; -- only unchanged pending external state remains after 12 polls (about one hour). +- only unchanged pending external state remains for at least one hour. Do not call a blocked or interrupted result clean. Do not stop merely because one poll is a no-op while asynchronous state is still pending. diff --git a/.codex/skills/review-self/agents/openai.yaml b/.codex/skills/review-self/agents/openai.yaml index 2d9e9ca22..74da970ca 100644 --- a/.codex/skills/review-self/agents/openai.yaml +++ b/.codex/skills/review-self/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Review Self" - short_description: "Review current work in five-minute loops" - default_prompt: "Use $review-self to review the current work, fix actionable findings, and recheck it every five minutes until stable." + short_description: "Review and simplify current work until stable" + default_prompt: "Use $review-self to review and simplify the current work under KISS and SSOT rules, fix actionable findings, and recheck at the requested interval until stable." diff --git a/AGENTS.md b/AGENTS.md index c80512384..b3404cae1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,6 +74,12 @@ Before posting or editing issues, pull requests, reviews, discussions, commits, release notes, or GitHub Releases, follow the mandatory language guard in [`knowledge/internal/06-git-deployment.md`](knowledge/internal/06-git-deployment.md#public-github-communication-language). +### KISS and SSOT + +KISS and SSOT are mandatory release criteria. The canonical rules live in +[`knowledge/internal/03-coding-style.md`](knowledge/internal/03-coding-style.md#0-kiss-and-ssot-are-release-requirements). +Apply that section before implementation and during every review. + ### Platform Function Naming - **iOS functions**: Must end with `IOS` suffix (e.g., `syncIOS`, `getReceiptDataIOS`) @@ -255,7 +261,8 @@ keep its wording agent-neutral. 1. Reviews the complete current diff, including staged, unstaged, and untracked work, against the original request and repository conventions 2. Fixes validated in-scope findings and runs path-specific verification -3. Rechecks through a real recurring wake-up after five minutes +3. Rechecks through a real recurring wake-up at the user's requested interval, + defaulting to five minutes 4. Finishes after two consecutive clean snapshots, or reports the exact blocker `review-self` does not grant commit, push, PR, merge, deploy, or release authority From c0b9f70687c6d1b94f0e1abf7f42346b027c8710 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:10:16 +0900 Subject: [PATCH 6/7] docs(knowledge): codify KISS and SSOT rules --- knowledge/_claude-context/context.md | 24 +++++++++++++++++++++++- knowledge/internal/03-coding-style.md | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 4e31db829..a259b14d6 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-08-09T13:54:08.141Z +> Last updated: 2026-08-09T21:46:58.647Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -541,6 +541,28 @@ isBillingProgramAvailableAndroid(program: BillingProgramAndroid!): BillingProgra ## General Principles +### 0. KISS and SSOT Are Release Requirements + +Prefer the simplest correct design that satisfies verified requirements. KISS +does not justify skipping error handling, lifecycle safety, tests, or public +contracts; it requires meeting them with the fewest independent concepts. + +- Give each stateful resource one clear owner and one terminal cleanup path. + Pass or reference that owner instead of creating fallback stores, scopes, + caches, or managers at multiple layers. +- Keep each fact in one canonical source. Generate or link mirrors and adapters; + never maintain equivalent rules, versions, schemas, or lifecycle decisions in + parallel files. +- Reuse an existing abstraction when it already owns the invariant. Add a new + helper only for real reuse, a necessary platform boundary, or isolated testing; + keep single-use helpers local. +- Do not add speculative configuration, indirection, background work, or state. + Every new layer must name the invariant it protects, its owner, its cleanup, + and the test that proves it is needed. +- When fixing a bug, first look for state or code that can be deleted or + consolidated. Prefer one understandable path over several defensive fallback + paths. + ### 1. Explicit Over Implicit Always be explicit about types and intentions: diff --git a/knowledge/internal/03-coding-style.md b/knowledge/internal/03-coding-style.md index fe8b6f6b8..dd32c80ec 100644 --- a/knowledge/internal/03-coding-style.md +++ b/knowledge/internal/03-coding-style.md @@ -5,6 +5,28 @@ ## General Principles +### 0. KISS and SSOT Are Release Requirements + +Prefer the simplest correct design that satisfies verified requirements. KISS +does not justify skipping error handling, lifecycle safety, tests, or public +contracts; it requires meeting them with the fewest independent concepts. + +- Give each stateful resource one clear owner and one terminal cleanup path. + Pass or reference that owner instead of creating fallback stores, scopes, + caches, or managers at multiple layers. +- Keep each fact in one canonical source. Generate or link mirrors and adapters; + never maintain equivalent rules, versions, schemas, or lifecycle decisions in + parallel files. +- Reuse an existing abstraction when it already owns the invariant. Add a new + helper only for real reuse, a necessary platform boundary, or isolated testing; + keep single-use helpers local. +- Do not add speculative configuration, indirection, background work, or state. + Every new layer must name the invariant it protects, its owner, its cleanup, + and the test that proves it is needed. +- When fixing a bug, first look for state or code that can be deleted or + consolidated. Prefer one understandable path over several defensive fallback + paths. + ### 1. Explicit Over Implicit Always be explicit about types and intentions: From aa8d083a3d447667d9decb6b815dde9bdd841206 Mon Sep 17 00:00:00 2001 From: Hyo Date: Mon, 10 Aug 2026 07:44:20 +0900 Subject: [PATCH 7/7] fix: address store API review feedback --- .claude/commands/review-pr.md | 7 +- .codex/skills/review-self/SKILL.md | 8 +- libraries/maui-iap/src/OpenIap.Maui/Types.cs | 1832 ++++++++++------- .../ios/promoted-product-listener-ios.tsx | 266 ++- .../hyo/martie/screens/AllProductsScreen.kt | 6 +- .../screens/AvailablePurchasesScreen.kt | 6 +- .../dev/hyo/martie/screens/OfferCodeScreen.kt | 6 +- .../hyo/martie/screens/OpenIapStoreContext.kt | 6 +- .../hyo/martie/screens/PurchaseFlowScreen.kt | 6 +- .../martie/screens/SubscriptionFlowScreen.kt | 6 +- .../dev/hyo/openiap/store/OpenIapStore.kt | 2 + packages/gql/codegen/plugins/csharp.ts | 8 +- packages/gql/src/codegen-defaults.test.ts | 12 + packages/gql/src/generated/Types.cs | 1832 ++++++++++------- 14 files changed, 2482 insertions(+), 1521 deletions(-) diff --git a/.claude/commands/review-pr.md b/.claude/commands/review-pr.md index e2c5fc2f3..f3a2b4b28 100644 --- a/.claude/commands/review-pr.md +++ b/.claude/commands/review-pr.md @@ -106,9 +106,10 @@ When fallback is required: acceptance criteria, changed-path conventions, and existing commit/push authority. Explicitly request one pass so `review-pr` remains the only polling owner. -3. Do not let the fallback round re-enter `review-pr`, request reviewers, or - schedule its own five-minute loop. It may inspect current review/CI evidence, - but this workflow owns thread handling and polling. +3. Do not let the fallback round re-enter `review-pr`, request reviewers, handle + trigger comments, invoke this fallback again, or schedule any recurring loop. + It may inspect current review/CI evidence, but this workflow owns thread + handling and polling. 4. Fix and verify every validated finding using the normal response rules. If a fix changes the head, request CodeRabbit again after the fix batch and run fallback again only if it remains unavailable for the new head. diff --git a/.codex/skills/review-self/SKILL.md b/.codex/skills/review-self/SKILL.md index de00ba451..0db9a54d5 100644 --- a/.codex/skills/review-self/SKILL.md +++ b/.codex/skills/review-self/SKILL.md @@ -132,11 +132,13 @@ the missing external review coverage with the full self-review procedure. - Carry a compact state capsule in the scheduled prompt or monitor state. Include the original goal and acceptance criteria, target and base, head/tree and working-tree fingerprints, seen feedback and check IDs, poll count, clean - count, start time, the current pending-state fingerprint and first-seen time, - finding fingerprints with fix attempts, and the existing commit/push - authority. + count, start time, the requested interval, the active interval, the current + pending-state fingerprint and first-seen time, finding fingerprints with fix + attempts, and the existing commit/push authority. - Keep loop state out of tracked repository files. Revalidate it against disk and remote state on every wake-up. +- Validate the requested and active intervals on every wake-up. Preserve an + explicit user interval unless the user changes it; otherwise use the default. - Increment the clean count only after a complete round has no actionable findings, all requested verification passes, required checks are terminal and successful or explicitly allowed to skip, no actionable feedback remains, and diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index eff98837c..904799465 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -17,8 +17,10 @@ namespace OpenIap; // Enums // ============================================================================ -/// Play Billing choice image layout (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Play Billing choice image layout (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(BillingChoiceImageLayoutAndroidJsonConverter))] public enum BillingChoiceImageLayoutAndroid { @@ -72,8 +74,10 @@ public static class BillingChoiceImageLayoutAndroidExtensions public static BillingChoiceImageLayoutAndroid FromJson(string value) => BillingChoiceImageLayoutAndroidJsonConverter.FromRawString(value); } -/// Choice screen renderer for Billing Choice availability (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Choice screen renderer for Billing Choice availability (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(BillingChoiceScreenTypeAndroidJsonConverter))] public enum BillingChoiceScreenTypeAndroid { @@ -127,36 +131,48 @@ public static class BillingChoiceScreenTypeAndroidExtensions public static BillingChoiceScreenTypeAndroid FromJson(string value) => BillingChoiceScreenTypeAndroidJsonConverter.FromRawString(value); } -/// Billing program types for Google Play Billing Programs (Android) -/// Available in Google Play Billing Library 8.2.0 (External Offer and External Content Link -/// integrations require 8.2.1+), EXTERNAL_PAYMENTS added in 8.3.0, -/// BILLING_CHOICE added in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (requires Play Billing 9.1.0+). +/// +/// Billing program types for Google Play Billing Programs (Android) +/// Available in Google Play Billing Library 8.2.0 (External Offer and External Content Link +/// integrations require 8.2.1+), EXTERNAL_PAYMENTS added in 8.3.0, +/// BILLING_CHOICE added in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(BillingProgramAndroidJsonConverter))] public enum BillingProgramAndroid { /// Unspecified billing program. Do not use. Unspecified, - /// User Choice Billing program. - /// User can select between Google Play Billing or alternative billing. - /// Available in Google Play Billing Library 7.0+ + /// + /// User Choice Billing program. + /// User can select between Google Play Billing or alternative billing. + /// Available in Google Play Billing Library 7.0+ + /// UserChoiceBilling, - /// External Content Links program. - /// Allows linking to external content outside the app. - /// Available in Google Play Billing Library 8.2.0+ + /// + /// External Content Links program. + /// Allows linking to external content outside the app. + /// Available in Google Play Billing Library 8.2.0+ + /// ExternalContentLink, - /// External Offers program. - /// Allows offering digital content purchases outside the app. - /// Available in Google Play Billing Library 8.2.0+ + /// + /// External Offers program. + /// Allows offering digital content purchases outside the app. + /// Available in Google Play Billing Library 8.2.0+ + /// ExternalOffer, - /// External Payments program (Japan only). - /// Allows presenting a side-by-side choice between Google Play Billing and developer's external payment option. - /// Users can choose to complete the purchase on the developer's website. - /// Available in Google Play Billing Library 8.3.0+ + /// + /// External Payments program (Japan only). + /// Allows presenting a side-by-side choice between Google Play Billing and developer's external payment option. + /// Users can choose to complete the purchase on the developer's website. + /// Available in Google Play Billing Library 8.3.0+ + /// ExternalPayments, - /// Billing Choice program. - /// Allows presenting Google Play Billing alongside an alternative in-app billing system or external web link. - /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Billing Choice program. + /// Allows presenting Google Play Billing alongside an alternative in-app billing system or external web link. + /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// BillingChoice } @@ -211,19 +227,25 @@ public static class BillingProgramAndroidExtensions public static BillingProgramAndroid FromJson(string value) => BillingProgramAndroidJsonConverter.FromRawString(value); } -/// Launch mode for developer billing option (Android) -/// Determines how the external payment URL is launched -/// Available in Google Play Billing Library 8.3.0+ +/// +/// Launch mode for developer billing option (Android) +/// Determines how the external payment URL is launched +/// Available in Google Play Billing Library 8.3.0+ +/// [JsonConverter(typeof(DeveloperBillingLaunchModeAndroidJsonConverter))] public enum DeveloperBillingLaunchModeAndroid { /// Unspecified launch mode. Do not use. Unspecified, - /// Google Play will launch the link in an external browser or eligible app. - /// Use this when you want Play to handle launching the external payment URL. + /// + /// Google Play will launch the link in an external browser or eligible app. + /// Use this when you want Play to handle launching the external payment URL. + /// LaunchInExternalBrowserOrApp, - /// The caller app will launch the link after Play returns control. - /// Use this when you want to handle launching the external payment URL yourself. + /// + /// The caller app will launch the link after Play returns control. + /// Use this when you want to handle launching the external payment URL yourself. + /// CallerWillLaunchLink } @@ -269,8 +291,10 @@ public static class DeveloperBillingLaunchModeAndroidExtensions public static DeveloperBillingLaunchModeAndroid FromJson(string value) => DeveloperBillingLaunchModeAndroidJsonConverter.FromRawString(value); } -/// Developer-provided billing destination type for Billing Program reporting details (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Developer-provided billing destination type for Billing Program reporting details (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(DeveloperBillingTypeAndroidJsonConverter))] public enum DeveloperBillingTypeAndroid { @@ -324,8 +348,10 @@ public static class DeveloperBillingTypeAndroidExtensions public static DeveloperBillingTypeAndroid FromJson(string value) => DeveloperBillingTypeAndroidJsonConverter.FromRawString(value); } -/// Discount offer type enumeration. -/// Categorizes the type of discount or promotional offer. +/// +/// Discount offer type enumeration. +/// Categorizes the type of discount or promotional offer. +/// [JsonConverter(typeof(DiscountOfferTypeJsonConverter))] public enum DiscountOfferType { @@ -600,10 +626,12 @@ public static class ErrorCodeExtensions public static ErrorCode FromJson(string value) => ErrorCodeJsonConverter.FromRawString(value); } -/// Launch mode for external link flow (Android) -/// Determines how the external URL is launched -/// Introduced in Google Play Billing Library 8.2.0. External Offer and External Content Link -/// integrations require 8.2.1+ and fresh details immediately before every redirect session. +/// +/// Launch mode for external link flow (Android) +/// Determines how the external URL is launched +/// Introduced in Google Play Billing Library 8.2.0. External Offer and External Content Link +/// integrations require 8.2.1+ and fresh details immediately before every redirect session. +/// [JsonConverter(typeof(ExternalLinkLaunchModeAndroidJsonConverter))] public enum ExternalLinkLaunchModeAndroid { @@ -657,9 +685,11 @@ public static class ExternalLinkLaunchModeAndroidExtensions public static ExternalLinkLaunchModeAndroid FromJson(string value) => ExternalLinkLaunchModeAndroidJsonConverter.FromRawString(value); } -/// Link type for external link flow (Android) -/// Specifies the type of external link destination -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Link type for external link flow (Android) +/// Specifies the type of external link destination +/// Available in Google Play Billing Library 8.2.0+ +/// [JsonConverter(typeof(ExternalLinkTypeAndroidJsonConverter))] public enum ExternalLinkTypeAndroid { @@ -713,14 +743,18 @@ public static class ExternalLinkTypeAndroidExtensions public static ExternalLinkTypeAndroid FromJson(string value) => ExternalLinkTypeAndroidJsonConverter.FromRawString(value); } -/// Notice types for ExternalPurchaseCustomLink (iOS 18.1+). -/// Determines the style of disclosure notice to display. -/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/noticetype +/// +/// Notice types for ExternalPurchaseCustomLink (iOS 18.1+). +/// Determines the style of disclosure notice to display. +/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/noticetype +/// [JsonConverter(typeof(ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter))] public enum ExternalPurchaseCustomLinkNoticeTypeIOS { - /// Notice type indicating external purchases will be displayed in a browser - /// or destination of the app's choice. + /// + /// Notice type indicating external purchases will be displayed in a browser + /// or destination of the app's choice. + /// Browser } @@ -761,17 +795,23 @@ public static class ExternalPurchaseCustomLinkNoticeTypeIOSExtensions public static ExternalPurchaseCustomLinkNoticeTypeIOS FromJson(string value) => ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter.FromRawString(value); } -/// Token types for ExternalPurchaseCustomLink (iOS 18.1+). -/// Used to request different types of external purchase tokens for reporting to Apple. -/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) +/// +/// Token types for ExternalPurchaseCustomLink (iOS 18.1+). +/// Used to request different types of external purchase tokens for reporting to Apple. +/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) +/// [JsonConverter(typeof(ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter))] public enum ExternalPurchaseCustomLinkTokenTypeIOS { - /// Token for customer acquisition tracking. - /// Use this when a new customer makes their first purchase through external link. + /// + /// Token for customer acquisition tracking. + /// Use this when a new customer makes their first purchase through external link. + /// Acquisition, - /// Token for ongoing services tracking. - /// Use this for existing customers making additional purchases. + /// + /// Token for ongoing services tracking. + /// Use this for existing customers making additional purchases. + /// Services } @@ -874,16 +914,20 @@ public enum IapEvent PurchaseError, PromotedProductIOS, UserChoiceBillingAndroid, - /// Fired for External Payments (8.3.0+) and Google-rendered Billing Choice - /// developer billing selections on Android. Billing Choice is available in - /// OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Fired for External Payments (8.3.0+) and Google-rendered Billing Choice + /// developer billing selections on Android. Billing Choice is available in + /// OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// DeveloperProvidedBillingAndroid, - /// Fired when a subscription enters a billing-issue state that requires user attention. - /// A StoreKit billing-retry subscription may no longer be a current entitlement. - /// Cross-platform unification of StoreKit 2 Message.billingIssue (iOS 16.4+, - /// Mac Catalyst 16.4+, visionOS 1.0+) and - /// Play Billing 8.1+ isSuspended. NOT emitted by Amazon Appstore or the Horizon - /// flavor, whose Billing Compatibility SDK implements only Play Billing 7.0. + /// + /// Fired when a subscription enters a billing-issue state that requires user attention. + /// A StoreKit billing-retry subscription may no longer be a current entitlement. + /// Cross-platform unification of StoreKit 2 Message.billingIssue (iOS 16.4+, + /// Mac Catalyst 16.4+, visionOS 1.0+) and + /// Play Billing 8.1+ isSuspended. NOT emitted by Amazon Appstore or the Horizon + /// flavor, whose Billing Compatibility SDK implements only Play Billing 7.0. + /// SubscriptionBillingIssue } @@ -1192,9 +1236,11 @@ public static class IapStoreExtensions public static IapStore FromJson(string value) => IapStoreJsonConverter.FromRawString(value); } -/// High-level in-app message category (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// High-level in-app message category (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// [JsonConverter(typeof(InAppMessageCategoryAndroidJsonConverter))] public enum InAppMessageCategoryAndroid { @@ -1243,9 +1289,11 @@ public static class InAppMessageCategoryAndroidExtensions public static InAppMessageCategoryAndroid FromJson(string value) => InAppMessageCategoryAndroidJsonConverter.FromRawString(value); } -/// Response code from Play billing in-app messages (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// Response code from Play billing in-app messages (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// [JsonConverter(typeof(InAppMessageResponseCodeAndroidJsonConverter))] public enum InAppMessageResponseCodeAndroid { @@ -1294,8 +1342,10 @@ public static class InAppMessageResponseCodeAndroidExtensions public static InAppMessageResponseCodeAndroid FromJson(string value) => InAppMessageResponseCodeAndroidJsonConverter.FromRawString(value); } -/// Payment mode for subscription offers. -/// Determines how the user pays during the offer period. +/// +/// Payment mode for subscription offers. +/// Determines how the user pays during the offer period. +/// [JsonConverter(typeof(PaymentModeJsonConverter))] public enum PaymentMode { @@ -1469,10 +1519,12 @@ public static class ProductQueryTypeExtensions public static ProductQueryType FromJson(string value) => ProductQueryTypeJsonConverter.FromRawString(value); } -/// Status code for individual products returned from queryProductDetailsAsync (Android) -/// Prior to 8.0, products that couldn't be fetched were simply not returned. -/// With 8.0+, these products are returned with a status code explaining why. -/// Available in Google Play Billing Library 8.0.0+ +/// +/// Status code for individual products returned from queryProductDetailsAsync (Android) +/// Prior to 8.0, products that couldn't be fetched were simply not returned. +/// With 8.0+, these products are returned with a status code explaining why. +/// Available in Google Play Billing Library 8.0.0+ +/// [JsonConverter(typeof(ProductStatusAndroidJsonConverter))] public enum ProductStatusAndroid { @@ -1745,8 +1797,10 @@ public static class PurchaseVerificationProviderExtensions public static PurchaseVerificationProvider FromJson(string value) => PurchaseVerificationProviderJsonConverter.FromRawString(value); } -/// Sub-response codes for more granular purchase error information (Android) -/// Available in Google Play Billing Library 8.0.0+ +/// +/// Sub-response codes for more granular purchase error information (Android) +/// Available in Google Play Billing Library 8.0.0+ +/// [JsonConverter(typeof(SubResponseCodeAndroidJsonConverter))] public enum SubResponseCodeAndroid { @@ -1861,8 +1915,10 @@ public enum SubscriptionOfferTypeIOS { Introductory, Promotional, - /// Win-back offer type (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. + /// + /// Win-back offer type (iOS 18+) + /// Used to re-engage churned subscribers with a discount or free trial. + /// WinBack } @@ -2038,9 +2094,11 @@ public static class SubscriptionPeriodUnitExtensions public static SubscriptionPeriodUnit FromJson(string value) => SubscriptionPeriodUnitJsonConverter.FromRawString(value); } -/// Replacement mode for subscription changes (Android) -/// These modes determine how the subscription replacement affects billing. -/// Available in Google Play Billing Library 8.1.0+ +/// +/// Replacement mode for subscription changes (Android) +/// These modes determine how the subscription replacement affects billing. +/// Available in Google Play Billing Library 8.1.0+ +/// [JsonConverter(typeof(SubscriptionReplacementModeAndroidJsonConverter))] public enum SubscriptionReplacementModeAndroid { @@ -2134,10 +2192,12 @@ public interface ProductCommon public interface PurchaseCommon { - /// The current plan identifier. This is: - /// - On Android: the basePlanId (e.g., "premium", "premium-year") - /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") - /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// + /// The current plan identifier. This is: + /// - On Android: the basePlanId (e.g., "premium", "premium-year") + /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") + /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// string? CurrentPlanId { get; } string Id { get; } IReadOnlyList? Ids { get; } @@ -2195,10 +2255,12 @@ public sealed record ActiveSubscription public bool? AutoRenewingAndroid { get; init; } [JsonPropertyName("basePlanIdAndroid")] public string? BasePlanIdAndroid { get; init; } - /// The current plan identifier. This is: - /// - On Android: the basePlanId (e.g., "premium", "premium-year") - /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") - /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// + /// The current plan identifier. This is: + /// - On Android: the basePlanId (e.g., "premium", "premium-year") + /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") + /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// [JsonPropertyName("currentPlanId")] public string? CurrentPlanId { get; init; } [JsonPropertyName("daysUntilExpirationIOS")] @@ -2216,8 +2278,10 @@ public sealed record ActiveSubscription /// Required for subscription upgrade/downgrade on Android [JsonPropertyName("purchaseTokenAndroid")] public string? PurchaseTokenAndroid { get; init; } - /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, - /// pending upgrades/downgrades, and auto-renewal preferences. + /// + /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, + /// pending upgrades/downgrades, and auto-renewal preferences. + /// [JsonPropertyName("renewalInfoIOS")] public RenewalInfoIOS? RenewalInfoIOS { get; init; } /// Unix timestamp in milliseconds since January 1, 1970 UTC. @@ -2227,10 +2291,12 @@ public sealed record ActiveSubscription public required string TransactionId { get; init; } } -/// Advanced Commerce metadata from a transaction (iOS 18.4+). -/// Contains item details, tax information, and refund data for purchases -/// made through the Advanced Commerce API using generic SKUs. -/// Only present for transactions that use the Advanced Commerce API. +/// +/// Advanced Commerce metadata from a transaction (iOS 18.4+). +/// Contains item details, tax information, and refund data for purchases +/// made through the Advanced Commerce API using generic SKUs. +/// Only present for transactions that use the Advanced Commerce API. +/// public sealed record AdvancedCommerceInfoIOS { /// Optional description @@ -2245,10 +2311,12 @@ public sealed record AdvancedCommerceInfoIOS /// The items purchased as part of this transaction [JsonPropertyName("items")] public required IReadOnlyList Items { get; init; } - /// Subscription period for this transaction. - /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 - /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, - /// or visionOS 2.4+). + /// + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). + /// [JsonPropertyName("period")] public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking @@ -2273,8 +2341,10 @@ public sealed record AdvancedCommerceItemDetailsIOS public string? JsonRepresentation { get; init; } } -/// An item purchased through the Advanced Commerce API (iOS 18.4+). -/// Represents a developer-defined product within a generic SKU transaction. +/// +/// An item purchased through the Advanced Commerce API (iOS 18.4+). +/// Represents a developer-defined product within a generic SKU transaction. +/// public sealed record AdvancedCommerceItemIOS { /// The item's detail information @@ -2316,28 +2386,36 @@ public sealed record AppTransaction public required string Environment { get; init; } [JsonPropertyName("originalAppVersion")] public required string OriginalAppVersion { get; init; } - /// Original App Store platform raw value. Xcode 27 adds the back-deployed managed - /// acquisition-platform value. + /// + /// Original App Store platform raw value. Xcode 27 adds the back-deployed managed + /// acquisition-platform value. + /// [JsonPropertyName("originalPlatform")] public string? OriginalPlatform { get; init; } [JsonPropertyName("originalPurchaseDate")] public required double OriginalPurchaseDate { get; init; } [JsonPropertyName("preorderDate")] public double? PreorderDate { get; init; } - /// Date the app-acquisition transaction was revoked (epoch milliseconds). - /// Available through the Xcode 27 SDK and back-deployed to Apple 16+. + /// + /// Date the app-acquisition transaction was revoked (epoch milliseconds). + /// Available through the Xcode 27 SDK and back-deployed to Apple 16+. + /// [JsonPropertyName("revocationDate")] public double? RevocationDate { get; init; } [JsonPropertyName("signedDate")] public required double SignedDate { get; init; } - /// Store channel of the original app purchase: consumer, education, enterprise, - /// or another future StoreKit value (Apple 27+ beta). + /// + /// Store channel of the original app purchase: consumer, education, enterprise, + /// or another future StoreKit value (Apple 27+ beta). + /// [JsonPropertyName("storeType")] public string? StoreType { get; init; } } -/// Display information for developer-rendered Billing Choice screens (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Display information for developer-rendered Billing Choice screens (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// public sealed record BillingChoiceInfoAndroid { /// URL for the Play Billing choice image matching the requested layout. @@ -2348,44 +2426,56 @@ public sealed record BillingChoiceInfoAndroid public string? PlayBillingLoyaltyInfo { get; init; } } -/// Result of checking billing program availability (Android) -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Result of checking billing program availability (Android) +/// Available in Google Play Billing Library 8.2.0+ +/// public sealed record BillingProgramAvailabilityResultAndroid { /// The billing program that was checked [JsonPropertyName("billingProgram")] public required BillingProgramAndroid BillingProgram { get; init; } - /// Billing Choice screen renderer. Populated only for available BILLING_CHOICE results. - /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// + /// Billing Choice screen renderer. Populated only for available BILLING_CHOICE results. + /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// [JsonPropertyName("choiceScreenType")] public BillingChoiceScreenTypeAndroid? ChoiceScreenType { get; init; } /// Whether the billing program is available for the user [JsonPropertyName("isAvailable")] public required bool IsAvailable { get; init; } - /// Whether external-link payment is available for Billing Choice. - /// Populated only for available BILLING_CHOICE results. - /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// + /// Whether external-link payment is available for Billing Choice. + /// Populated only for available BILLING_CHOICE results. + /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// [JsonPropertyName("isExternalLinkAvailable")] public bool? IsExternalLinkAvailable { get; init; } } -/// Reporting details for transactions made outside of Google Play Billing (Android) -/// Contains the external transaction token needed for reporting -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Reporting details for transactions made outside of Google Play Billing (Android) +/// Contains the external transaction token needed for reporting +/// Available in Google Play Billing Library 8.2.0+ +/// public sealed record BillingProgramReportingDetailsAndroid { /// The billing program that the reporting details are associated with [JsonPropertyName("billingProgram")] public required BillingProgramAndroid BillingProgram { get; init; } - /// External transaction token used to report transactions made outside of Google Play Billing. - /// Do not cache it for a later redirect session. For External Offer, the same token may report - /// multiple purchases made during the session that generated it. + /// + /// External transaction token used to report transactions made outside of Google Play Billing. + /// Do not cache it for a later redirect session. For External Offer, the same token may report + /// multiple purchases made during the session that generated it. + /// [JsonPropertyName("externalTransactionToken")] public required string ExternalTransactionToken { get; init; } } -/// Extended billing result with sub-response code (Android) -/// Available in Google Play Billing Library 8.0.0+ +/// +/// Extended billing result with sub-response code (Android) +/// Available in Google Play Billing Library 8.0.0+ +/// public sealed record BillingResultAndroid { /// Debug message from the billing library @@ -2394,14 +2484,18 @@ public sealed record BillingResultAndroid /// The response code from the billing operation [JsonPropertyName("responseCode")] public required int ResponseCode { get; init; } - /// Sub-response code for more granular error information (8.0+). - /// Provides additional context when responseCode indicates an error. + /// + /// Sub-response code for more granular error information (8.0+). + /// Provides additional context when responseCode indicates an error. + /// [JsonPropertyName("subResponseCode")] public SubResponseCodeAndroid? SubResponseCode { get; init; } } -/// Metadata for one auto-renewable subscription included in an Apple -/// subscription bundle (Apple 27+ beta). +/// +/// Metadata for one auto-renewable subscription included in an Apple +/// subscription bundle (Apple 27+ beta). +/// public sealed record BundledSubscriptionIOS { [JsonPropertyName("description")] @@ -2424,21 +2518,29 @@ public sealed record BundledSubscriptionIOS public required int SubscriptionGroupLevel { get; init; } } -/// Details provided when user selects developer billing option (Android) -/// Received via DeveloperProvidedBillingListener callback -/// Available in Google Play Billing Library 8.3.0+ +/// +/// Details provided when user selects developer billing option (Android) +/// Received via DeveloperProvidedBillingListener callback +/// Available in Google Play Billing Library 8.3.0+ +/// public sealed record DeveloperProvidedBillingDetailsAndroid { - /// External transaction token used to report transactions made through developer billing. - /// Nullable for flows such as external payments where no token is returned. + /// + /// External transaction token used to report transactions made through developer billing. + /// Nullable for flows such as external payments where no token is returned. + /// [JsonPropertyName("externalTransactionToken")] public string? ExternalTransactionToken { get; init; } - /// URI to launch for an external-link Billing Choice flow, when provided by - /// Google Play. + /// + /// URI to launch for an external-link Billing Choice flow, when provided by + /// Google Play. + /// [JsonPropertyName("linkUri")] public string? LinkUri { get; init; } - /// Original external transaction ID when replacing a subscription that was - /// purchased through developer billing. + /// + /// Original external transaction ID when replacing a subscription that was + /// purchased through developer billing. + /// [JsonPropertyName("originalExternalTransactionId")] public string? OriginalExternalTransactionId { get; init; } /// Products selected for the developer billing flow. @@ -2460,8 +2562,10 @@ public sealed record DeveloperProvidedBillingProductAndroid public required ProductType Type { get; init; } } -/// Discount amount details for one-time purchase offers (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Discount amount details for one-time purchase offers (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record DiscountAmountAndroid { /// Discount amount in micro-units (1,000,000 = 1 unit of currency) @@ -2472,35 +2576,45 @@ public sealed record DiscountAmountAndroid public required string FormattedDiscountAmount { get; init; } } -/// Discount display information for one-time purchase offers (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Discount display information for one-time purchase offers (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record DiscountDisplayInfoAndroid { - /// Absolute discount amount details - /// Only returned for fixed amount discounts + /// + /// Absolute discount amount details + /// Only returned for fixed amount discounts + /// [JsonPropertyName("discountAmount")] public DiscountAmountAndroid? DiscountAmount { get; init; } - /// Percentage discount (e.g., 33 for 33% off) - /// Only returned for percentage-based discounts + /// + /// Percentage discount (e.g., 33 for 33% off) + /// Only returned for percentage-based discounts + /// [JsonPropertyName("percentageDiscount")] public int? PercentageDiscount { get; init; } } -/// Standardized one-time product discount offer. -/// Provides a platform-neutral OpenIAP shape for Google Play one-time product -/// purchase options and offers. -/// -/// Currently populated only on Android (Google Play Billing 8.0+). -/// iOS does not populate this type. -/// -/// @see https://openiap.dev/docs/types/discount-offer +/// +/// Standardized one-time product discount offer. +/// Provides a platform-neutral OpenIAP shape for Google Play one-time product +/// purchase options and offers. +/// +/// Currently populated only on Android (Google Play Billing 8.0+). +/// iOS does not populate this type. +/// +/// @see https://openiap.dev/docs/types/discount-offer +/// public sealed record DiscountOffer { /// Currency code (ISO 4217, e.g., "USD") [JsonPropertyName("currency")] public required string Currency { get; init; } - /// [Android] Fixed discount amount in micro-units. - /// Only present for fixed amount discounts. + /// + /// [Android] Fixed discount amount in micro-units. + /// Only present for fixed amount discounts. + /// [JsonPropertyName("discountAmountMicrosAndroid")] public string? DiscountAmountMicrosAndroid { get; init; } /// Formatted display price string (e.g., "$4.99") @@ -2509,53 +2623,71 @@ public sealed record DiscountOffer /// [Android] Formatted discount amount including its currency sign (e.g., "$5.00"). [JsonPropertyName("formattedDiscountAmountAndroid")] public string? FormattedDiscountAmountAndroid { get; init; } - /// [Android] Original full price in micro-units before discount. - /// Divide by 1,000,000 to get the actual price. - /// Use for displaying strikethrough original price. + /// + /// [Android] Original full price in micro-units before discount. + /// Divide by 1,000,000 to get the actual price. + /// Use for displaying strikethrough original price. + /// [JsonPropertyName("fullPriceMicrosAndroid")] public string? FullPriceMicrosAndroid { get; init; } - /// Unique identifier for the offer. - /// - iOS: Not applicable (one-time discounts not supported) - /// - Android: offerId from the Google Play one-time purchase option + /// + /// Unique identifier for the offer. + /// - iOS: Not applicable (one-time discounts not supported) + /// - Android: offerId from the Google Play one-time purchase option + /// [JsonPropertyName("id")] public string? Id { get; init; } - /// [Android] Limited quantity information. - /// Contains maximumQuantity and remainingQuantity. + /// + /// [Android] Limited quantity information. + /// Contains maximumQuantity and remainingQuantity. + /// [JsonPropertyName("limitedQuantityInfoAndroid")] public LimitedQuantityInfoAndroid? LimitedQuantityInfoAndroid { get; init; } /// [Android] List of tags associated with this offer. [JsonPropertyName("offerTagsAndroid")] public IReadOnlyList? OfferTagsAndroid { get; init; } - /// [Android] Offer token required for purchase. - /// Must be passed to requestPurchase() when purchasing with this offer. + /// + /// [Android] Offer token required for purchase. + /// Must be passed to requestPurchase() when purchasing with this offer. + /// [JsonPropertyName("offerTokenAndroid")] public string? OfferTokenAndroid { get; init; } - /// [Android] Percentage discount (e.g., 33 for 33% off). - /// Only present for percentage-based discounts. + /// + /// [Android] Percentage discount (e.g., 33 for 33% off). + /// Only present for percentage-based discounts. + /// [JsonPropertyName("percentageDiscountAndroid")] public int? PercentageDiscountAndroid { get; init; } - /// [Android] Pre-order details if this is a pre-order offer. - /// Available in Google Play Billing Library 8.1.0+ + /// + /// [Android] Pre-order details if this is a pre-order offer. + /// Available in Google Play Billing Library 8.1.0+ + /// [JsonPropertyName("preorderDetailsAndroid")] public PreorderDetailsAndroid? PreorderDetailsAndroid { get; init; } /// Numeric price value [JsonPropertyName("price")] public required double Price { get; init; } - /// [Android] Purchase option ID for this offer. - /// Used to identify which purchase option the user selected. - /// Available in Google Play Billing Library 8.0+ + /// + /// [Android] Purchase option ID for this offer. + /// Used to identify which purchase option the user selected. + /// Available in Google Play Billing Library 8.0+ + /// [JsonPropertyName("purchaseOptionIdAndroid")] public string? PurchaseOptionIdAndroid { get; init; } /// [Android] Rental details if this is a rental offer. [JsonPropertyName("rentalDetailsAndroid")] public RentalDetailsAndroid? RentalDetailsAndroid { get; init; } - /// Offer category. DiscountOffer currently represents Android one-time product - /// offers and is populated as OneTime. Introductory and Promotional are used by - /// SubscriptionOffer. + /// + /// Offer category. DiscountOffer currently represents Android one-time product + /// offers and is populated as OneTime. Introductory and Promotional are used by + /// SubscriptionOffer. + /// [JsonPropertyName("type")] public required DiscountOfferType Type { get; init; } - /// [Android] Valid time window for the offer. - /// Contains startTimeMillis and endTimeMillis. + /// + /// [Android] Valid time window for the offer. + /// Contains startTimeMillis and endTimeMillis. + /// [JsonPropertyName("validTimeWindowAndroid")] public ValidTimeWindowAndroid? ValidTimeWindowAndroid { get; init; } } @@ -2587,8 +2719,10 @@ public sealed record ExternalPurchaseCustomLinkTokenResultIOS /// Optional error message if token retrieval failed [JsonPropertyName("error")] public string? Error { get; init; } - /// The external purchase token string. - /// Report this token to Apple's External Purchase Server API. + /// + /// The external purchase token string. + /// Report this token to Apple's External Purchase Server API. + /// [JsonPropertyName("token")] public string? Token { get; init; } } @@ -2604,16 +2738,20 @@ public sealed record ExternalPurchaseLinkResultIOS public required bool Success { get; init; } } -/// Result of presenting external purchase notice sheet (iOS 17.4+) -/// Returns the token when user continues to external purchase. +/// +/// Result of presenting external purchase notice sheet (iOS 17.4+) +/// Returns the token when user continues to external purchase. +/// public sealed record ExternalPurchaseNoticeResultIOS { /// Optional error message if the presentation failed [JsonPropertyName("error")] public string? Error { get; init; } - /// External purchase token returned when user continues (iOS 17.4+). - /// This token should be reported to Apple's External Purchase Server API. - /// Only present when result is Continue. + /// + /// External purchase token returned when user continues (iOS 17.4+). + /// This token should be reported to Apple's External Purchase Server API. + /// Only present when result is Continue. + /// [JsonPropertyName("externalPurchaseToken")] public string? ExternalPurchaseToken { get; init; } /// Notice result indicating user action @@ -2629,8 +2767,10 @@ public sealed record FetchProductsResultProducts(IReadOnlyList? Value) public sealed record FetchProductsResultSubscriptions(IReadOnlyList? Value) : FetchProductsResult; -/// Public app-facing data attached to one store product in IAPKit. -/// Never place credentials, signing keys, or server-authoritative rules here. +/// +/// Public app-facing data attached to one store product in IAPKit. +/// Never place credentials, signing keys, or server-authoritative rules here. +/// public sealed record IapkitProductClientPayload { [JsonPropertyName("body")] @@ -2643,9 +2783,11 @@ public sealed record IapkitProductClientPayload public required double Version { get; init; } } -/// Result from showing Play billing in-app messages (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// Result from showing Play billing in-app messages (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// public sealed record InAppMessageResultAndroid { /// Purchase token returned when a subscription status changed. @@ -2656,26 +2798,34 @@ public sealed record InAppMessageResultAndroid public required InAppMessageResponseCodeAndroid ResponseCode { get; init; } } -/// Installment plan details for subscription offers (Android) -/// Contains information about the installment plan commitment. -/// Available in Google Play Billing Library 7.0+ +/// +/// Installment plan details for subscription offers (Android) +/// Contains information about the installment plan commitment. +/// Available in Google Play Billing Library 7.0+ +/// public sealed record 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. + /// + /// 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. + /// [JsonPropertyName("commitmentPaymentsCount")] public required int CommitmentPaymentsCount { get; init; } - /// 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). + /// + /// 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). + /// [JsonPropertyName("subsequentCommitmentPaymentsCount")] public required int SubsequentCommitmentPaymentsCount { get; init; } } -/// Limited quantity information for one-time purchase offers (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Limited quantity information for one-time purchase offers (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record LimitedQuantityInfoAndroid { /// Maximum quantity a user can purchase @@ -2686,33 +2836,45 @@ public sealed record LimitedQuantityInfoAndroid public required int RemainingQuantity { get; init; } } -/// 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+ +/// +/// 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 sealed record PendingPurchaseUpdateAndroid { - /// Product IDs for the pending purchase update. - /// These are the new products the user is switching to. + /// + /// Product IDs for the pending purchase update. + /// These are the new products the user is switching to. + /// [JsonPropertyName("products")] public required IReadOnlyList Products { get; init; } - /// Purchase token for the pending transaction. - /// Use this token to track or manage the pending purchase update. + /// + /// Purchase token for the pending transaction. + /// Use this token to track or manage the pending purchase update. + /// [JsonPropertyName("purchaseToken")] public required string PurchaseToken { get; init; } } -/// Pre-order details for one-time purchase products (Android) -/// Available in Google Play Billing Library 8.1.0+ +/// +/// Pre-order details for one-time purchase products (Android) +/// Available in Google Play Billing Library 8.1.0+ +/// public sealed record PreorderDetailsAndroid { - /// Pre-order presale end time in milliseconds since epoch. - /// This is when the presale period ends and the product will be released. + /// + /// Pre-order presale end time in milliseconds since epoch. + /// This is when the presale period ends and the product will be released. + /// [JsonPropertyName("preorderPresaleEndTimeMillis")] public required string PreorderPresaleEndTimeMillis { get; init; } - /// Pre-order release time in milliseconds since epoch. - /// This is when the product will be available to users who pre-ordered. + /// + /// Pre-order release time in milliseconds since epoch. + /// This is when the product will be available to users who pre-ordered. + /// [JsonPropertyName("preorderReleaseTimeMillis")] public required string PreorderReleaseTimeMillis { get; init; } } @@ -2747,9 +2909,11 @@ public sealed record ProductAndroid : Product, ProductCommon public string? DebugDescription { get; init; } [JsonPropertyName("description")] public required string Description { get; init; } - /// Standardized Android one-time product purchase options and offers. - /// Native metadata uses Android-suffixed fields. - /// @see https://openiap.dev/docs/types/discount-offer + /// + /// Standardized Android one-time product purchase options and offers. + /// Native metadata uses Android-suffixed fields. + /// @see https://openiap.dev/docs/types/discount-offer + /// [JsonPropertyName("discountOffers")] public IReadOnlyList? DiscountOffers { get; init; } [JsonPropertyName("displayName")] @@ -2764,16 +2928,20 @@ public sealed record ProductAndroid : Product, ProductCommon public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.Android; [JsonPropertyName("price")] public double? Price { get; init; } - /// Product-level status code indicating fetch result (Android 8.0+) - /// OK = product fetched successfully - /// NOT_FOUND = SKU doesn't exist - /// NO_OFFERS_AVAILABLE = user not eligible for any offers - /// Available in Google Play Billing Library 8.0.0+ + /// + /// Product-level status code indicating fetch result (Android 8.0+) + /// OK = product fetched successfully + /// NOT_FOUND = SKU doesn't exist + /// NO_OFFERS_AVAILABLE = user not eligible for any offers + /// Available in Google Play Billing Library 8.0.0+ + /// [JsonPropertyName("productStatusAndroid")] public ProductStatusAndroid? ProductStatusAndroid { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with Android-specific fields using suffix. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with Android-specific fields using suffix. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } [JsonPropertyName("title")] @@ -2806,14 +2974,18 @@ public sealed record ProductIOS : Product, ProductCommon public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.IOS; [JsonPropertyName("price")] public double? Price { get; init; } - /// iOS 26.4+ subscription pricing terms, including billing plan metadata for - /// monthly subscriptions with a 12-month commitment. + /// + /// iOS 26.4+ subscription pricing terms, including billing plan metadata for + /// monthly subscriptions with a 12-month commitment. + /// [JsonPropertyName("pricingTermsIOS")] public IReadOnlyList? PricingTermsIOS { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with iOS-specific fields using suffix. - /// Note: iOS does not support one-time product discounts. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with iOS-specific fields using suffix. + /// Note: iOS does not support one-time product discounts. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } [JsonPropertyName("title")] @@ -2844,16 +3016,20 @@ public sealed record ProductSubscriptionAndroid : ProductSubscription, ProductCo public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.Android; [JsonPropertyName("price")] public double? Price { get; init; } - /// Product-level status code indicating fetch result (Android 8.0+) - /// OK = product fetched successfully - /// NOT_FOUND = SKU doesn't exist - /// NO_OFFERS_AVAILABLE = user not eligible for any offers - /// Available in Google Play Billing Library 8.0.0+ + /// + /// Product-level status code indicating fetch result (Android 8.0+) + /// OK = product fetched successfully + /// NOT_FOUND = SKU doesn't exist + /// NO_OFFERS_AVAILABLE = user not eligible for any offers + /// Available in Google Play Billing Library 8.0.0+ + /// [JsonPropertyName("productStatusAndroid")] public ProductStatusAndroid? ProductStatusAndroid { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with Android-specific fields using suffix. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with Android-specific fields using suffix. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public required IReadOnlyList SubscriptionOffers { get; init; } [JsonPropertyName("title")] @@ -2864,8 +3040,10 @@ public sealed record ProductSubscriptionAndroid : ProductSubscription, ProductCo public sealed record ProductSubscriptionIOS : ProductSubscription, ProductCommon { - /// Subscriptions included in this Apple subscription bundle. Empty or null for - /// every other product type (Apple 27+ beta). + /// + /// Subscriptions included in this Apple subscription bundle. Empty or null for + /// every other product type (Apple 27+ beta). + /// [JsonPropertyName("bundledSubscriptionsIOS")] public IReadOnlyList? BundledSubscriptionsIOS { get; init; } [JsonPropertyName("currency")] @@ -2900,16 +3078,20 @@ public sealed record ProductSubscriptionIOS : ProductSubscription, ProductCommon public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.IOS; [JsonPropertyName("price")] public double? Price { get; init; } - /// iOS 26.4+ subscription pricing terms, including billing plan metadata for - /// monthly subscriptions with a 12-month commitment. + /// + /// iOS 26.4+ subscription pricing terms, including billing plan metadata for + /// monthly subscriptions with a 12-month commitment. + /// [JsonPropertyName("pricingTermsIOS")] public IReadOnlyList? PricingTermsIOS { get; init; } /// App Store subscription group identifier for intro-offer eligibility checks. [JsonPropertyName("subscriptionGroupIdIOS")] public string? SubscriptionGroupIdIOS { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with iOS-specific fields using suffix. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with iOS-specific fields using suffix. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } [JsonPropertyName("subscriptionPeriodNumberIOS")] @@ -2942,11 +3124,13 @@ public sealed record PurchaseAndroid : Purchase, PurchaseCommon public bool? IsAcknowledgedAndroid { get; init; } [JsonPropertyName("isAutoRenewing")] public required bool IsAutoRenewing { get; init; } - /// Whether the subscription is suspended (Android) - /// A suspended subscription means the user's payment method failed and they need to fix it. - /// Users should be directed to the subscription center to resolve the issue. - /// Do NOT grant entitlements for suspended subscriptions. - /// Available in Google Play Billing Library 8.1.0+ + /// + /// Whether the subscription is suspended (Android) + /// A suspended subscription means the user's payment method failed and they need to fix it. + /// Users should be directed to the subscription center to resolve the issue. + /// Do NOT grant entitlements for suspended subscriptions. + /// Available in Google Play Billing Library 8.1.0+ + /// [JsonPropertyName("isSuspendedAndroid")] public bool? IsSuspendedAndroid { get; init; } [JsonPropertyName("obfuscatedAccountIdAndroid")] @@ -2955,10 +3139,12 @@ public sealed record PurchaseAndroid : Purchase, PurchaseCommon public string? ObfuscatedProfileIdAndroid { get; init; } [JsonPropertyName("packageNameAndroid")] public string? PackageNameAndroid { get; init; } - /// 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+ + /// + /// 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+ + /// [JsonPropertyName("pendingPurchaseUpdateAndroid")] public PendingPurchaseUpdateAndroid? PendingPurchaseUpdateAndroid { get; init; } [JsonPropertyName("productId")] @@ -2979,13 +3165,17 @@ public sealed record PurchaseAndroid : Purchase, PurchaseCommon public required double TransactionDate { get; init; } [JsonPropertyName("transactionId")] public string? TransactionId { get; init; } - /// Amazon Appstore user id (PurchaseResponse.getUserData().getUserId()). - /// Only populated on the Amazon flavor; required for server-side Amazon RVS - /// receipt verification (userId + receiptId). Null on Google Play and Horizon. + /// + /// Amazon Appstore user id (PurchaseResponse.getUserData().getUserId()). + /// Only populated on the Amazon flavor; required for server-side Amazon RVS + /// receipt verification (userId + receiptId). Null on Google Play and Horizon. + /// [JsonPropertyName("userIdAmazon")] public string? UserIdAmazon { get; init; } - /// Amazon Appstore marketplace (PurchaseResponse.getUserData().getMarketplace()), - /// for example "US" or "FR". Only populated on the Amazon flavor. + /// + /// Amazon Appstore marketplace (PurchaseResponse.getUserData().getMarketplace()), + /// for example "US" or "FR". Only populated on the Amazon flavor. + /// [JsonPropertyName("userMarketplaceAmazon")] public string? UserMarketplaceAmazon { get; init; } } @@ -3014,9 +3204,11 @@ public sealed record PurchaseError public sealed record PurchaseIOS : Purchase, PurchaseCommon { - /// Advanced Commerce API metadata (iOS 18.4+). - /// Present only for transactions that use the Advanced Commerce API. - /// Contains item details, tax information, and refund data for generic SKU purchases. + /// + /// Advanced Commerce API metadata (iOS 18.4+). + /// Present only for transactions that use the Advanced Commerce API. + /// Contains item details, tax information, and refund data for generic SKU purchases. + /// [JsonPropertyName("advancedCommerceInfoIOS")] public AdvancedCommerceInfoIOS? AdvancedCommerceInfoIOS { get; init; } [JsonPropertyName("appAccountToken")] @@ -3026,8 +3218,10 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon /// iOS 26.4+ billing plan selected for this transaction. [JsonPropertyName("billingPlanTypeIOS")] public SubscriptionBillingPlanTypeIOS? BillingPlanTypeIOS { get; init; } - /// Original transaction identifier for the subscription bundle that produced - /// this transaction (Apple 27+ SDK; back-deployed by StoreKit). + /// + /// Original transaction identifier for the subscription bundle that produced + /// this transaction (Apple 27+ SDK; back-deployed by StoreKit). + /// [JsonPropertyName("bundleOriginalTransactionIdIOS")] public string? BundleOriginalTransactionIdIOS { get; init; } /// Product identifier of the subscription bundle that produced this transaction. @@ -3071,8 +3265,10 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon /// StoreKit ownership raw value. Xcode 27 adds the back-deployed assigned value. [JsonPropertyName("ownershipTypeIOS")] public string? OwnershipTypeIOS { get; init; } - /// Original transaction identifier replaced when moving between a standalone - /// subscription and a subscription bundle. + /// + /// Original transaction identifier replaced when moving between a standalone + /// subscription and a subscription bundle. + /// [JsonPropertyName("previousOriginalTransactionIdIOS")] public string? PreviousOriginalTransactionIdIOS { get; init; } [JsonPropertyName("productId")] @@ -3096,8 +3292,10 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon /// Normalized StoreKit revocation reason, including upgraded_to_bundle. [JsonPropertyName("revocationReasonIOS")] public string? RevocationReasonIOS { get; init; } - /// StoreKit revocation type, including assignment-revocation on Apple 26.4+ - /// when compiled with the Xcode 27 SDK. + /// + /// StoreKit revocation type, including assignment-revocation on Apple 26.4+ + /// when compiled with the Xcode 27 SDK. + /// [JsonPropertyName("revocationTypeIOS")] public string? RevocationTypeIOS { get; init; } /// Store where purchase was made @@ -3150,8 +3348,10 @@ public sealed record RenewalCommitmentInfoIOS public required double CommitmentRenewalPrice { get; init; } } -/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo -/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo +/// +/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo +/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo +/// public sealed record RenewalInfoIOS { [JsonPropertyName("autoRenewPreference")] @@ -3165,44 +3365,60 @@ public sealed record RenewalInfoIOS /// Subscription-group identifier for the bundle used by the next renewal. [JsonPropertyName("bundleSubscriptionGroupId")] public string? BundleSubscriptionGroupId { get; init; } - /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a - /// 12-month commitment. + /// + /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a + /// 12-month commitment. + /// [JsonPropertyName("commitmentInfo")] public RenewalCommitmentInfoIOS? CommitmentInfo { get; init; } - /// StoreKit's raw integer expiration-reason value represented as a string. - /// Xcode 27 adds the back-deployed unbundled case. Preserve unknown future values. + /// + /// StoreKit's raw integer expiration-reason value represented as a string. + /// Xcode 27 adds the back-deployed unbundled case. Preserve unknown future values. + /// [JsonPropertyName("expirationReason")] public string? ExpirationReason { get; init; } - /// Grace period expiration date (milliseconds since epoch) - /// When set, subscription is in grace period (billing issue but still has access) + /// + /// Grace period expiration date (milliseconds since epoch) + /// When set, subscription is in grace period (billing issue but still has access) + /// [JsonPropertyName("gracePeriodExpirationDate")] public double? GracePeriodExpirationDate { get; init; } - /// True if subscription failed to renew due to billing issue and is retrying - /// StoreKit exposes this directly as RenewalInfo.isInBillingRetry. + /// + /// True if subscription failed to renew due to billing issue and is retrying + /// StoreKit exposes this directly as RenewalInfo.isInBillingRetry. + /// [JsonPropertyName("isInBillingRetry")] public bool? IsInBillingRetry { get; init; } [JsonPropertyName("jsonRepresentation")] public string? JsonRepresentation { get; init; } - /// Product ID that will be used on next renewal (when user upgrades/downgrades) - /// If set and different from current productId, subscription will change on expiration + /// + /// Product ID that will be used on next renewal (when user upgrades/downgrades) + /// If set and different from current productId, subscription will change on expiration + /// [JsonPropertyName("pendingUpgradeProductId")] public string? PendingUpgradeProductId { get; init; } - /// User's response to subscription price increase - /// Possible values: "AGREED", "PENDING", null (no price increase) + /// + /// User's response to subscription price increase + /// Possible values: "AGREED", "PENDING", null (no price increase) + /// [JsonPropertyName("priceIncreaseStatus")] public string? PriceIncreaseStatus { get; init; } /// iOS 26.4+ billing plan that will renew after the current period. [JsonPropertyName("renewalBillingPlanType")] public SubscriptionBillingPlanTypeIOS? RenewalBillingPlanType { get; init; } - /// Expected renewal date (milliseconds since epoch) - /// For active subscriptions, when the next renewal/charge will occur + /// + /// Expected renewal date (milliseconds since epoch) + /// For active subscriptions, when the next renewal/charge will occur + /// [JsonPropertyName("renewalDate")] public double? RenewalDate { get; init; } /// Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) [JsonPropertyName("renewalOfferId")] public string? RenewalOfferId { get; init; } - /// Type of offer applied to next renewal - /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. + /// + /// Type of offer applied to next renewal + /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. + /// [JsonPropertyName("renewalOfferType")] public string? RenewalOfferType { get; init; } [JsonPropertyName("willAutoRenew")] @@ -3212,12 +3428,16 @@ public sealed record RenewalInfoIOS public bool? WillUnbundle { get; init; } } -/// Rental details for one-time purchase products that can be rented (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Rental details for one-time purchase products that can be rented (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record RentalDetailsAndroid { - /// Rental expiration period in ISO 8601 format - /// Time after rental period ends when user can still extend + /// + /// Rental expiration period in ISO 8601 format + /// Time after rental period ends when user can still extend + /// [JsonPropertyName("rentalExpirationPeriod")] public string? RentalExpirationPeriod { get; init; } /// Rental period in ISO 8601 format (e.g., P7D for 7 days) @@ -3233,19 +3453,25 @@ public sealed record RequestPurchaseResultPurchases(IReadOnlyList? Val public sealed record RequestVerifyPurchaseWithIapkitResult { - /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. - /// Public product payload when includeClientPayload was requested, the - /// Apple or Google receipt is valid, and a payload exists for that product. + /// + /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. + /// Public product payload when includeClientPayload was requested, the + /// Apple or Google receipt is valid, and a payload exists for that product. + /// [JsonPropertyName("clientPayload")] public IapkitProductClientPayload? ClientPayload { get; init; } - /// True when the purchase is valid and actionable. - /// Only entitled, pending-acknowledgment, or ready-to-consume return true. - /// Callers must still match productId and use the platform plus app-owned product - /// type to choose the fulfillment path. + /// + /// True when the purchase is valid and actionable. + /// Only entitled, pending-acknowledgment, or ready-to-consume return true. + /// Callers must still match productId and use the platform plus app-owned product + /// type to choose the fulfillment path. + /// [JsonPropertyName("isValid")] public required bool IsValid { get; init; } - /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. - /// Store-verified product identifier when the provider returns one. + /// + /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. + /// Store-verified product identifier when the provider returns one. + /// [JsonPropertyName("productId")] public string? ProductId { get; init; } /// The current state of the purchase. @@ -3265,18 +3491,22 @@ public sealed record SubscriptionCommitmentInfoIOS public required double Price { get; init; } } -/// Standardized subscription discount/promotional offer. -/// Provides a unified interface for subscription offers across iOS and Android. -/// -/// Both platforms support subscription offers with different implementations: -/// - iOS: Introductory offers, promotional offers with server-side signatures -/// - Android: Offer tokens with pricing phases -/// -/// @see https://openiap.dev/docs/types/subscription-offer +/// +/// Standardized subscription discount/promotional offer. +/// Provides a unified interface for subscription offers across iOS and Android. +/// +/// Both platforms support subscription offers with different implementations: +/// - iOS: Introductory offers, promotional offers with server-side signatures +/// - Android: Offer tokens with pricing phases +/// +/// @see https://openiap.dev/docs/types/subscription-offer +/// public sealed record SubscriptionOffer { - /// [Android] Base plan identifier. - /// Identifies which base plan this offer belongs to. + /// + /// [Android] Base plan identifier. + /// Identifies which base plan this offer belongs to. + /// [JsonPropertyName("basePlanIdAndroid")] public string? BasePlanIdAndroid { get; init; } /// Currency code (ISO 4217, e.g., "USD") @@ -3285,25 +3515,33 @@ public sealed record SubscriptionOffer /// Formatted display price string (e.g., "$9.99/month") [JsonPropertyName("displayPrice")] public required string DisplayPrice { get; init; } - /// Unique identifier for the offer. - /// - iOS: Discount identifier from App Store Connect - /// - Android: offerId from the Google Play subscription offer + /// + /// Unique identifier for the offer. + /// - iOS: Discount identifier from App Store Connect + /// - Android: offerId from the Google Play subscription offer + /// [JsonPropertyName("id")] public required string Id { get; init; } - /// [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+ + /// + /// [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+ + /// [JsonPropertyName("installmentPlanDetailsAndroid")] public InstallmentPlanDetailsAndroid? InstallmentPlanDetailsAndroid { get; init; } - /// [iOS] Key identifier for signature validation. - /// Used with server-side signature generation for promotional offers. + /// + /// [iOS] Key identifier for signature validation. + /// Used with server-side signature generation for promotional offers. + /// [JsonPropertyName("keyIdentifierIOS")] public string? KeyIdentifierIOS { get; init; } /// [iOS] Localized price string. [JsonPropertyName("localizedPriceIOS")] public string? LocalizedPriceIOS { get; init; } - /// [iOS] Cryptographic nonce (UUID) for signature validation. - /// Must be generated server-side for each purchase attempt. + /// + /// [iOS] Cryptographic nonce (UUID) for signature validation. + /// Must be generated server-side for each purchase attempt. + /// [JsonPropertyName("nonceIOS")] public string? NonceIOS { get; init; } /// [iOS] Number of billing periods for this discount. @@ -3312,8 +3550,10 @@ public sealed record SubscriptionOffer /// [Android] List of tags associated with this offer. [JsonPropertyName("offerTagsAndroid")] public IReadOnlyList? OfferTagsAndroid { get; init; } - /// [Android] Offer token required for purchase. - /// Must be passed to requestPurchase() when purchasing with this offer. + /// + /// [Android] Offer token required for purchase. + /// Must be passed to requestPurchase() when purchasing with this offer. + /// [JsonPropertyName("offerTokenAndroid")] public string? OfferTokenAndroid { get; init; } /// Payment mode during the offer period @@ -3328,16 +3568,22 @@ public sealed record SubscriptionOffer /// Numeric price value [JsonPropertyName("price")] public required double Price { get; init; } - /// [Android] Pricing phases for this subscription offer. - /// Contains detailed pricing information for each phase (trial, intro, regular). + /// + /// [Android] Pricing phases for this subscription offer. + /// Contains detailed pricing information for each phase (trial, intro, regular). + /// [JsonPropertyName("pricingPhasesAndroid")] public PricingPhasesAndroid? PricingPhasesAndroid { get; init; } - /// [iOS] Server-generated signature for promotional offer validation. - /// Required when applying promotional offers on iOS. + /// + /// [iOS] Server-generated signature for promotional offer validation. + /// Required when applying promotional offers on iOS. + /// [JsonPropertyName("signatureIOS")] public string? SignatureIOS { get; init; } - /// [iOS] Timestamp when the signature was generated. - /// Used for signature validation. + /// + /// [iOS] Timestamp when the signature was generated. + /// Used for signature validation. + /// [JsonPropertyName("timestampIOS")] public double? TimestampIOS { get; init; } /// Type of subscription offer (Introductory or Promotional) @@ -3400,22 +3646,28 @@ public sealed record TransactionCommitmentInfoIOS public required int TotalBillingPeriods { get; init; } } -/// User Choice Billing event details (Android) -/// Fired when a user selects alternative billing in the User Choice Billing dialog +/// +/// User Choice Billing event details (Android) +/// Fired when a user selects alternative billing in the User Choice Billing dialog +/// public sealed record UserChoiceBillingDetails { /// Token that must be reported to Google Play within 24 hours [JsonPropertyName("externalTransactionToken")] public required string ExternalTransactionToken { get; init; } - /// External transaction ID of the originating subscription when the user is - /// upgrading or downgrading a developer-billed subscription. Available in - /// OpenIAP Spec 2.3.0 / openiap-google 2.3.1 (requires Play Billing 9.1+). + /// + /// External transaction ID of the originating subscription when the user is + /// upgrading or downgrading a developer-billed subscription. Available in + /// OpenIAP Spec 2.3.0 / openiap-google 2.3.1 (requires Play Billing 9.1+). + /// [JsonPropertyName("originalExternalTransactionId")] public string? OriginalExternalTransactionId { get; init; } - /// Structured product details selected in the user-choice flow, including the - /// product type and offer token. Legacy payloads may omit this field; use - /// products as the product-ID fallback. Available in OpenIAP Spec 2.3.0 / - /// openiap-google 2.3.1 (requires Play Billing 9.1+). + /// + /// Structured product details selected in the user-choice flow, including the + /// product type and offer token. Legacy payloads may omit this field; use + /// products as the product-ID fallback. Available in OpenIAP Spec 2.3.0 / + /// openiap-google 2.3.1 (requires Play Billing 9.1+). + /// [JsonPropertyName("productDetailsAndroid")] public IReadOnlyList? ProductDetailsAndroid { get; init; } /// List of product IDs selected by the user @@ -3423,8 +3675,10 @@ public sealed record UserChoiceBillingDetails public required IReadOnlyList Products { get; init; } } -/// Valid time window for when an offer is available (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Valid time window for when an offer is available (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record ValidTimeWindowAndroid { /// End time in milliseconds since epoch @@ -3475,8 +3729,10 @@ public sealed record VerifyPurchaseResultAndroid : VerifyPurchaseResult public required bool TestTransaction { get; init; } } -/// Result from Meta Horizon verify_entitlement API. -/// Returns verification status and grant time for the entitlement. +/// +/// Result from Meta Horizon verify_entitlement API. +/// Returns verification status and grant time for the entitlement. +/// public sealed record VerifyPurchaseResultHorizon : VerifyPurchaseResult { /// Unix timestamp (seconds) when the entitlement was granted. @@ -3539,8 +3795,10 @@ public sealed record AndroidSubscriptionOfferInput public required string OfferToken { get; init; } } -/// Parameters for showing a billing program information dialog (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Parameters for showing a billing program information dialog (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// public sealed record BillingProgramInformationDialogParamsAndroid { /// Billing program. Currently only BILLING_CHOICE is supported. @@ -3561,26 +3819,34 @@ public sealed record DeepLinkOptions public string? PackageNameAndroid { get; init; } } -/// Parameters for a developer billing option in a purchase flow (Android). -/// Used with BillingFlowParams for external payments (8.3.0+) and Billing Choice -/// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+). -/// Only billingProgram is required; link fields are used when the selected program -/// links outside the app. +/// +/// Parameters for a developer billing option in a purchase flow (Android). +/// Used with BillingFlowParams for external payments (8.3.0+) and Billing Choice +/// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+). +/// Only billingProgram is required; link fields are used when the selected program +/// links outside the app. +/// public sealed record DeveloperBillingOptionParamsAndroid { /// The billing program. Use EXTERNAL_PAYMENTS or BILLING_CHOICE. [JsonPropertyName("billingProgram")] public required BillingProgramAndroid BillingProgram { get; init; } - /// The URI where the external payment will be processed. - /// Required only when the selected billing program links outside the app. + /// + /// The URI where the external payment will be processed. + /// Required only when the selected billing program links outside the app. + /// [JsonPropertyName("linkUri")] public string? LinkUri { get; init; } - /// The launch mode for the external payment link. - /// Required only when the selected billing program links outside the app. + /// + /// The launch mode for the external payment link. + /// Required only when the selected billing program links outside the app. + /// [JsonPropertyName("launchMode")] public DeveloperBillingLaunchModeAndroid? LaunchMode { get; init; } - /// A pre-generated external transaction token for a Billing Choice external-link - /// flow. Omit it when Google Play should provide the token in the callback. + /// + /// A pre-generated external transaction token for a Billing Choice external-link + /// flow. Omit it when Google Play should provide the token in the callback. + /// [JsonPropertyName("externalTransactionToken")] public string? ExternalTransactionToken { get; init; } } @@ -3604,8 +3870,10 @@ public sealed record DiscountOfferInputIOS public required double Timestamp { get; init; } } -/// Parameters for fetching Billing Choice display information (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Parameters for fetching Billing Choice display information (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// public sealed record GetBillingChoiceInfoParamsAndroid { /// Billing program. Currently only BILLING_CHOICE is supported. @@ -3619,9 +3887,11 @@ public sealed record GetBillingChoiceInfoParamsAndroid public string? UserLocale { get; init; } } -/// Parameters for showing Play billing in-app messages (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// Parameters for showing Play billing in-app messages (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// public sealed record InAppMessageParamsAndroid { /// In-app message categories to show. Defaults to transactional messages. @@ -3632,31 +3902,37 @@ public sealed record InAppMessageParamsAndroid /// Connection initialization configuration public sealed record InitConnectionConfig { - /// Enable a specific billing program for Android (7.0+) - /// When set, enables the specified billing program for external transactions. - /// - USER_CHOICE_BILLING: User can select between Google Play or alternative (7.0+) - /// - EXTERNAL_CONTENT_LINK: Link to external content (introduced in 8.2.0; use 8.2.1+) - /// - EXTERNAL_OFFER: External offers for digital content (introduced in 8.2.0; use 8.2.1+) - /// - EXTERNAL_PAYMENTS: Developer provided billing, Japan only (8.3.0+) - /// - BILLING_CHOICE: Google-rendered or developer-rendered billing choice - /// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+) + /// + /// Enable a specific billing program for Android (7.0+) + /// When set, enables the specified billing program for external transactions. + /// - USER_CHOICE_BILLING: User can select between Google Play or alternative (7.0+) + /// - EXTERNAL_CONTENT_LINK: Link to external content (introduced in 8.2.0; use 8.2.1+) + /// - EXTERNAL_OFFER: External offers for digital content (introduced in 8.2.0; use 8.2.1+) + /// - EXTERNAL_PAYMENTS: Developer provided billing, Japan only (8.3.0+) + /// - BILLING_CHOICE: Google-rendered or developer-rendered billing choice + /// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+) + /// [JsonPropertyName("enableBillingProgramAndroid")] public BillingProgramAndroid? EnableBillingProgramAndroid { get; init; } - /// Billing Choice renderer configured in Play Console. Available in OpenIAP - /// Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// GOOGLE_RENDERED registers the developer-provided billing listener so OpenIAP - /// can emit the selection event. DEVELOPER_RENDERED omits that listener so the - /// app can render its own choice screen and use the reporting/dialog/link APIs. - /// Must match choiceScreenType returned by isBillingProgramAvailableAndroid. - /// Defaults to GOOGLE_RENDERED. + /// + /// Billing Choice renderer configured in Play Console. Available in OpenIAP + /// Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// GOOGLE_RENDERED registers the developer-provided billing listener so OpenIAP + /// can emit the selection event. DEVELOPER_RENDERED omits that listener so the + /// app can render its own choice screen and use the reporting/dialog/link APIs. + /// Must match choiceScreenType returned by isBillingProgramAvailableAndroid. + /// Defaults to GOOGLE_RENDERED. + /// [JsonPropertyName("billingChoiceScreenTypeAndroid")] public BillingChoiceScreenTypeAndroid? BillingChoiceScreenTypeAndroid { get; init; } = global::OpenIap.BillingChoiceScreenTypeAndroid.GoogleRendered; } -/// Parameters for launching an external link (Android) -/// Used with launchExternalLink to initiate external offer, app install, or -/// developer-rendered Billing Choice flows -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Parameters for launching an external link (Android) +/// Used with launchExternalLink to initiate external offer, app install, or +/// developer-rendered Billing Choice flows +/// Available in Google Play Billing Library 8.2.0+ +/// public sealed record LaunchExternalLinkParamsAndroid { /// The billing program (EXTERNAL_CONTENT_LINK, EXTERNAL_OFFER, or BILLING_CHOICE) @@ -3671,9 +3947,11 @@ public sealed record LaunchExternalLinkParamsAndroid /// The URI where the content will be accessed from [JsonPropertyName("linkUri")] public required string LinkUri { get; init; } - /// External transaction token for a developer-rendered Billing Choice external-link - /// flow. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). Generate it with createBillingProgramReportingDetailsAndroid. + /// + /// External transaction token for a developer-rendered Billing Choice external-link + /// flow. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). Generate it with createBillingProgramReportingDetailsAndroid. + /// [JsonPropertyName("externalTransactionToken")] public string? ExternalTransactionToken { get; init; } } @@ -3686,18 +3964,22 @@ public sealed record ProductRequest public ProductQueryType? Type { get; init; } = global::OpenIap.ProductQueryType.InApp; } -/// JWS promotional offer input for iOS 15+ (StoreKit 2, WWDC 2025). -/// New signature format using compact JWS string for promotional offers. -/// This provides a simpler alternative to the legacy signature-based promotional offers. -/// Back-deployed to iOS 15. +/// +/// JWS promotional offer input for iOS 15+ (StoreKit 2, WWDC 2025). +/// New signature format using compact JWS string for promotional offers. +/// This provides a simpler alternative to the legacy signature-based promotional offers. +/// Back-deployed to iOS 15. +/// public sealed record PromotionalOfferJWSInputIOS { /// The promotional offer identifier from App Store Connect [JsonPropertyName("offerId")] public required string OfferId { get; init; } - /// Compact JWS string signed by your server. - /// The JWS should contain the promotional offer signature data. - /// Format: header.payload.signature (base64url encoded) + /// + /// Compact JWS string signed by your server. + /// The JWS should contain the promotional offer signature data. + /// Format: header.payload.signature (base64url encoded) + /// [JsonPropertyName("jws")] public required string Jws { get; init; } } @@ -3714,19 +3996,23 @@ public sealed record PurchaseOptions /// Limit to currently active items on iOS [JsonPropertyName("onlyIncludeActiveItemsIOS")] public bool? OnlyIncludeActiveItemsIOS { get; init; } - /// Include suspended subscriptions in the result (Android 8.1+). - /// Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. - /// Users should be directed to the subscription center to resolve payment issues. - /// Default: false (only active subscriptions are returned) + /// + /// Include suspended subscriptions in the result (Android 8.1+). + /// Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. + /// Users should be directed to the subscription center to resolve payment issues. + /// Default: false (only active subscriptions are returned) + /// [JsonPropertyName("includeSuspendedAndroid")] public bool? IncludeSuspendedAndroid { get; init; } } public sealed record PurchaseUpdatedListenerOptions { - /// iOS only. Defaults to true. When false, listener callbacks also receive - /// StoreKit replay events for a transaction ID that was already emitted during - /// the current connection session. Android ignores this option. + /// + /// iOS only. Defaults to true. When false, listener callbacks also receive + /// StoreKit replay events for a transaction ID that was already emitted during + /// the current connection session. Android ignores this option. + /// [JsonPropertyName("dedupeTransactionIOS")] public bool? DedupeTransactionIOS { get; init; } } @@ -3742,18 +4028,24 @@ public sealed record RequestPurchaseAndroidProps /// Obfuscated profile ID [JsonPropertyName("obfuscatedProfileId")] public string? ObfuscatedProfileId { get; init; } - /// Personalized offer flag. - /// When true, indicates the price was customized for this user. + /// + /// Personalized offer flag. + /// When true, indicates the price was customized for this user. + /// [JsonPropertyName("isOfferPersonalized")] public bool? IsOfferPersonalized { get; init; } - /// Offer token for one-time purchase discounts (8.0+). - /// Pass the offerToken from discountOffers - /// to apply a discount offer to the purchase. + /// + /// Offer token for one-time purchase discounts (8.0+). + /// Pass the offerToken from discountOffers + /// to apply a discount offer to the purchase. + /// [JsonPropertyName("offerToken")] public string? OfferToken { get; init; } - /// Developer billing option parameters for external payments and Billing Choice. - /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). + /// + /// Developer billing option parameters for external payments and Billing Choice. + /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). + /// [JsonPropertyName("developerBillingOption")] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -3772,14 +4064,18 @@ public sealed record RequestPurchaseIosProps /// Purchase quantity [JsonPropertyName("quantity")] public int? Quantity { get; init; } - /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). - /// iOS only supports promotional offers for auto-renewable subscriptions. + /// + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. + /// [JsonPropertyName("withOffer")] public DiscountOfferInputIOS? WithOffer { get; init; } - /// Advanced commerce data token (iOS 15+). - /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing - /// campaign tokens, affiliate IDs, or other attribution data. - /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// + /// Advanced commerce data token (iOS 15+). + /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing + /// campaign tokens, affiliate IDs, or other attribution data. + /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// [JsonPropertyName("advancedCommerceData")] public string? AdvancedCommerceData { get; init; } } @@ -3813,13 +4109,15 @@ public void Validate() void IJsonOnDeserialized.OnDeserialized() => Validate(); } -/// Platform-specific purchase request parameters. -/// -/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. -/// - apple: Always targets App Store -/// - google: Targets Play Store by default, Horizon when built with horizon flavor, -/// or Fire OS when built with amazon flavor -/// (determined at build time, not runtime) +/// +/// Platform-specific purchase request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor +/// (determined at build time, not runtime) +/// public sealed record RequestPurchasePropsByPlatforms { /// Apple-specific purchase parameters @@ -3841,31 +4139,39 @@ public sealed record RequestSubscriptionAndroidProps /// Obfuscated profile ID [JsonPropertyName("obfuscatedProfileId")] public string? ObfuscatedProfileId { get; init; } - /// Personalized offer flag. - /// When true, indicates the price was customized for this user. + /// + /// Personalized offer flag. + /// When true, indicates the price was customized for this user. + /// [JsonPropertyName("isOfferPersonalized")] public bool? IsOfferPersonalized { get; init; } /// Purchase token for upgrades/downgrades [JsonPropertyName("purchaseToken")] public string? PurchaseToken { get; init; } - /// Original external transaction ID for replacing a subscription that was - /// purchased through developer billing. Available in OpenIAP Spec 2.1.0 / - /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Original external transaction ID for replacing a subscription that was + /// purchased through developer billing. Available in OpenIAP Spec 2.1.0 / + /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// [JsonPropertyName("originalExternalTransactionId")] public string? OriginalExternalTransactionId { get; init; } /// Subscription offers [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } - /// Product-level replacement parameters (8.1.0+) - /// Use this instead of replacementMode for item-level replacement - /// This singular form requires skus to contain exactly one target product. - /// Multi-item subscription changes need a per-target replacement mapping and - /// are rejected rather than applying one oldProductId to multiple products. + /// + /// Product-level replacement parameters (8.1.0+) + /// Use this instead of replacementMode for item-level replacement + /// This singular form requires skus to contain exactly one target product. + /// Multi-item subscription changes need a per-target replacement mapping and + /// are rejected rather than applying one oldProductId to multiple products. + /// [JsonPropertyName("subscriptionProductReplacementParams")] public SubscriptionProductReplacementParamsAndroid? SubscriptionProductReplacementParams { get; init; } - /// Developer billing option parameters for external payments and Billing Choice. - /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). + /// + /// Developer billing option parameters for external payments and Billing Choice. + /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). + /// [JsonPropertyName("developerBillingOption")] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -3880,46 +4186,60 @@ public sealed record RequestSubscriptionIosProps public string? AppAccountToken { get; init; } [JsonPropertyName("quantity")] public int? Quantity { get; init; } - /// Promotional offer to apply for subscription purchases. - /// Requires server-signed offer with nonce, timestamp, keyId, and signature. + /// + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. + /// [JsonPropertyName("withOffer")] public DiscountOfferInputIOS? WithOffer { get; init; } - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// The offer is available when the customer is eligible and can be discovered - /// via StoreKit Message (automatic) or subscription offer APIs. + /// + /// Win-back offer to apply (iOS 18+) + /// Used to re-engage churned subscribers with a discount or free trial. + /// The offer is available when the customer is eligible and can be discovered + /// via StoreKit Message (automatic) or subscription offer APIs. + /// [JsonPropertyName("winBackOffer")] public WinBackOfferInputIOS? WinBackOffer { get; init; } - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. + /// + /// JWS promotional offer (iOS 15+, WWDC 2025). + /// New signature format using compact JWS string for promotional offers. + /// Back-deployed to iOS 15. + /// [JsonPropertyName("promotionalOfferJWS")] public PromotionalOfferJWSInputIOS? PromotionalOfferJws { get; init; } - /// Billing plan to use when purchasing an annual subscription that offers - /// monthly billing with a 12-month commitment (iOS 26.4+). + /// + /// Billing plan to use when purchasing an annual subscription that offers + /// monthly billing with a 12-month commitment (iOS 26.4+). + /// [JsonPropertyName("billingPlanType")] public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; } - /// Compact JWS string for overriding introductory offer eligibility - /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility. - /// Generate the JWS on your server and pass it to StoreKit's - /// introductoryOfferEligibility(compactJWS:) purchase option. + /// + /// Compact JWS string for overriding introductory offer eligibility + /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility. + /// Generate the JWS on your server and pass it to StoreKit's + /// introductoryOfferEligibility(compactJWS:) purchase option. + /// [JsonPropertyName("compactJWS")] public string? CompactJws { get; init; } - /// Advanced commerce data token (iOS 15+). - /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing - /// campaign tokens, affiliate IDs, or other attribution data. - /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// + /// Advanced commerce data token (iOS 15+). + /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing + /// campaign tokens, affiliate IDs, or other attribution data. + /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// [JsonPropertyName("advancedCommerceData")] public string? AdvancedCommerceData { get; init; } } -/// Platform-specific subscription request parameters. -/// -/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. -/// - apple: Always targets App Store -/// - google: Targets Play Store by default, Horizon when built with horizon flavor, -/// or Fire OS when built with amazon flavor -/// (determined at build time, not runtime) +/// +/// Platform-specific subscription request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor +/// (determined at build time, not runtime) +/// public sealed record RequestSubscriptionPropsByPlatforms { /// Apple-specific subscription parameters @@ -3957,26 +4277,32 @@ public sealed record RequestVerifyPurchaseWithIapkitGoogleProps public required string PurchaseToken { get; init; } } -/// Platform-specific verification parameters for IAPKit. -/// -/// - apple: Verifies via App Store (JWS token) -/// - google: Verifies via Play Store (purchase token) -/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) +/// +/// Platform-specific verification parameters for IAPKit. +/// +/// - apple: Verifies via App Store (JWS token) +/// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) +/// public sealed record RequestVerifyPurchaseWithIapkitProps { /// API key used for the Authorization header (Bearer {apiKey}). [JsonPropertyName("apiKey")] public string? ApiKey { get; init; } - /// Available in OpenIAP Spec 2.3.1 / openiap-apple 2.4.0 / openiap-google 2.4.0. - /// Base URL for the IAPKit server. Defaults to https://kit.openiap.dev. - /// Set this to a reachable HTTP(S) origin when self-hosting or testing a local IAPKit server. - /// The apiKey must be issued by the same IAPKit/Convex deployment as this server. + /// + /// Available in OpenIAP Spec 2.3.1 / openiap-apple 2.4.0 / openiap-google 2.4.0. + /// Base URL for the IAPKit server. Defaults to https://kit.openiap.dev. + /// Set this to a reachable HTTP(S) origin when self-hosting or testing a local IAPKit server. + /// The apiKey must be issued by the same IAPKit/Convex deployment as this server. + /// [JsonPropertyName("baseUrl")] public string? BaseUrl { get; init; } - /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. - /// Include the product's public IAPKit client payload in a valid Apple or - /// Google verification response. Defaults to false so existing response - /// shapes and bandwidth remain unchanged. + /// + /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. + /// Include the product's public IAPKit client payload in a valid Apple or + /// Google verification response. Defaults to false so existing response + /// shapes and bandwidth remain unchanged. + /// [JsonPropertyName("includeClientPayload")] public bool? IncludeClientPayload { get; init; } /// Apple App Store verification parameters. @@ -3990,9 +4316,11 @@ public sealed record RequestVerifyPurchaseWithIapkitProps public RequestVerifyPurchaseWithIapkitAmazonProps? Amazon { get; init; } } -/// Product-level subscription replacement parameters (Android) -/// Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams -/// Available in Google Play Billing Library 8.1.0+ +/// +/// Product-level subscription replacement parameters (Android) +/// Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams +/// Available in Google Play Billing Library 8.1.0+ +/// public sealed record SubscriptionProductReplacementParamsAndroid { /// The old product ID that needs to be replaced @@ -4003,8 +4331,10 @@ public sealed record SubscriptionProductReplacementParamsAndroid public required SubscriptionReplacementModeAndroid ReplacementMode { get; init; } } -/// Apple App Store verification parameters. -/// Used for server-side receipt validation via App Store Server API. +/// +/// Apple App Store verification parameters. +/// Used for server-side receipt validation via App Store Server API. +/// public sealed record VerifyPurchaseAppleOptions { /// Product SKU to validate @@ -4012,10 +4342,12 @@ public sealed record VerifyPurchaseAppleOptions public required string Sku { get; init; } } -/// Google Play Store verification parameters. -/// Used for server-side receipt validation via Google Play Developer API. -/// -/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. +/// +/// Google Play Store verification parameters. +/// Used for server-side receipt validation via Google Play Developer API. +/// +/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. +/// public sealed record VerifyPurchaseGoogleOptions { /// Product SKU to validate @@ -4024,12 +4356,16 @@ public sealed record VerifyPurchaseGoogleOptions /// Android package name (e.g., com.example.app) [JsonPropertyName("packageName")] public required string PackageName { get; init; } - /// Purchase token from the purchase response. - /// ⚠️ Sensitive: Do not log this value. + /// + /// Purchase token from the purchase response. + /// ⚠️ Sensitive: Do not log this value. + /// [JsonPropertyName("purchaseToken")] public required string PurchaseToken { get; init; } - /// Google OAuth2 access token for API authentication. - /// ⚠️ Sensitive: Do not log this value. + /// + /// Google OAuth2 access token for API authentication. + /// ⚠️ Sensitive: Do not log this value. + /// [JsonPropertyName("accessToken")] public required string AccessToken { get; init; } /// Whether this is a subscription purchase (affects API endpoint used) @@ -4037,11 +4373,13 @@ public sealed record VerifyPurchaseGoogleOptions public bool? IsSub { get; init; } } -/// Meta Horizon (Quest) verification parameters. -/// Used for server-side entitlement verification via Meta's S2S API. -/// POST https://graph.oculus.com/$APP_ID/verify_entitlement -/// -/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. +/// +/// Meta Horizon (Quest) verification parameters. +/// Used for server-side entitlement verification via Meta's S2S API. +/// POST https://graph.oculus.com/$APP_ID/verify_entitlement +/// +/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. +/// public sealed record VerifyPurchaseHorizonOptions { /// The SKU for the add-on item, defined in Meta Developer Dashboard @@ -4050,17 +4388,21 @@ public sealed record VerifyPurchaseHorizonOptions /// The user ID of the user whose purchase you want to verify [JsonPropertyName("userId")] public required string UserId { get; init; } - /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). - /// ⚠️ Sensitive: Do not log this value. + /// + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + /// ⚠️ Sensitive: Do not log this value. + /// [JsonPropertyName("accessToken")] public required string AccessToken { get; init; } } -/// Platform-specific purchase verification parameters. -/// -/// - apple: Verifies via App Store Server API -/// - google: Verifies via Google Play Developer API -/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) +/// +/// Platform-specific purchase verification parameters. +/// +/// - apple: Verifies via App Store Server API +/// - google: Verifies via Google Play Developer API +/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) +/// public sealed record VerifyPurchaseProps { /// Apple App Store verification parameters. @@ -4082,10 +4424,12 @@ public sealed record VerifyPurchaseWithProviderProps public RequestVerifyPurchaseWithIapkitProps? Iapkit { get; init; } } -/// Win-back offer input for iOS 18+ (StoreKit 2) -/// Win-back offers are used to re-engage churned subscribers. -/// The offer is automatically presented via StoreKit Message when eligible, -/// or can be applied programmatically during purchase. +/// +/// Win-back offer input for iOS 18+ (StoreKit 2) +/// Win-back offers are used to re-engage churned subscribers. +/// The offer is automatically presented via StoreKit Message when eligible, +/// or can be applied programmatically during purchase. +/// public sealed record WinBackOfferInputIOS { /// The win-back offer ID from App Store Connect @@ -4100,306 +4444,404 @@ public sealed record WinBackOfferInputIOS /// GraphQL root mutation operations. public interface MutationResolver { - /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android + /// + /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android + /// Task AcknowledgePurchaseAndroidAsync(string purchaseToken); - /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios + /// + /// Present the refund request sheet (iOS 15+). See also Features → Refund. + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios + /// Task BeginRefundRequestIOSAsync(string sku); - /// Clear pending transactions in the queue (sandbox helper). - /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios + /// + /// Clear pending transactions in the queue (sandbox helper). + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios + /// Task ClearTransactionIOSAsync(); - /// Consume a consumable purchase so it can be re-bought. - /// See: https://openiap.dev/docs/apis/android/consume-purchase-android + /// + /// Consume a consumable purchase so it can be re-bought. + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android + /// Task ConsumePurchaseAndroidAsync(string purchaseToken); - /// Create the reporting details and external transaction token required by a billing program. - /// Introduced in Play Billing 8.2.0. External Offer and External Content Link integrations - /// must use 8.2.1+ and create fresh details immediately before every redirect session; - /// do not cache the token for a later redirect. The same token may report multiple purchases - /// made during one External Offer session. - /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. - /// Returns external transaction token needed for reporting external transactions. - /// developerBillingType is optional. When program is BILLING_CHOICE and developerBillingType is omitted, - /// native Android defaults it to IN_APP. - /// The Billing Choice extension is available in OpenIAP Spec 2.1.0 / - /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// + /// Create the reporting details and external transaction token required by a billing program. + /// Introduced in Play Billing 8.2.0. External Offer and External Content Link integrations + /// must use 8.2.1+ and create fresh details immediately before every redirect session; + /// do not cache the token for a later redirect. The same token may report multiple purchases + /// made during one External Offer session. + /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. + /// Returns external transaction token needed for reporting external transactions. + /// developerBillingType is optional. When program is BILLING_CHOICE and developerBillingType is omitted, + /// native Android defaults it to IN_APP. + /// The Billing Choice extension is available in OpenIAP Spec 2.1.0 / + /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// Task CreateBillingProgramReportingDetailsAndroidAsync(BillingProgramAndroid program, DeveloperBillingTypeAndroid? developerBillingType = null); - /// Open the platform's subscription management UI. - /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions + /// + /// Open the platform's subscription management UI. + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions + /// Task DeepLinkToSubscriptionsAsync(DeepLinkOptions? options = null); - /// Close the store connection and release resources. - /// See: https://openiap.dev/docs/apis/end-connection + /// + /// Close the store connection and release resources. + /// See: https://openiap.dev/docs/apis/end-connection + /// Task EndConnectionAsync(); - /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://openiap.dev/docs/apis/finish-transaction + /// + /// Complete a transaction after server-side verification. Required on Android within 3 days. + /// See: https://openiap.dev/docs/apis/finish-transaction + /// Task FinishTransactionAsync(PurchaseInput purchase, bool? isConsumable = null); - /// Initialize the store connection. Call before any IAP API. - /// See: https://openiap.dev/docs/apis/init-connection + /// + /// Initialize the store connection. Call before any IAP API. + /// See: https://openiap.dev/docs/apis/init-connection + /// Task InitConnectionAsync(InitConnectionConfig? config = null); - /// Check whether a billing program (e.g., External Payments) is available for the current user. - /// Replaces the deprecated isExternalOfferAvailableAsync API. - /// Introduced in Google Play Billing Library 8.2.0. External Offer and External - /// Content Link integrations must use 8.2.1+ because 8.2.1 fixes this API. - /// Returns availability result with isAvailable flag. - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android + /// + /// Check whether a billing program (e.g., External Payments) is available for the current user. + /// Replaces the deprecated isExternalOfferAvailableAsync API. + /// Introduced in Google Play Billing Library 8.2.0. External Offer and External + /// Content Link integrations must use 8.2.1+ because 8.2.1 fixes this API. + /// Returns availability result with isAvailable flag. + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android + /// Task IsBillingProgramAvailableAndroidAsync(BillingProgramAndroid program); - /// Launch an external content/offer link from inside the Billing Programs flow (introduced in - /// Play Billing 8.2.0; External Offer and External Content Link require 8.2.1+), - /// including developer-rendered Billing Choice external-link flows. - /// Billing Choice availability: OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). - /// Replaces the deprecated showExternalOfferInformationDialog API. - /// Shows Play Store dialog and optionally launches external URL. - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/launch-external-link-android + /// + /// Launch an external content/offer link from inside the Billing Programs flow (introduced in + /// Play Billing 8.2.0; External Offer and External Content Link require 8.2.1+), + /// including developer-rendered Billing Choice external-link flows. + /// Billing Choice availability: OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). + /// Replaces the deprecated showExternalOfferInformationDialog API. + /// Shows Play Store dialog and optionally launches external URL. + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android + /// Task LaunchExternalLinkAndroidAsync(LaunchExternalLinkParamsAndroid @params); - /// Open the Google Play offer/promo code redemption flow so the user can enter a code. - /// On Google Play builds, launches the Play Store redeem page - /// (https://play.google.com/redeem). A purchase listener can receive the redeemed - /// purchase while the app is running with an active billing connection; always - /// reconcile with getAvailablePurchases when the app resumes. - /// Does not require the billing client to be initialized (no Play Billing version requirement). - /// Available in OpenIAP Spec 2.4.2 / openiap-google 2.5.0. - /// Android counterpart of presentCodeRedemptionSheetIOS. - /// Returns true when the redemption flow was launched, or false when the current - /// store flavor does not provide an equivalent redemption flow. - /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android + /// + /// Open the Google Play offer/promo code redemption flow so the user can enter a code. + /// On Google Play builds, launches the Play Store redeem page + /// (https://play.google.com/redeem). A purchase listener can receive the redeemed + /// purchase while the app is running with an active billing connection; always + /// reconcile with getAvailablePurchases when the app resumes. + /// Does not require the billing client to be initialized (no Play Billing version requirement). + /// Available in OpenIAP Spec 2.4.2 / openiap-google 2.5.0. + /// Android counterpart of presentCodeRedemptionSheetIOS. + /// Returns true when the redemption flow was launched, or false when the current + /// store flavor does not provide an equivalent redemption flow. + /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android + /// Task OpenRedeemOfferCodeAndroidAsync(); - /// Show the App Store offer code redemption sheet. - /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or - /// visionOS 27+, returns the verified transaction produced by the redemption. - /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, - /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. - /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the - /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 - /// call has no effect and returns null. Reconcile null results from a presented - /// sheet through the normal transaction listener or an explicit - /// available-purchases refresh. - /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// + /// Show the App Store offer code redemption sheet. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit + /// available-purchases refresh. + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// Task PresentCodeRedemptionSheetIOSAsync(); - /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// + /// Present an external purchase link, StoreKit External (iOS 16+). + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// Task PresentExternalPurchaseLinkIOSAsync(string url); - /// Present the external purchase notice sheet (iOS 17.4+). - /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// + /// Present the external purchase notice sheet (iOS 17.4+). + /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// Task PresentExternalPurchaseNoticeSheetIOSAsync(); - /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://openiap.dev/docs/apis/request-purchase + /// + /// Initiate a purchase or subscription flow; rely on events for final state. + /// See: https://openiap.dev/docs/apis/request-purchase + /// Task RequestPurchaseAsync(RequestPurchaseProps @params); - /// Restore non-consumable and active subscription purchases. - /// See: https://openiap.dev/docs/apis/restore-purchases + /// + /// Restore non-consumable and active subscription purchases. + /// See: https://openiap.dev/docs/apis/restore-purchases + /// Task RestorePurchasesAsync(); - /// Show Google's mandatory information dialog before a developer-rendered, - /// in-app Billing Choice screen. - /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/show-billing-program-information-dialog-android + /// + /// Show Google's mandatory information dialog before a developer-rendered, + /// in-app Billing Choice screen. + /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/show-billing-program-information-dialog-android + /// Task ShowBillingProgramInformationDialogAndroidAsync(BillingProgramInformationDialogParamsAndroid @params); - /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). - /// Call this after a deliberate customer interaction before linking out to external purchases. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios - /// Parameter noticeType: Notice type determining the style of disclosure + /// + /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). + /// Call this after a deliberate customer interaction before linking out to external purchases. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// Parameter noticeType: Notice type determining the style of disclosure + /// Task ShowExternalPurchaseCustomLinkNoticeIOSAsync(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); - /// Overlay Play billing in-app messages, such as payment issues or subscription price-change confirmations. - /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 - /// (upstream API available since Play Billing 4.1.0). - /// Returns a response code and, when the subscription status changes, the related purchase token. - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/show-in-app-messages-android + /// + /// Overlay Play billing in-app messages, such as payment issues or subscription price-change confirmations. + /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 + /// (upstream API available since Play Billing 4.1.0). + /// Returns a response code and, when the subscription status changes, the related purchase token. + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/show-in-app-messages-android + /// Task ShowInAppMessagesAndroidAsync(InAppMessageParamsAndroid? @params = null); - /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// + /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// Task> ShowManageSubscriptionsIOSAsync(); - /// Force sync transactions with the App Store (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/sync-ios + /// + /// Force sync transactions with the App Store (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/sync-ios + /// Task SyncIOSAsync(); - /// Verify a purchase against your own backend. Returns a platform-specific - /// variant of VerifyPurchaseResult — VerifyPurchaseResultIOS exposes isValid - /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store - /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. - /// Inspect the concrete variant before reading fields. - /// See: https://openiap.dev/docs/features/validation#verify-purchase + /// + /// Verify a purchase against your own backend. Returns a platform-specific + /// variant of VerifyPurchaseResult — VerifyPurchaseResultIOS exposes isValid + /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store + /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. + /// Inspect the concrete variant before reading fields. + /// See: https://openiap.dev/docs/features/validation#verify-purchase + /// Task VerifyPurchaseAsync(VerifyPurchaseProps options); - /// Verify via a managed provider without standing up your own server. The - /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform - /// availability may differ by implementation. - /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider + /// + /// Verify via a managed provider without standing up your own server. The + /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform + /// availability may differ by implementation. + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider + /// Task VerifyPurchaseWithProviderAsync(VerifyPurchaseWithProviderProps options); } /// GraphQL root query operations. public interface QueryResolver { - /// Check eligibility for the external purchase notice sheet (iOS 17.4+). - /// Uses ExternalPurchase.canPresent. - /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// + /// Check eligibility for the external purchase notice sheet (iOS 17.4+). + /// Uses ExternalPurchase.canPresent. + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// Task CanPresentExternalPurchaseNoticeIOSAsync(); - /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios + /// + /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios + /// Task CurrentEntitlementIOSAsync(string sku); - /// Fetch products or subscriptions from the store. - /// See: https://openiap.dev/docs/apis/fetch-products + /// + /// Fetch products or subscriptions from the store. + /// See: https://openiap.dev/docs/apis/fetch-products + /// Task FetchProductsAsync(ProductRequest @params); - /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://openiap.dev/docs/apis/get-active-subscriptions + /// + /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). + /// See: https://openiap.dev/docs/apis/get-active-subscriptions + /// Task> GetActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); - /// List every StoreKit transaction (finished + unfinished) for the current user. - /// Requires the SKIncludeConsumableInAppPurchaseHistory Info.plist key in the host app - /// for finished consumables to be included (iOS 18+). - /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios + /// + /// List every StoreKit transaction (finished + unfinished) for the current user. + /// Requires the SKIncludeConsumableInAppPurchaseHistory Info.plist key in the host app + /// for finished consumables to be included (iOS 18+). + /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios + /// Task> GetAllTransactionsIOSAsync(); - /// Fetch the app transaction (iOS 16+). - /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios + /// + /// Fetch the app transaction (iOS 16+). + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios + /// Task GetAppTransactionIOSAsync(); - /// List active purchases for the current user. - /// See: https://openiap.dev/docs/apis/get-available-purchases + /// + /// List active purchases for the current user. + /// See: https://openiap.dev/docs/apis/get-available-purchases + /// Task> GetAvailablePurchasesAsync(PurchaseOptions? options = null); - /// Fetch Play Billing assets and loyalty text for developer-rendered Billing Choice screens. - /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// Throws OpenIapError.NotPrepared if billing client is not ready. - /// See: https://openiap.dev/docs/apis/android/get-billing-choice-info-android + /// + /// Fetch Play Billing assets and loyalty text for developer-rendered Billing Choice screens. + /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Throws OpenIapError.NotPrepared if billing client is not ready. + /// See: https://openiap.dev/docs/apis/android/get-billing-choice-info-android + /// Task GetBillingChoiceInfoAndroidAsync(GetBillingChoiceInfoParamsAndroid @params); - /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). - /// Use this token to report transactions made through ExternalPurchaseCustomLink. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios - /// Parameter tokenType: Token type: acquisition (new customers) or services (existing customers) + /// + /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). + /// Use this token to report transactions made through ExternalPurchaseCustomLink. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// Parameter tokenType: Token type: acquisition (new customers) or services (existing customers) + /// Task GetExternalPurchaseCustomLinkTokenIOSAsync(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); - /// List unfinished StoreKit transactions in the queue. - /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// + /// List unfinished StoreKit transactions in the queue. + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// Task> GetPendingTransactionsIOSAsync(); - /// Read the App Store-promoted product, if any (iOS 15+). - /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the - /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an - /// externally redeemed win-back offer, OpenIAP preserves it for the next - /// matching requestPurchase unless the caller supplies an explicit win-back or - /// promotional offer. - /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios + /// + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios + /// Task GetPromotedProductIOSAsync(); - /// Get base64-encoded receipt data (legacy validation). - /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios + /// + /// Get base64-encoded receipt data (legacy validation). + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios + /// Task GetReceiptDataIOSAsync(); - /// Return the store-authoritative country code: ISO 3166-1 alpha-3 on Apple - /// platforms and alpha-2 on Android. The operation fails when the store cannot - /// provide a value; implementations must not synthesize a locale fallback. - /// See: https://openiap.dev/docs/apis/get-storefront + /// + /// Return the store-authoritative country code: ISO 3166-1 alpha-3 on Apple + /// platforms and alpha-2 on Android. The operation fails when the store cannot + /// provide a value; implementations must not synthesize a locale fallback. + /// See: https://openiap.dev/docs/apis/get-storefront + /// Task GetStorefrontAsync(); - /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// + /// Return the JWS string for a transaction (StoreKit 2). + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// Task GetTransactionJwsIOSAsync(string sku); - /// Check whether the user has any active subscription. - /// See: https://openiap.dev/docs/apis/has-active-subscriptions + /// + /// Check whether the user has any active subscription. + /// See: https://openiap.dev/docs/apis/has-active-subscriptions + /// Task HasActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); - /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). - /// Returns true if the app can use custom external purchase links. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// + /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). + /// Returns true if the app can use custom external purchase links. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// Task IsEligibleForExternalPurchaseCustomLinkIOSAsync(); - /// Check intro-offer eligibility for a subscription group. - /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// + /// Check intro-offer eligibility for a subscription group. + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// Task IsEligibleForIntroOfferIOSAsync(string groupId); - /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// + /// Check whether a transaction's JWS verification passed (StoreKit 2). + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// Task IsTransactionVerifiedIOSAsync(string sku); - /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios + /// + /// Get the latest verified transaction for a product, using StoreKit 2. + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios + /// Task LatestTransactionIOSAsync(string sku); - /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios + /// + /// Get subscription status objects from StoreKit 2 (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios + /// Task> SubscriptionStatusIOSAsync(string sku); } /// GraphQL root subscription operations. public interface SubscriptionResolver { - /// Fires when a user selects developer billing in an External Payments or - /// Billing Choice flow (Android only). The payload can contain an external - /// transaction token, link URI, original transaction ID, and selected products. - /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / - /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Fires when a user selects developer billing in an External Payments or + /// Billing Choice flow (Android only). The payload can contain an external + /// transaction token, link URI, original transaction ID, and selected products. + /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / + /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Task DeveloperProvidedBillingAndroidAsync(); - /// Fires when the App Store surfaces a promoted product (iOS only). - /// A win-back offer attached to PurchaseIntent is preserved for the next - /// matching requestPurchase unless the caller supplies an explicit win-back or - /// promotional offer. + /// + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. + /// Task PromotedProductIOSAsync(); /// Fires when a purchase fails or is cancelled Task PurchaseErrorAsync(); - /// Fires when a purchase completes successfully or a pending purchase resolves - /// Options can opt iOS listeners into duplicate StoreKit transaction replays - /// for diagnostics; default listeners receive one event per transaction ID - /// during a single connection session. + /// + /// Fires when a purchase completes successfully or a pending purchase resolves + /// Options can opt iOS listeners into duplicate StoreKit transaction replays + /// for diagnostics; default listeners receive one event per transaction ID + /// during a single connection session. + /// Task PurchaseUpdatedAsync(PurchaseUpdatedListenerOptions? options = null); - /// Fires when a subscription enters a billing-issue state that needs user action - /// (payment method failed, card expired, etc.). Cross-platform unification: - /// - /// - iOS 16.4+ / Mac Catalyst 16.4+ / visionOS 1.0+: delivered via StoreKit 2 - /// `Message.Reason.billingIssue`. - /// - Android (Play flavor, Billing 8.1+): emitted when `isSuspended == true` is first detected - /// on a previously healthy subscription. Requires Google Play Billing Library 8.1.0 or newer. - /// - Android (Horizon flavor): NOT emitted. The Horizon Billing Compatibility SDK implements - /// the Play Billing 7.0 API surface which does not expose a suspended-subscription signal. - /// - Android (Amazon flavor): NOT emitted. Amazon Appstore IAP does not expose an - /// equivalent subscription billing-issue signal. - /// - /// Listeners should not assume the event will fire on every store. Direct users to the - /// platform subscription management UI (`deepLinkToSubscriptions`) to resolve the issue. + /// + /// Fires when a subscription enters a billing-issue state that needs user action + /// (payment method failed, card expired, etc.). Cross-platform unification: + /// + /// - iOS 16.4+ / Mac Catalyst 16.4+ / visionOS 1.0+: delivered via StoreKit 2 + /// `Message.Reason.billingIssue`. + /// - Android (Play flavor, Billing 8.1+): emitted when `isSuspended == true` is first detected + /// on a previously healthy subscription. Requires Google Play Billing Library 8.1.0 or newer. + /// - Android (Horizon flavor): NOT emitted. The Horizon Billing Compatibility SDK implements + /// the Play Billing 7.0 API surface which does not expose a suspended-subscription signal. + /// - Android (Amazon flavor): NOT emitted. Amazon Appstore IAP does not expose an + /// equivalent subscription billing-issue signal. + /// + /// Listeners should not assume the event will fire on every store. Direct users to the + /// platform subscription management UI (`deepLinkToSubscriptions`) to resolve the issue. + /// Task SubscriptionBillingIssueAsync(); - /// Fires when a user selects alternative billing in the User Choice Billing dialog (Android only) - /// Only triggered when the user selects alternative billing instead of Google Play billing + /// + /// Fires when a user selects alternative billing in the User Choice Billing dialog (Android only) + /// Only triggered when the user selects alternative billing instead of Google Play billing + /// Task UserChoiceBillingAndroidAsync(); } diff --git a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx index 6d27637be..5d2074c05 100644 --- a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -47,6 +47,10 @@ val promotedProductListener: Flow`} dart: ( {`// iOS only Stream get purchasePromoted;`} + ), + gdscript: ( + {`# iOS only +signal promoted_product_ios(product_id: String)`} ), csharp: ( {`using OpenIap; @@ -73,29 +77,33 @@ IObservable promotedProducts = OpenIapClient.Instance.PromotedProductIOS } from 'expo-iap'; const subscription = promotedProductListenerIOS(async (product) => { - const productId = product.id; - console.log('Promoted product tapped:', productId); - - // Refetch as "all" because the listener's legacy Product payload does not - // distinguish promoted subscriptions at the type level. - const items = (await fetchProducts({ skus: [productId], type: 'all' })) ?? []; - const item = items.find((candidate) => candidate.id === productId); - if (!item) return; - - const confirmed = await showPurchaseConfirmation(item); - - if (confirmed) { - if (item.type === 'subs') { - await requestPurchase({ - request: { apple: { sku: productId } }, - type: 'subs' - }); - } else { - await requestPurchase({ - request: { apple: { sku: productId } }, - type: 'in-app' - }); + try { + const productId = product.id; + console.log('Promoted product tapped:', productId); + + // Refetch as "all" because the listener's legacy Product payload does not + // distinguish promoted subscriptions at the type level. + const items = (await fetchProducts({ skus: [productId], type: 'all' })) ?? []; + const item = items.find((candidate) => candidate.id === productId); + if (!item) return; + + const confirmed = await showPurchaseConfirmation(item); + + if (confirmed) { + if (item.type === 'subs') { + await requestPurchase({ + request: { apple: { sku: productId } }, + type: 'subs' + }); + } else { + await requestPurchase({ + request: { apple: { sku: productId } }, + type: 'in-app' + }); + } } + } catch (error) { + console.error('Promoted purchase failed:', error); } }); @@ -154,6 +162,7 @@ func stopListeningForPromotedProducts() { kmp: ( {`import io.github.hyochan.kmpiap.KmpIAP import io.github.hyochan.kmpiap.openiap.* +import kotlinx.coroutines.CancellationException val iap = KmpIAP() @@ -161,41 +170,47 @@ scope.launch { iap.promotedProductListener.collect { productId -> productId ?: return@collect - val result = iap.fetchProducts( - ProductRequest(skus = listOf(productId), type = ProductQueryType.All) - ) - val item = (result as? FetchProductsResultAll) - ?.value - ?.firstOrNull() - - when (item) { - is ProductOrSubscription.ProductItem -> { - if (!showPurchaseConfirmation(item.value)) return@collect - iap.requestPurchase( - RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - apple = RequestPurchaseIosProps(sku = productId) - ) - ), - type = ProductQueryType.InApp + try { + val result = iap.fetchProducts( + ProductRequest(skus = listOf(productId), type = ProductQueryType.All) + ) + val item = (result as? FetchProductsResultAll) + ?.value + ?.firstOrNull() + + when (item) { + is ProductOrSubscription.ProductItem -> { + if (!showPurchaseConfirmation(item.value)) return@collect + iap.requestPurchase( + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + apple = RequestPurchaseIosProps(sku = productId) + ) + ), + type = ProductQueryType.InApp + ) ) - ) - } - is ProductOrSubscription.ProductSubscriptionItem -> { - if (!showPurchaseConfirmation(item.value)) return@collect - iap.requestPurchase( - RequestPurchaseProps( - request = RequestPurchaseProps.Request.Subscription( - RequestSubscriptionPropsByPlatforms( - apple = RequestSubscriptionIosProps(sku = productId) - ) - ), - type = ProductQueryType.Subs + } + is ProductOrSubscription.ProductSubscriptionItem -> { + if (!showPurchaseConfirmation(item.value)) return@collect + iap.requestPurchase( + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Subscription( + RequestSubscriptionPropsByPlatforms( + apple = RequestSubscriptionIosProps(sku = productId) + ) + ), + type = ProductQueryType.Subs + ) ) - ) + } + null -> Unit } - null -> Unit + } catch (error: CancellationException) { + throw error + } catch (error: Exception) { + println("Promoted purchase failed: \${error.message}") } } }`} @@ -207,40 +222,77 @@ scope.launch { final iap = FlutterInappPurchase.instance; final subscription = iap.purchasePromoted.listen((productId) async { if (productId == null) return; - print('Promoted product tapped: $productId'); + try { + print('Promoted product tapped: $productId'); - // Fetch product details - final products = await iap.fetchProducts( - skus: [productId], - type: ProductQueryType.All, - ); + final products = await iap.fetchProducts( + skus: [productId], + type: ProductQueryType.All, + ); - if (products.isNotEmpty) { - // Show product info to user and confirm purchase - final confirmed = await showPurchaseConfirmation(products.first); + if (products.isNotEmpty) { + final confirmed = await showPurchaseConfirmation(products.first); - if (confirmed) { - if (products.first.type == ProductType.Subs) { - await iap.requestPurchase( - RequestPurchaseProps.subs(( - apple: RequestSubscriptionIosProps(sku: productId), - google: null, - )), - ); - } else { - await iap.requestPurchase( - RequestPurchaseProps.inApp(( - apple: RequestPurchaseIosProps(sku: productId), - google: null, - )), - ); + if (confirmed) { + if (products.first.type == ProductType.Subs) { + await iap.requestPurchase( + RequestPurchaseProps.subs(( + apple: RequestSubscriptionIosProps(sku: productId), + google: null, + )), + ); + } else { + await iap.requestPurchase( + RequestPurchaseProps.inApp(( + apple: RequestPurchaseIosProps(sku: productId), + google: null, + )), + ); + } } } + } catch (error) { + print('Promoted purchase failed: $error'); } }); // Cleanup when done subscription.cancel();`} + ), + gdscript: ( + {`const Types = preload("res://addons/godot-iap/types.gd") + +func _ready() -> void: + GodotIapPlugin.promoted_product_ios.connect(_on_promoted_product_ios) + +func _on_promoted_product_ios(product_id: String) -> void: + var request := Types.ProductRequest.new() + var skus: Array[String] = [product_id] + request.skus = skus + request.type = Types.ProductQueryType.ALL + + var items = await GodotIapPlugin.fetch_products(request) + if items.is_empty(): + return + + var item = items[0] + if not await show_purchase_confirmation(item): + return + + var props := Types.RequestPurchaseProps.new() + if item is Types.ProductSubscriptionIOS: + props.request_subscription = Types.RequestSubscriptionPropsByPlatforms.new() + props.request_subscription.apple = Types.RequestSubscriptionIosProps.new() + props.request_subscription.apple.sku = product_id + props.type = Types.ProductQueryType.SUBS + else: + props.request = Types.RequestPurchasePropsByPlatforms.new() + props.request.apple = Types.RequestPurchaseIosProps.new() + props.request.apple.sku = product_id + props.type = Types.ProductQueryType.IN_APP + + # Results arrive through purchase_updated or purchase_error. + GodotIapPlugin.request_purchase(props)`} ), csharp: ( {`using OpenIap; @@ -250,36 +302,48 @@ var iap = OpenIapClient.Instance; var query = (QueryResolver)iap; var mutate = (MutationResolver)iap; -using var subscription = iap.PromotedProductIOS.Subscribe(async productId => +async Task HandlePromotedProductAsync(string productId) { - var result = await query.FetchProductsAsync(new ProductRequest + try { - Skus = new[] { productId }, - Type = ProductQueryType.All, - }); + var result = await query.FetchProductsAsync(new ProductRequest + { + Skus = new[] { productId }, + Type = ProductQueryType.All, + }); - var product = (result as FetchProductsResultAll)?.Value?.FirstOrDefault(); - if (product is null || !await ShowPurchaseConfirmationAsync(product)) return; + var product = (result as FetchProductsResultAll)?.Value?.FirstOrDefault(); + if (product is null || !await ShowPurchaseConfirmationAsync(product)) return; - var props = product is ProductSubscription - ? new RequestPurchaseProps - { - RequestSubscription = new RequestSubscriptionPropsByPlatforms + var props = product is ProductSubscription + ? new RequestPurchaseProps { - Apple = new RequestSubscriptionIosProps { Sku = productId }, - }, - Type = ProductQueryType.Subs, - } - : new RequestPurchaseProps - { - RequestPurchase = new RequestPurchasePropsByPlatforms + RequestSubscription = new RequestSubscriptionPropsByPlatforms + { + Apple = new RequestSubscriptionIosProps { Sku = productId }, + }, + Type = ProductQueryType.Subs, + } + : new RequestPurchaseProps { - Apple = new RequestPurchaseIosProps { Sku = productId }, - }, - Type = ProductQueryType.InApp, - }; + RequestPurchase = new RequestPurchasePropsByPlatforms + { + Apple = new RequestPurchaseIosProps { Sku = productId }, + }, + Type = ProductQueryType.InApp, + }; + + await mutate.RequestPurchaseAsync(props); + } + catch (Exception error) + { + Console.Error.WriteLine($"Promoted purchase failed: {error.Message}"); + } +} - await mutate.RequestPurchaseAsync(props); +using var subscription = iap.PromotedProductIOS.Subscribe(productId => +{ + _ = HandlePromotedProductAsync(productId); });`} ), }} diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt index 68f2ccee4..65042bb6a 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt @@ -26,17 +26,15 @@ import dev.hyo.openiap.ProductQueryType import dev.hyo.openiap.ProductType import dev.hyo.openiap.ProductRequest import dev.hyo.openiap.ProductSubscription -import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun AllProductsScreen( - navController: NavController, - storeParam: OpenIapStore? = null + navController: NavController ) { - val iapStore = currentOpenIapStore(storeParam) + val iapStore = currentOpenIapStore() val products by iapStore.products.collectAsState() val subscriptions by iapStore.subscriptions.collectAsState() val status by iapStore.status.collectAsState() diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt index 25b39b36b..63a3219d1 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AvailablePurchasesScreen.kt @@ -23,17 +23,15 @@ import dev.hyo.martie.screens.uis.* import dev.hyo.martie.util.PREMIUM_SUBSCRIPTION_PRODUCT_ID import dev.hyo.openiap.PurchaseAndroid import dev.hyo.openiap.PurchaseState -import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun AvailablePurchasesScreen( - navController: NavController, - storeParam: OpenIapStore? = null + navController: NavController ) { - val iapStore = currentOpenIapStore(storeParam) + val iapStore = currentOpenIapStore() val purchases by iapStore.availablePurchases.collectAsState() val status by iapStore.status.collectAsState() val connectionStatus by iapStore.isConnected.collectAsState() diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt index d7be98f04..523970487 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OfferCodeScreen.kt @@ -21,18 +21,16 @@ import androidx.navigation.NavController import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* import dev.hyo.martie.util.findActivity -import dev.hyo.openiap.store.OpenIapStore import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun OfferCodeScreen( - navController: NavController, - storeParam: OpenIapStore? = null + navController: NavController ) { val context = LocalContext.current val activity = remember(context) { context.findActivity() } - val iapStore = currentOpenIapStore(storeParam) + val iapStore = currentOpenIapStore() var showResult by remember { mutableStateOf(false) } var resultMessage by remember { mutableStateOf("") } diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt index d3c67f496..24c040681 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/OpenIapStoreContext.kt @@ -4,9 +4,9 @@ import androidx.compose.runtime.Composable import dev.hyo.openiap.IapContext import dev.hyo.openiap.store.OpenIapStore -/** Uses an injected store or the Activity-owned store provided by AppNavigation. */ +/** Uses the Activity-owned store provided by AppNavigation. */ @Composable -internal fun currentOpenIapStore(storeParam: OpenIapStore?): OpenIapStore = - storeParam ?: requireNotNull(IapContext.LocalOpenIapStore.current) { +internal fun currentOpenIapStore(): OpenIapStore = + requireNotNull(IapContext.LocalOpenIapStore.current) { "OpenIapStore must be provided by IapContext.OpenIapProvider" } diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt index 8ddf7d989..ff50c56e5 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt @@ -19,7 +19,6 @@ import dev.hyo.martie.IapConstants import dev.hyo.martie.models.AppColors import dev.hyo.martie.screens.uis.* import dev.hyo.openiap.IapkitPurchaseState -import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first @@ -53,11 +52,10 @@ enum class VerificationMethod(val displayName: String) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun PurchaseFlowScreen( - navController: NavController, - storeParam: OpenIapStore? = null + navController: NavController ) { val uiScope = rememberCoroutineScope() - val iapStore = currentOpenIapStore(storeParam) + val iapStore = currentOpenIapStore() val products by iapStore.products.collectAsState() val purchases by iapStore.availablePurchases.collectAsState() val androidProducts = remember(products) { products.filterIsInstance() } diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt index 2b53eef2c..7609ca858 100644 --- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt +++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt @@ -29,7 +29,6 @@ import dev.hyo.openiap.ProductType import dev.hyo.openiap.ProductSubscriptionAndroid import dev.hyo.openiap.PurchaseAndroid import dev.hyo.openiap.PurchaseState -import dev.hyo.openiap.store.OpenIapStore import dev.hyo.openiap.store.PurchaseResultStatus import dev.hyo.openiap.OpenIapError import dev.hyo.openiap.ProductRequest @@ -84,8 +83,7 @@ private fun formatRemaining(deltaMillis: Long): String { @OptIn(ExperimentalMaterial3Api::class) @Composable fun SubscriptionFlowScreen( - navController: NavController, - storeParam: OpenIapStore? = null + navController: NavController ) { val context = LocalContext.current val uiScope = rememberCoroutineScope() @@ -93,7 +91,7 @@ fun SubscriptionFlowScreen( // SharedPreferences to track current offer (necessary since Google doesn't provide offer info) val prefs = remember { context.getSharedPreferences(SUBSCRIPTION_PREFS_NAME, Context.MODE_PRIVATE) } - val iapStore = currentOpenIapStore(storeParam) + val iapStore = currentOpenIapStore() val products by iapStore.products.collectAsState() val subscriptions by iapStore.subscriptions.collectAsState() val purchases by iapStore.availablePurchases.collectAsState() diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt index c3b5b14f7..4057d97b9 100644 --- a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt +++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt @@ -278,6 +278,8 @@ class OpenIapStore(private val module: OpenIapProtocol) { * Convenience overload — calls the config-accepting variant with `null`. * * @return `true` once the Play Billing client is connected. + * @throws OpenIapError.MissingCurrentActivity when Horizon has no Activity supplied by + * the constructor, [setActivity], or [IapContext.OpenIapProvider]. * @throws OpenIapError.InitConnection when the billing client fails to initialize * (e.g. Play Store missing, version too old). * diff --git a/packages/gql/codegen/plugins/csharp.ts b/packages/gql/codegen/plugins/csharp.ts index 20fa58186..16636416a 100644 --- a/packages/gql/codegen/plugins/csharp.ts +++ b/packages/gql/codegen/plugins/csharp.ts @@ -263,9 +263,15 @@ export class CSharpPlugin extends CodegenPlugin { private emitDoc(description: string | undefined, indent: string = ''): void { if (!description) return; const lines = description.split(/\r?\n/); + if (lines.length === 1) { + this.emit(`${indent}/// ${escapeXml(lines[0])}`); + return; + } + this.emit(`${indent}/// `); for (const line of lines) { - this.emit(`${indent}/// ${escapeXml(line)}`); + this.emit(line ? `${indent}/// ${escapeXml(line)}` : `${indent}///`); } + this.emit(`${indent}/// `); } // ============================================================================ diff --git a/packages/gql/src/codegen-defaults.test.ts b/packages/gql/src/codegen-defaults.test.ts index b87b83fce..9ea4bc53b 100644 --- a/packages/gql/src/codegen-defaults.test.ts +++ b/packages/gql/src/codegen-defaults.test.ts @@ -53,6 +53,18 @@ function objectSchema(fields: IRField[], enums: IREnum[]): IRSchema { } describe('codegen defaults', () => { + it('wraps multiline C# documentation in one XML summary element', () => { + const documentedField = field('value', stringType); + documentedField.description = 'First line.\nSecond .'; + + const output = new CSharpPlugin({ outputPath: 'Types.cs' }).generate(schema([documentedField])); + + expect(output).toContain( + [' /// ', ' /// First line.', ' /// Second <line>.', ' /// '].join('\n'), + ); + expect(output).not.toContain('\n /// '); + }); + it('keeps unsupported non-null C# defaults required and escapes string literals', () => { const output = new CSharpPlugin({ outputPath: 'Types.cs' }).generate( schema([field('unsupportedDefault', stringType, { raw: 'unsupported' }), field('escapedString', stringType, 'quote " and slash \\')]), diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index eff98837c..904799465 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -17,8 +17,10 @@ namespace OpenIap; // Enums // ============================================================================ -/// Play Billing choice image layout (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Play Billing choice image layout (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(BillingChoiceImageLayoutAndroidJsonConverter))] public enum BillingChoiceImageLayoutAndroid { @@ -72,8 +74,10 @@ public static class BillingChoiceImageLayoutAndroidExtensions public static BillingChoiceImageLayoutAndroid FromJson(string value) => BillingChoiceImageLayoutAndroidJsonConverter.FromRawString(value); } -/// Choice screen renderer for Billing Choice availability (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Choice screen renderer for Billing Choice availability (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(BillingChoiceScreenTypeAndroidJsonConverter))] public enum BillingChoiceScreenTypeAndroid { @@ -127,36 +131,48 @@ public static class BillingChoiceScreenTypeAndroidExtensions public static BillingChoiceScreenTypeAndroid FromJson(string value) => BillingChoiceScreenTypeAndroidJsonConverter.FromRawString(value); } -/// Billing program types for Google Play Billing Programs (Android) -/// Available in Google Play Billing Library 8.2.0 (External Offer and External Content Link -/// integrations require 8.2.1+), EXTERNAL_PAYMENTS added in 8.3.0, -/// BILLING_CHOICE added in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (requires Play Billing 9.1.0+). +/// +/// Billing program types for Google Play Billing Programs (Android) +/// Available in Google Play Billing Library 8.2.0 (External Offer and External Content Link +/// integrations require 8.2.1+), EXTERNAL_PAYMENTS added in 8.3.0, +/// BILLING_CHOICE added in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(BillingProgramAndroidJsonConverter))] public enum BillingProgramAndroid { /// Unspecified billing program. Do not use. Unspecified, - /// User Choice Billing program. - /// User can select between Google Play Billing or alternative billing. - /// Available in Google Play Billing Library 7.0+ + /// + /// User Choice Billing program. + /// User can select between Google Play Billing or alternative billing. + /// Available in Google Play Billing Library 7.0+ + /// UserChoiceBilling, - /// External Content Links program. - /// Allows linking to external content outside the app. - /// Available in Google Play Billing Library 8.2.0+ + /// + /// External Content Links program. + /// Allows linking to external content outside the app. + /// Available in Google Play Billing Library 8.2.0+ + /// ExternalContentLink, - /// External Offers program. - /// Allows offering digital content purchases outside the app. - /// Available in Google Play Billing Library 8.2.0+ + /// + /// External Offers program. + /// Allows offering digital content purchases outside the app. + /// Available in Google Play Billing Library 8.2.0+ + /// ExternalOffer, - /// External Payments program (Japan only). - /// Allows presenting a side-by-side choice between Google Play Billing and developer's external payment option. - /// Users can choose to complete the purchase on the developer's website. - /// Available in Google Play Billing Library 8.3.0+ + /// + /// External Payments program (Japan only). + /// Allows presenting a side-by-side choice between Google Play Billing and developer's external payment option. + /// Users can choose to complete the purchase on the developer's website. + /// Available in Google Play Billing Library 8.3.0+ + /// ExternalPayments, - /// Billing Choice program. - /// Allows presenting Google Play Billing alongside an alternative in-app billing system or external web link. - /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Billing Choice program. + /// Allows presenting Google Play Billing alongside an alternative in-app billing system or external web link. + /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// BillingChoice } @@ -211,19 +227,25 @@ public static class BillingProgramAndroidExtensions public static BillingProgramAndroid FromJson(string value) => BillingProgramAndroidJsonConverter.FromRawString(value); } -/// Launch mode for developer billing option (Android) -/// Determines how the external payment URL is launched -/// Available in Google Play Billing Library 8.3.0+ +/// +/// Launch mode for developer billing option (Android) +/// Determines how the external payment URL is launched +/// Available in Google Play Billing Library 8.3.0+ +/// [JsonConverter(typeof(DeveloperBillingLaunchModeAndroidJsonConverter))] public enum DeveloperBillingLaunchModeAndroid { /// Unspecified launch mode. Do not use. Unspecified, - /// Google Play will launch the link in an external browser or eligible app. - /// Use this when you want Play to handle launching the external payment URL. + /// + /// Google Play will launch the link in an external browser or eligible app. + /// Use this when you want Play to handle launching the external payment URL. + /// LaunchInExternalBrowserOrApp, - /// The caller app will launch the link after Play returns control. - /// Use this when you want to handle launching the external payment URL yourself. + /// + /// The caller app will launch the link after Play returns control. + /// Use this when you want to handle launching the external payment URL yourself. + /// CallerWillLaunchLink } @@ -269,8 +291,10 @@ public static class DeveloperBillingLaunchModeAndroidExtensions public static DeveloperBillingLaunchModeAndroid FromJson(string value) => DeveloperBillingLaunchModeAndroidJsonConverter.FromRawString(value); } -/// Developer-provided billing destination type for Billing Program reporting details (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Developer-provided billing destination type for Billing Program reporting details (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// [JsonConverter(typeof(DeveloperBillingTypeAndroidJsonConverter))] public enum DeveloperBillingTypeAndroid { @@ -324,8 +348,10 @@ public static class DeveloperBillingTypeAndroidExtensions public static DeveloperBillingTypeAndroid FromJson(string value) => DeveloperBillingTypeAndroidJsonConverter.FromRawString(value); } -/// Discount offer type enumeration. -/// Categorizes the type of discount or promotional offer. +/// +/// Discount offer type enumeration. +/// Categorizes the type of discount or promotional offer. +/// [JsonConverter(typeof(DiscountOfferTypeJsonConverter))] public enum DiscountOfferType { @@ -600,10 +626,12 @@ public static class ErrorCodeExtensions public static ErrorCode FromJson(string value) => ErrorCodeJsonConverter.FromRawString(value); } -/// Launch mode for external link flow (Android) -/// Determines how the external URL is launched -/// Introduced in Google Play Billing Library 8.2.0. External Offer and External Content Link -/// integrations require 8.2.1+ and fresh details immediately before every redirect session. +/// +/// Launch mode for external link flow (Android) +/// Determines how the external URL is launched +/// Introduced in Google Play Billing Library 8.2.0. External Offer and External Content Link +/// integrations require 8.2.1+ and fresh details immediately before every redirect session. +/// [JsonConverter(typeof(ExternalLinkLaunchModeAndroidJsonConverter))] public enum ExternalLinkLaunchModeAndroid { @@ -657,9 +685,11 @@ public static class ExternalLinkLaunchModeAndroidExtensions public static ExternalLinkLaunchModeAndroid FromJson(string value) => ExternalLinkLaunchModeAndroidJsonConverter.FromRawString(value); } -/// Link type for external link flow (Android) -/// Specifies the type of external link destination -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Link type for external link flow (Android) +/// Specifies the type of external link destination +/// Available in Google Play Billing Library 8.2.0+ +/// [JsonConverter(typeof(ExternalLinkTypeAndroidJsonConverter))] public enum ExternalLinkTypeAndroid { @@ -713,14 +743,18 @@ public static class ExternalLinkTypeAndroidExtensions public static ExternalLinkTypeAndroid FromJson(string value) => ExternalLinkTypeAndroidJsonConverter.FromRawString(value); } -/// Notice types for ExternalPurchaseCustomLink (iOS 18.1+). -/// Determines the style of disclosure notice to display. -/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/noticetype +/// +/// Notice types for ExternalPurchaseCustomLink (iOS 18.1+). +/// Determines the style of disclosure notice to display. +/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/noticetype +/// [JsonConverter(typeof(ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter))] public enum ExternalPurchaseCustomLinkNoticeTypeIOS { - /// Notice type indicating external purchases will be displayed in a browser - /// or destination of the app's choice. + /// + /// Notice type indicating external purchases will be displayed in a browser + /// or destination of the app's choice. + /// Browser } @@ -761,17 +795,23 @@ public static class ExternalPurchaseCustomLinkNoticeTypeIOSExtensions public static ExternalPurchaseCustomLinkNoticeTypeIOS FromJson(string value) => ExternalPurchaseCustomLinkNoticeTypeIOSJsonConverter.FromRawString(value); } -/// Token types for ExternalPurchaseCustomLink (iOS 18.1+). -/// Used to request different types of external purchase tokens for reporting to Apple. -/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) +/// +/// Token types for ExternalPurchaseCustomLink (iOS 18.1+). +/// Used to request different types of external purchase tokens for reporting to Apple. +/// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) +/// [JsonConverter(typeof(ExternalPurchaseCustomLinkTokenTypeIOSJsonConverter))] public enum ExternalPurchaseCustomLinkTokenTypeIOS { - /// Token for customer acquisition tracking. - /// Use this when a new customer makes their first purchase through external link. + /// + /// Token for customer acquisition tracking. + /// Use this when a new customer makes their first purchase through external link. + /// Acquisition, - /// Token for ongoing services tracking. - /// Use this for existing customers making additional purchases. + /// + /// Token for ongoing services tracking. + /// Use this for existing customers making additional purchases. + /// Services } @@ -874,16 +914,20 @@ public enum IapEvent PurchaseError, PromotedProductIOS, UserChoiceBillingAndroid, - /// Fired for External Payments (8.3.0+) and Google-rendered Billing Choice - /// developer billing selections on Android. Billing Choice is available in - /// OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Fired for External Payments (8.3.0+) and Google-rendered Billing Choice + /// developer billing selections on Android. Billing Choice is available in + /// OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// DeveloperProvidedBillingAndroid, - /// Fired when a subscription enters a billing-issue state that requires user attention. - /// A StoreKit billing-retry subscription may no longer be a current entitlement. - /// Cross-platform unification of StoreKit 2 Message.billingIssue (iOS 16.4+, - /// Mac Catalyst 16.4+, visionOS 1.0+) and - /// Play Billing 8.1+ isSuspended. NOT emitted by Amazon Appstore or the Horizon - /// flavor, whose Billing Compatibility SDK implements only Play Billing 7.0. + /// + /// Fired when a subscription enters a billing-issue state that requires user attention. + /// A StoreKit billing-retry subscription may no longer be a current entitlement. + /// Cross-platform unification of StoreKit 2 Message.billingIssue (iOS 16.4+, + /// Mac Catalyst 16.4+, visionOS 1.0+) and + /// Play Billing 8.1+ isSuspended. NOT emitted by Amazon Appstore or the Horizon + /// flavor, whose Billing Compatibility SDK implements only Play Billing 7.0. + /// SubscriptionBillingIssue } @@ -1192,9 +1236,11 @@ public static class IapStoreExtensions public static IapStore FromJson(string value) => IapStoreJsonConverter.FromRawString(value); } -/// High-level in-app message category (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// High-level in-app message category (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// [JsonConverter(typeof(InAppMessageCategoryAndroidJsonConverter))] public enum InAppMessageCategoryAndroid { @@ -1243,9 +1289,11 @@ public static class InAppMessageCategoryAndroidExtensions public static InAppMessageCategoryAndroid FromJson(string value) => InAppMessageCategoryAndroidJsonConverter.FromRawString(value); } -/// Response code from Play billing in-app messages (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// Response code from Play billing in-app messages (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// [JsonConverter(typeof(InAppMessageResponseCodeAndroidJsonConverter))] public enum InAppMessageResponseCodeAndroid { @@ -1294,8 +1342,10 @@ public static class InAppMessageResponseCodeAndroidExtensions public static InAppMessageResponseCodeAndroid FromJson(string value) => InAppMessageResponseCodeAndroidJsonConverter.FromRawString(value); } -/// Payment mode for subscription offers. -/// Determines how the user pays during the offer period. +/// +/// Payment mode for subscription offers. +/// Determines how the user pays during the offer period. +/// [JsonConverter(typeof(PaymentModeJsonConverter))] public enum PaymentMode { @@ -1469,10 +1519,12 @@ public static class ProductQueryTypeExtensions public static ProductQueryType FromJson(string value) => ProductQueryTypeJsonConverter.FromRawString(value); } -/// Status code for individual products returned from queryProductDetailsAsync (Android) -/// Prior to 8.0, products that couldn't be fetched were simply not returned. -/// With 8.0+, these products are returned with a status code explaining why. -/// Available in Google Play Billing Library 8.0.0+ +/// +/// Status code for individual products returned from queryProductDetailsAsync (Android) +/// Prior to 8.0, products that couldn't be fetched were simply not returned. +/// With 8.0+, these products are returned with a status code explaining why. +/// Available in Google Play Billing Library 8.0.0+ +/// [JsonConverter(typeof(ProductStatusAndroidJsonConverter))] public enum ProductStatusAndroid { @@ -1745,8 +1797,10 @@ public static class PurchaseVerificationProviderExtensions public static PurchaseVerificationProvider FromJson(string value) => PurchaseVerificationProviderJsonConverter.FromRawString(value); } -/// Sub-response codes for more granular purchase error information (Android) -/// Available in Google Play Billing Library 8.0.0+ +/// +/// Sub-response codes for more granular purchase error information (Android) +/// Available in Google Play Billing Library 8.0.0+ +/// [JsonConverter(typeof(SubResponseCodeAndroidJsonConverter))] public enum SubResponseCodeAndroid { @@ -1861,8 +1915,10 @@ public enum SubscriptionOfferTypeIOS { Introductory, Promotional, - /// Win-back offer type (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. + /// + /// Win-back offer type (iOS 18+) + /// Used to re-engage churned subscribers with a discount or free trial. + /// WinBack } @@ -2038,9 +2094,11 @@ public static class SubscriptionPeriodUnitExtensions public static SubscriptionPeriodUnit FromJson(string value) => SubscriptionPeriodUnitJsonConverter.FromRawString(value); } -/// Replacement mode for subscription changes (Android) -/// These modes determine how the subscription replacement affects billing. -/// Available in Google Play Billing Library 8.1.0+ +/// +/// Replacement mode for subscription changes (Android) +/// These modes determine how the subscription replacement affects billing. +/// Available in Google Play Billing Library 8.1.0+ +/// [JsonConverter(typeof(SubscriptionReplacementModeAndroidJsonConverter))] public enum SubscriptionReplacementModeAndroid { @@ -2134,10 +2192,12 @@ public interface ProductCommon public interface PurchaseCommon { - /// The current plan identifier. This is: - /// - On Android: the basePlanId (e.g., "premium", "premium-year") - /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") - /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// + /// The current plan identifier. This is: + /// - On Android: the basePlanId (e.g., "premium", "premium-year") + /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") + /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// string? CurrentPlanId { get; } string Id { get; } IReadOnlyList? Ids { get; } @@ -2195,10 +2255,12 @@ public sealed record ActiveSubscription public bool? AutoRenewingAndroid { get; init; } [JsonPropertyName("basePlanIdAndroid")] public string? BasePlanIdAndroid { get; init; } - /// The current plan identifier. This is: - /// - On Android: the basePlanId (e.g., "premium", "premium-year") - /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") - /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// + /// The current plan identifier. This is: + /// - On Android: the basePlanId (e.g., "premium", "premium-year") + /// - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly") + /// This provides a unified way to identify which specific plan/tier the user is subscribed to. + /// [JsonPropertyName("currentPlanId")] public string? CurrentPlanId { get; init; } [JsonPropertyName("daysUntilExpirationIOS")] @@ -2216,8 +2278,10 @@ public sealed record ActiveSubscription /// Required for subscription upgrade/downgrade on Android [JsonPropertyName("purchaseTokenAndroid")] public string? PurchaseTokenAndroid { get; init; } - /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, - /// pending upgrades/downgrades, and auto-renewal preferences. + /// + /// Renewal information from StoreKit 2 (iOS only). Contains details about subscription renewal status, + /// pending upgrades/downgrades, and auto-renewal preferences. + /// [JsonPropertyName("renewalInfoIOS")] public RenewalInfoIOS? RenewalInfoIOS { get; init; } /// Unix timestamp in milliseconds since January 1, 1970 UTC. @@ -2227,10 +2291,12 @@ public sealed record ActiveSubscription public required string TransactionId { get; init; } } -/// Advanced Commerce metadata from a transaction (iOS 18.4+). -/// Contains item details, tax information, and refund data for purchases -/// made through the Advanced Commerce API using generic SKUs. -/// Only present for transactions that use the Advanced Commerce API. +/// +/// Advanced Commerce metadata from a transaction (iOS 18.4+). +/// Contains item details, tax information, and refund data for purchases +/// made through the Advanced Commerce API using generic SKUs. +/// Only present for transactions that use the Advanced Commerce API. +/// public sealed record AdvancedCommerceInfoIOS { /// Optional description @@ -2245,10 +2311,12 @@ public sealed record AdvancedCommerceInfoIOS /// The items purchased as part of this transaction [JsonPropertyName("items")] public required IReadOnlyList Items { get; init; } - /// Subscription period for this transaction. - /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 - /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, - /// or visionOS 2.4+). + /// + /// Subscription period for this transaction. + /// Available in OpenIAP Spec 3.1.0 / openiap-apple 3.1.0 + /// (requires iOS 18.4+, macOS 15.4+, tvOS 18.4+, watchOS 11.4+, + /// or visionOS 2.4+). + /// [JsonPropertyName("period")] public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking @@ -2273,8 +2341,10 @@ public sealed record AdvancedCommerceItemDetailsIOS public string? JsonRepresentation { get; init; } } -/// An item purchased through the Advanced Commerce API (iOS 18.4+). -/// Represents a developer-defined product within a generic SKU transaction. +/// +/// An item purchased through the Advanced Commerce API (iOS 18.4+). +/// Represents a developer-defined product within a generic SKU transaction. +/// public sealed record AdvancedCommerceItemIOS { /// The item's detail information @@ -2316,28 +2386,36 @@ public sealed record AppTransaction public required string Environment { get; init; } [JsonPropertyName("originalAppVersion")] public required string OriginalAppVersion { get; init; } - /// Original App Store platform raw value. Xcode 27 adds the back-deployed managed - /// acquisition-platform value. + /// + /// Original App Store platform raw value. Xcode 27 adds the back-deployed managed + /// acquisition-platform value. + /// [JsonPropertyName("originalPlatform")] public string? OriginalPlatform { get; init; } [JsonPropertyName("originalPurchaseDate")] public required double OriginalPurchaseDate { get; init; } [JsonPropertyName("preorderDate")] public double? PreorderDate { get; init; } - /// Date the app-acquisition transaction was revoked (epoch milliseconds). - /// Available through the Xcode 27 SDK and back-deployed to Apple 16+. + /// + /// Date the app-acquisition transaction was revoked (epoch milliseconds). + /// Available through the Xcode 27 SDK and back-deployed to Apple 16+. + /// [JsonPropertyName("revocationDate")] public double? RevocationDate { get; init; } [JsonPropertyName("signedDate")] public required double SignedDate { get; init; } - /// Store channel of the original app purchase: consumer, education, enterprise, - /// or another future StoreKit value (Apple 27+ beta). + /// + /// Store channel of the original app purchase: consumer, education, enterprise, + /// or another future StoreKit value (Apple 27+ beta). + /// [JsonPropertyName("storeType")] public string? StoreType { get; init; } } -/// Display information for developer-rendered Billing Choice screens (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Display information for developer-rendered Billing Choice screens (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// public sealed record BillingChoiceInfoAndroid { /// URL for the Play Billing choice image matching the requested layout. @@ -2348,44 +2426,56 @@ public sealed record BillingChoiceInfoAndroid public string? PlayBillingLoyaltyInfo { get; init; } } -/// Result of checking billing program availability (Android) -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Result of checking billing program availability (Android) +/// Available in Google Play Billing Library 8.2.0+ +/// public sealed record BillingProgramAvailabilityResultAndroid { /// The billing program that was checked [JsonPropertyName("billingProgram")] public required BillingProgramAndroid BillingProgram { get; init; } - /// Billing Choice screen renderer. Populated only for available BILLING_CHOICE results. - /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// + /// Billing Choice screen renderer. Populated only for available BILLING_CHOICE results. + /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// [JsonPropertyName("choiceScreenType")] public BillingChoiceScreenTypeAndroid? ChoiceScreenType { get; init; } /// Whether the billing program is available for the user [JsonPropertyName("isAvailable")] public required bool IsAvailable { get; init; } - /// Whether external-link payment is available for Billing Choice. - /// Populated only for available BILLING_CHOICE results. - /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// + /// Whether external-link payment is available for Billing Choice. + /// Populated only for available BILLING_CHOICE results. + /// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0. + /// [JsonPropertyName("isExternalLinkAvailable")] public bool? IsExternalLinkAvailable { get; init; } } -/// Reporting details for transactions made outside of Google Play Billing (Android) -/// Contains the external transaction token needed for reporting -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Reporting details for transactions made outside of Google Play Billing (Android) +/// Contains the external transaction token needed for reporting +/// Available in Google Play Billing Library 8.2.0+ +/// public sealed record BillingProgramReportingDetailsAndroid { /// The billing program that the reporting details are associated with [JsonPropertyName("billingProgram")] public required BillingProgramAndroid BillingProgram { get; init; } - /// External transaction token used to report transactions made outside of Google Play Billing. - /// Do not cache it for a later redirect session. For External Offer, the same token may report - /// multiple purchases made during the session that generated it. + /// + /// External transaction token used to report transactions made outside of Google Play Billing. + /// Do not cache it for a later redirect session. For External Offer, the same token may report + /// multiple purchases made during the session that generated it. + /// [JsonPropertyName("externalTransactionToken")] public required string ExternalTransactionToken { get; init; } } -/// Extended billing result with sub-response code (Android) -/// Available in Google Play Billing Library 8.0.0+ +/// +/// Extended billing result with sub-response code (Android) +/// Available in Google Play Billing Library 8.0.0+ +/// public sealed record BillingResultAndroid { /// Debug message from the billing library @@ -2394,14 +2484,18 @@ public sealed record BillingResultAndroid /// The response code from the billing operation [JsonPropertyName("responseCode")] public required int ResponseCode { get; init; } - /// Sub-response code for more granular error information (8.0+). - /// Provides additional context when responseCode indicates an error. + /// + /// Sub-response code for more granular error information (8.0+). + /// Provides additional context when responseCode indicates an error. + /// [JsonPropertyName("subResponseCode")] public SubResponseCodeAndroid? SubResponseCode { get; init; } } -/// Metadata for one auto-renewable subscription included in an Apple -/// subscription bundle (Apple 27+ beta). +/// +/// Metadata for one auto-renewable subscription included in an Apple +/// subscription bundle (Apple 27+ beta). +/// public sealed record BundledSubscriptionIOS { [JsonPropertyName("description")] @@ -2424,21 +2518,29 @@ public sealed record BundledSubscriptionIOS public required int SubscriptionGroupLevel { get; init; } } -/// Details provided when user selects developer billing option (Android) -/// Received via DeveloperProvidedBillingListener callback -/// Available in Google Play Billing Library 8.3.0+ +/// +/// Details provided when user selects developer billing option (Android) +/// Received via DeveloperProvidedBillingListener callback +/// Available in Google Play Billing Library 8.3.0+ +/// public sealed record DeveloperProvidedBillingDetailsAndroid { - /// External transaction token used to report transactions made through developer billing. - /// Nullable for flows such as external payments where no token is returned. + /// + /// External transaction token used to report transactions made through developer billing. + /// Nullable for flows such as external payments where no token is returned. + /// [JsonPropertyName("externalTransactionToken")] public string? ExternalTransactionToken { get; init; } - /// URI to launch for an external-link Billing Choice flow, when provided by - /// Google Play. + /// + /// URI to launch for an external-link Billing Choice flow, when provided by + /// Google Play. + /// [JsonPropertyName("linkUri")] public string? LinkUri { get; init; } - /// Original external transaction ID when replacing a subscription that was - /// purchased through developer billing. + /// + /// Original external transaction ID when replacing a subscription that was + /// purchased through developer billing. + /// [JsonPropertyName("originalExternalTransactionId")] public string? OriginalExternalTransactionId { get; init; } /// Products selected for the developer billing flow. @@ -2460,8 +2562,10 @@ public sealed record DeveloperProvidedBillingProductAndroid public required ProductType Type { get; init; } } -/// Discount amount details for one-time purchase offers (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Discount amount details for one-time purchase offers (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record DiscountAmountAndroid { /// Discount amount in micro-units (1,000,000 = 1 unit of currency) @@ -2472,35 +2576,45 @@ public sealed record DiscountAmountAndroid public required string FormattedDiscountAmount { get; init; } } -/// Discount display information for one-time purchase offers (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Discount display information for one-time purchase offers (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record DiscountDisplayInfoAndroid { - /// Absolute discount amount details - /// Only returned for fixed amount discounts + /// + /// Absolute discount amount details + /// Only returned for fixed amount discounts + /// [JsonPropertyName("discountAmount")] public DiscountAmountAndroid? DiscountAmount { get; init; } - /// Percentage discount (e.g., 33 for 33% off) - /// Only returned for percentage-based discounts + /// + /// Percentage discount (e.g., 33 for 33% off) + /// Only returned for percentage-based discounts + /// [JsonPropertyName("percentageDiscount")] public int? PercentageDiscount { get; init; } } -/// Standardized one-time product discount offer. -/// Provides a platform-neutral OpenIAP shape for Google Play one-time product -/// purchase options and offers. -/// -/// Currently populated only on Android (Google Play Billing 8.0+). -/// iOS does not populate this type. -/// -/// @see https://openiap.dev/docs/types/discount-offer +/// +/// Standardized one-time product discount offer. +/// Provides a platform-neutral OpenIAP shape for Google Play one-time product +/// purchase options and offers. +/// +/// Currently populated only on Android (Google Play Billing 8.0+). +/// iOS does not populate this type. +/// +/// @see https://openiap.dev/docs/types/discount-offer +/// public sealed record DiscountOffer { /// Currency code (ISO 4217, e.g., "USD") [JsonPropertyName("currency")] public required string Currency { get; init; } - /// [Android] Fixed discount amount in micro-units. - /// Only present for fixed amount discounts. + /// + /// [Android] Fixed discount amount in micro-units. + /// Only present for fixed amount discounts. + /// [JsonPropertyName("discountAmountMicrosAndroid")] public string? DiscountAmountMicrosAndroid { get; init; } /// Formatted display price string (e.g., "$4.99") @@ -2509,53 +2623,71 @@ public sealed record DiscountOffer /// [Android] Formatted discount amount including its currency sign (e.g., "$5.00"). [JsonPropertyName("formattedDiscountAmountAndroid")] public string? FormattedDiscountAmountAndroid { get; init; } - /// [Android] Original full price in micro-units before discount. - /// Divide by 1,000,000 to get the actual price. - /// Use for displaying strikethrough original price. + /// + /// [Android] Original full price in micro-units before discount. + /// Divide by 1,000,000 to get the actual price. + /// Use for displaying strikethrough original price. + /// [JsonPropertyName("fullPriceMicrosAndroid")] public string? FullPriceMicrosAndroid { get; init; } - /// Unique identifier for the offer. - /// - iOS: Not applicable (one-time discounts not supported) - /// - Android: offerId from the Google Play one-time purchase option + /// + /// Unique identifier for the offer. + /// - iOS: Not applicable (one-time discounts not supported) + /// - Android: offerId from the Google Play one-time purchase option + /// [JsonPropertyName("id")] public string? Id { get; init; } - /// [Android] Limited quantity information. - /// Contains maximumQuantity and remainingQuantity. + /// + /// [Android] Limited quantity information. + /// Contains maximumQuantity and remainingQuantity. + /// [JsonPropertyName("limitedQuantityInfoAndroid")] public LimitedQuantityInfoAndroid? LimitedQuantityInfoAndroid { get; init; } /// [Android] List of tags associated with this offer. [JsonPropertyName("offerTagsAndroid")] public IReadOnlyList? OfferTagsAndroid { get; init; } - /// [Android] Offer token required for purchase. - /// Must be passed to requestPurchase() when purchasing with this offer. + /// + /// [Android] Offer token required for purchase. + /// Must be passed to requestPurchase() when purchasing with this offer. + /// [JsonPropertyName("offerTokenAndroid")] public string? OfferTokenAndroid { get; init; } - /// [Android] Percentage discount (e.g., 33 for 33% off). - /// Only present for percentage-based discounts. + /// + /// [Android] Percentage discount (e.g., 33 for 33% off). + /// Only present for percentage-based discounts. + /// [JsonPropertyName("percentageDiscountAndroid")] public int? PercentageDiscountAndroid { get; init; } - /// [Android] Pre-order details if this is a pre-order offer. - /// Available in Google Play Billing Library 8.1.0+ + /// + /// [Android] Pre-order details if this is a pre-order offer. + /// Available in Google Play Billing Library 8.1.0+ + /// [JsonPropertyName("preorderDetailsAndroid")] public PreorderDetailsAndroid? PreorderDetailsAndroid { get; init; } /// Numeric price value [JsonPropertyName("price")] public required double Price { get; init; } - /// [Android] Purchase option ID for this offer. - /// Used to identify which purchase option the user selected. - /// Available in Google Play Billing Library 8.0+ + /// + /// [Android] Purchase option ID for this offer. + /// Used to identify which purchase option the user selected. + /// Available in Google Play Billing Library 8.0+ + /// [JsonPropertyName("purchaseOptionIdAndroid")] public string? PurchaseOptionIdAndroid { get; init; } /// [Android] Rental details if this is a rental offer. [JsonPropertyName("rentalDetailsAndroid")] public RentalDetailsAndroid? RentalDetailsAndroid { get; init; } - /// Offer category. DiscountOffer currently represents Android one-time product - /// offers and is populated as OneTime. Introductory and Promotional are used by - /// SubscriptionOffer. + /// + /// Offer category. DiscountOffer currently represents Android one-time product + /// offers and is populated as OneTime. Introductory and Promotional are used by + /// SubscriptionOffer. + /// [JsonPropertyName("type")] public required DiscountOfferType Type { get; init; } - /// [Android] Valid time window for the offer. - /// Contains startTimeMillis and endTimeMillis. + /// + /// [Android] Valid time window for the offer. + /// Contains startTimeMillis and endTimeMillis. + /// [JsonPropertyName("validTimeWindowAndroid")] public ValidTimeWindowAndroid? ValidTimeWindowAndroid { get; init; } } @@ -2587,8 +2719,10 @@ public sealed record ExternalPurchaseCustomLinkTokenResultIOS /// Optional error message if token retrieval failed [JsonPropertyName("error")] public string? Error { get; init; } - /// The external purchase token string. - /// Report this token to Apple's External Purchase Server API. + /// + /// The external purchase token string. + /// Report this token to Apple's External Purchase Server API. + /// [JsonPropertyName("token")] public string? Token { get; init; } } @@ -2604,16 +2738,20 @@ public sealed record ExternalPurchaseLinkResultIOS public required bool Success { get; init; } } -/// Result of presenting external purchase notice sheet (iOS 17.4+) -/// Returns the token when user continues to external purchase. +/// +/// Result of presenting external purchase notice sheet (iOS 17.4+) +/// Returns the token when user continues to external purchase. +/// public sealed record ExternalPurchaseNoticeResultIOS { /// Optional error message if the presentation failed [JsonPropertyName("error")] public string? Error { get; init; } - /// External purchase token returned when user continues (iOS 17.4+). - /// This token should be reported to Apple's External Purchase Server API. - /// Only present when result is Continue. + /// + /// External purchase token returned when user continues (iOS 17.4+). + /// This token should be reported to Apple's External Purchase Server API. + /// Only present when result is Continue. + /// [JsonPropertyName("externalPurchaseToken")] public string? ExternalPurchaseToken { get; init; } /// Notice result indicating user action @@ -2629,8 +2767,10 @@ public sealed record FetchProductsResultProducts(IReadOnlyList? Value) public sealed record FetchProductsResultSubscriptions(IReadOnlyList? Value) : FetchProductsResult; -/// Public app-facing data attached to one store product in IAPKit. -/// Never place credentials, signing keys, or server-authoritative rules here. +/// +/// Public app-facing data attached to one store product in IAPKit. +/// Never place credentials, signing keys, or server-authoritative rules here. +/// public sealed record IapkitProductClientPayload { [JsonPropertyName("body")] @@ -2643,9 +2783,11 @@ public sealed record IapkitProductClientPayload public required double Version { get; init; } } -/// Result from showing Play billing in-app messages (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// Result from showing Play billing in-app messages (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// public sealed record InAppMessageResultAndroid { /// Purchase token returned when a subscription status changed. @@ -2656,26 +2798,34 @@ public sealed record InAppMessageResultAndroid public required InAppMessageResponseCodeAndroid ResponseCode { get; init; } } -/// Installment plan details for subscription offers (Android) -/// Contains information about the installment plan commitment. -/// Available in Google Play Billing Library 7.0+ +/// +/// Installment plan details for subscription offers (Android) +/// Contains information about the installment plan commitment. +/// Available in Google Play Billing Library 7.0+ +/// public sealed record 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. + /// + /// 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. + /// [JsonPropertyName("commitmentPaymentsCount")] public required int CommitmentPaymentsCount { get; init; } - /// 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). + /// + /// 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). + /// [JsonPropertyName("subsequentCommitmentPaymentsCount")] public required int SubsequentCommitmentPaymentsCount { get; init; } } -/// Limited quantity information for one-time purchase offers (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Limited quantity information for one-time purchase offers (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record LimitedQuantityInfoAndroid { /// Maximum quantity a user can purchase @@ -2686,33 +2836,45 @@ public sealed record LimitedQuantityInfoAndroid public required int RemainingQuantity { get; init; } } -/// 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+ +/// +/// 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 sealed record PendingPurchaseUpdateAndroid { - /// Product IDs for the pending purchase update. - /// These are the new products the user is switching to. + /// + /// Product IDs for the pending purchase update. + /// These are the new products the user is switching to. + /// [JsonPropertyName("products")] public required IReadOnlyList Products { get; init; } - /// Purchase token for the pending transaction. - /// Use this token to track or manage the pending purchase update. + /// + /// Purchase token for the pending transaction. + /// Use this token to track or manage the pending purchase update. + /// [JsonPropertyName("purchaseToken")] public required string PurchaseToken { get; init; } } -/// Pre-order details for one-time purchase products (Android) -/// Available in Google Play Billing Library 8.1.0+ +/// +/// Pre-order details for one-time purchase products (Android) +/// Available in Google Play Billing Library 8.1.0+ +/// public sealed record PreorderDetailsAndroid { - /// Pre-order presale end time in milliseconds since epoch. - /// This is when the presale period ends and the product will be released. + /// + /// Pre-order presale end time in milliseconds since epoch. + /// This is when the presale period ends and the product will be released. + /// [JsonPropertyName("preorderPresaleEndTimeMillis")] public required string PreorderPresaleEndTimeMillis { get; init; } - /// Pre-order release time in milliseconds since epoch. - /// This is when the product will be available to users who pre-ordered. + /// + /// Pre-order release time in milliseconds since epoch. + /// This is when the product will be available to users who pre-ordered. + /// [JsonPropertyName("preorderReleaseTimeMillis")] public required string PreorderReleaseTimeMillis { get; init; } } @@ -2747,9 +2909,11 @@ public sealed record ProductAndroid : Product, ProductCommon public string? DebugDescription { get; init; } [JsonPropertyName("description")] public required string Description { get; init; } - /// Standardized Android one-time product purchase options and offers. - /// Native metadata uses Android-suffixed fields. - /// @see https://openiap.dev/docs/types/discount-offer + /// + /// Standardized Android one-time product purchase options and offers. + /// Native metadata uses Android-suffixed fields. + /// @see https://openiap.dev/docs/types/discount-offer + /// [JsonPropertyName("discountOffers")] public IReadOnlyList? DiscountOffers { get; init; } [JsonPropertyName("displayName")] @@ -2764,16 +2928,20 @@ public sealed record ProductAndroid : Product, ProductCommon public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.Android; [JsonPropertyName("price")] public double? Price { get; init; } - /// Product-level status code indicating fetch result (Android 8.0+) - /// OK = product fetched successfully - /// NOT_FOUND = SKU doesn't exist - /// NO_OFFERS_AVAILABLE = user not eligible for any offers - /// Available in Google Play Billing Library 8.0.0+ + /// + /// Product-level status code indicating fetch result (Android 8.0+) + /// OK = product fetched successfully + /// NOT_FOUND = SKU doesn't exist + /// NO_OFFERS_AVAILABLE = user not eligible for any offers + /// Available in Google Play Billing Library 8.0.0+ + /// [JsonPropertyName("productStatusAndroid")] public ProductStatusAndroid? ProductStatusAndroid { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with Android-specific fields using suffix. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with Android-specific fields using suffix. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } [JsonPropertyName("title")] @@ -2806,14 +2974,18 @@ public sealed record ProductIOS : Product, ProductCommon public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.IOS; [JsonPropertyName("price")] public double? Price { get; init; } - /// iOS 26.4+ subscription pricing terms, including billing plan metadata for - /// monthly subscriptions with a 12-month commitment. + /// + /// iOS 26.4+ subscription pricing terms, including billing plan metadata for + /// monthly subscriptions with a 12-month commitment. + /// [JsonPropertyName("pricingTermsIOS")] public IReadOnlyList? PricingTermsIOS { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with iOS-specific fields using suffix. - /// Note: iOS does not support one-time product discounts. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with iOS-specific fields using suffix. + /// Note: iOS does not support one-time product discounts. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } [JsonPropertyName("title")] @@ -2844,16 +3016,20 @@ public sealed record ProductSubscriptionAndroid : ProductSubscription, ProductCo public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.Android; [JsonPropertyName("price")] public double? Price { get; init; } - /// Product-level status code indicating fetch result (Android 8.0+) - /// OK = product fetched successfully - /// NOT_FOUND = SKU doesn't exist - /// NO_OFFERS_AVAILABLE = user not eligible for any offers - /// Available in Google Play Billing Library 8.0.0+ + /// + /// Product-level status code indicating fetch result (Android 8.0+) + /// OK = product fetched successfully + /// NOT_FOUND = SKU doesn't exist + /// NO_OFFERS_AVAILABLE = user not eligible for any offers + /// Available in Google Play Billing Library 8.0.0+ + /// [JsonPropertyName("productStatusAndroid")] public ProductStatusAndroid? ProductStatusAndroid { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with Android-specific fields using suffix. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with Android-specific fields using suffix. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public required IReadOnlyList SubscriptionOffers { get; init; } [JsonPropertyName("title")] @@ -2864,8 +3040,10 @@ public sealed record ProductSubscriptionAndroid : ProductSubscription, ProductCo public sealed record ProductSubscriptionIOS : ProductSubscription, ProductCommon { - /// Subscriptions included in this Apple subscription bundle. Empty or null for - /// every other product type (Apple 27+ beta). + /// + /// Subscriptions included in this Apple subscription bundle. Empty or null for + /// every other product type (Apple 27+ beta). + /// [JsonPropertyName("bundledSubscriptionsIOS")] public IReadOnlyList? BundledSubscriptionsIOS { get; init; } [JsonPropertyName("currency")] @@ -2900,16 +3078,20 @@ public sealed record ProductSubscriptionIOS : ProductSubscription, ProductCommon public IapPlatform Platform { get; init; } = global::OpenIap.IapPlatform.IOS; [JsonPropertyName("price")] public double? Price { get; init; } - /// iOS 26.4+ subscription pricing terms, including billing plan metadata for - /// monthly subscriptions with a 12-month commitment. + /// + /// iOS 26.4+ subscription pricing terms, including billing plan metadata for + /// monthly subscriptions with a 12-month commitment. + /// [JsonPropertyName("pricingTermsIOS")] public IReadOnlyList? PricingTermsIOS { get; init; } /// App Store subscription group identifier for intro-offer eligibility checks. [JsonPropertyName("subscriptionGroupIdIOS")] public string? SubscriptionGroupIdIOS { get; init; } - /// Standardized subscription offers. - /// Cross-platform type with iOS-specific fields using suffix. - /// @see https://openiap.dev/docs/types/subscription-offer + /// + /// Standardized subscription offers. + /// Cross-platform type with iOS-specific fields using suffix. + /// @see https://openiap.dev/docs/types/subscription-offer + /// [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } [JsonPropertyName("subscriptionPeriodNumberIOS")] @@ -2942,11 +3124,13 @@ public sealed record PurchaseAndroid : Purchase, PurchaseCommon public bool? IsAcknowledgedAndroid { get; init; } [JsonPropertyName("isAutoRenewing")] public required bool IsAutoRenewing { get; init; } - /// Whether the subscription is suspended (Android) - /// A suspended subscription means the user's payment method failed and they need to fix it. - /// Users should be directed to the subscription center to resolve the issue. - /// Do NOT grant entitlements for suspended subscriptions. - /// Available in Google Play Billing Library 8.1.0+ + /// + /// Whether the subscription is suspended (Android) + /// A suspended subscription means the user's payment method failed and they need to fix it. + /// Users should be directed to the subscription center to resolve the issue. + /// Do NOT grant entitlements for suspended subscriptions. + /// Available in Google Play Billing Library 8.1.0+ + /// [JsonPropertyName("isSuspendedAndroid")] public bool? IsSuspendedAndroid { get; init; } [JsonPropertyName("obfuscatedAccountIdAndroid")] @@ -2955,10 +3139,12 @@ public sealed record PurchaseAndroid : Purchase, PurchaseCommon public string? ObfuscatedProfileIdAndroid { get; init; } [JsonPropertyName("packageNameAndroid")] public string? PackageNameAndroid { get; init; } - /// 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+ + /// + /// 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+ + /// [JsonPropertyName("pendingPurchaseUpdateAndroid")] public PendingPurchaseUpdateAndroid? PendingPurchaseUpdateAndroid { get; init; } [JsonPropertyName("productId")] @@ -2979,13 +3165,17 @@ public sealed record PurchaseAndroid : Purchase, PurchaseCommon public required double TransactionDate { get; init; } [JsonPropertyName("transactionId")] public string? TransactionId { get; init; } - /// Amazon Appstore user id (PurchaseResponse.getUserData().getUserId()). - /// Only populated on the Amazon flavor; required for server-side Amazon RVS - /// receipt verification (userId + receiptId). Null on Google Play and Horizon. + /// + /// Amazon Appstore user id (PurchaseResponse.getUserData().getUserId()). + /// Only populated on the Amazon flavor; required for server-side Amazon RVS + /// receipt verification (userId + receiptId). Null on Google Play and Horizon. + /// [JsonPropertyName("userIdAmazon")] public string? UserIdAmazon { get; init; } - /// Amazon Appstore marketplace (PurchaseResponse.getUserData().getMarketplace()), - /// for example "US" or "FR". Only populated on the Amazon flavor. + /// + /// Amazon Appstore marketplace (PurchaseResponse.getUserData().getMarketplace()), + /// for example "US" or "FR". Only populated on the Amazon flavor. + /// [JsonPropertyName("userMarketplaceAmazon")] public string? UserMarketplaceAmazon { get; init; } } @@ -3014,9 +3204,11 @@ public sealed record PurchaseError public sealed record PurchaseIOS : Purchase, PurchaseCommon { - /// Advanced Commerce API metadata (iOS 18.4+). - /// Present only for transactions that use the Advanced Commerce API. - /// Contains item details, tax information, and refund data for generic SKU purchases. + /// + /// Advanced Commerce API metadata (iOS 18.4+). + /// Present only for transactions that use the Advanced Commerce API. + /// Contains item details, tax information, and refund data for generic SKU purchases. + /// [JsonPropertyName("advancedCommerceInfoIOS")] public AdvancedCommerceInfoIOS? AdvancedCommerceInfoIOS { get; init; } [JsonPropertyName("appAccountToken")] @@ -3026,8 +3218,10 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon /// iOS 26.4+ billing plan selected for this transaction. [JsonPropertyName("billingPlanTypeIOS")] public SubscriptionBillingPlanTypeIOS? BillingPlanTypeIOS { get; init; } - /// Original transaction identifier for the subscription bundle that produced - /// this transaction (Apple 27+ SDK; back-deployed by StoreKit). + /// + /// Original transaction identifier for the subscription bundle that produced + /// this transaction (Apple 27+ SDK; back-deployed by StoreKit). + /// [JsonPropertyName("bundleOriginalTransactionIdIOS")] public string? BundleOriginalTransactionIdIOS { get; init; } /// Product identifier of the subscription bundle that produced this transaction. @@ -3071,8 +3265,10 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon /// StoreKit ownership raw value. Xcode 27 adds the back-deployed assigned value. [JsonPropertyName("ownershipTypeIOS")] public string? OwnershipTypeIOS { get; init; } - /// Original transaction identifier replaced when moving between a standalone - /// subscription and a subscription bundle. + /// + /// Original transaction identifier replaced when moving between a standalone + /// subscription and a subscription bundle. + /// [JsonPropertyName("previousOriginalTransactionIdIOS")] public string? PreviousOriginalTransactionIdIOS { get; init; } [JsonPropertyName("productId")] @@ -3096,8 +3292,10 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon /// Normalized StoreKit revocation reason, including upgraded_to_bundle. [JsonPropertyName("revocationReasonIOS")] public string? RevocationReasonIOS { get; init; } - /// StoreKit revocation type, including assignment-revocation on Apple 26.4+ - /// when compiled with the Xcode 27 SDK. + /// + /// StoreKit revocation type, including assignment-revocation on Apple 26.4+ + /// when compiled with the Xcode 27 SDK. + /// [JsonPropertyName("revocationTypeIOS")] public string? RevocationTypeIOS { get; init; } /// Store where purchase was made @@ -3150,8 +3348,10 @@ public sealed record RenewalCommitmentInfoIOS public required double CommitmentRenewalPrice { get; init; } } -/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo -/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo +/// +/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo +/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo +/// public sealed record RenewalInfoIOS { [JsonPropertyName("autoRenewPreference")] @@ -3165,44 +3365,60 @@ public sealed record RenewalInfoIOS /// Subscription-group identifier for the bundle used by the next renewal. [JsonPropertyName("bundleSubscriptionGroupId")] public string? BundleSubscriptionGroupId { get; init; } - /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a - /// 12-month commitment. + /// + /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a + /// 12-month commitment. + /// [JsonPropertyName("commitmentInfo")] public RenewalCommitmentInfoIOS? CommitmentInfo { get; init; } - /// StoreKit's raw integer expiration-reason value represented as a string. - /// Xcode 27 adds the back-deployed unbundled case. Preserve unknown future values. + /// + /// StoreKit's raw integer expiration-reason value represented as a string. + /// Xcode 27 adds the back-deployed unbundled case. Preserve unknown future values. + /// [JsonPropertyName("expirationReason")] public string? ExpirationReason { get; init; } - /// Grace period expiration date (milliseconds since epoch) - /// When set, subscription is in grace period (billing issue but still has access) + /// + /// Grace period expiration date (milliseconds since epoch) + /// When set, subscription is in grace period (billing issue but still has access) + /// [JsonPropertyName("gracePeriodExpirationDate")] public double? GracePeriodExpirationDate { get; init; } - /// True if subscription failed to renew due to billing issue and is retrying - /// StoreKit exposes this directly as RenewalInfo.isInBillingRetry. + /// + /// True if subscription failed to renew due to billing issue and is retrying + /// StoreKit exposes this directly as RenewalInfo.isInBillingRetry. + /// [JsonPropertyName("isInBillingRetry")] public bool? IsInBillingRetry { get; init; } [JsonPropertyName("jsonRepresentation")] public string? JsonRepresentation { get; init; } - /// Product ID that will be used on next renewal (when user upgrades/downgrades) - /// If set and different from current productId, subscription will change on expiration + /// + /// Product ID that will be used on next renewal (when user upgrades/downgrades) + /// If set and different from current productId, subscription will change on expiration + /// [JsonPropertyName("pendingUpgradeProductId")] public string? PendingUpgradeProductId { get; init; } - /// User's response to subscription price increase - /// Possible values: "AGREED", "PENDING", null (no price increase) + /// + /// User's response to subscription price increase + /// Possible values: "AGREED", "PENDING", null (no price increase) + /// [JsonPropertyName("priceIncreaseStatus")] public string? PriceIncreaseStatus { get; init; } /// iOS 26.4+ billing plan that will renew after the current period. [JsonPropertyName("renewalBillingPlanType")] public SubscriptionBillingPlanTypeIOS? RenewalBillingPlanType { get; init; } - /// Expected renewal date (milliseconds since epoch) - /// For active subscriptions, when the next renewal/charge will occur + /// + /// Expected renewal date (milliseconds since epoch) + /// For active subscriptions, when the next renewal/charge will occur + /// [JsonPropertyName("renewalDate")] public double? RenewalDate { get; init; } /// Offer ID applied to next renewal (promotional offer, subscription offer code, etc.) [JsonPropertyName("renewalOfferId")] public string? RenewalOfferId { get; init; } - /// Type of offer applied to next renewal - /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. + /// + /// Type of offer applied to next renewal + /// Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc. + /// [JsonPropertyName("renewalOfferType")] public string? RenewalOfferType { get; init; } [JsonPropertyName("willAutoRenew")] @@ -3212,12 +3428,16 @@ public sealed record RenewalInfoIOS public bool? WillUnbundle { get; init; } } -/// Rental details for one-time purchase products that can be rented (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Rental details for one-time purchase products that can be rented (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record RentalDetailsAndroid { - /// Rental expiration period in ISO 8601 format - /// Time after rental period ends when user can still extend + /// + /// Rental expiration period in ISO 8601 format + /// Time after rental period ends when user can still extend + /// [JsonPropertyName("rentalExpirationPeriod")] public string? RentalExpirationPeriod { get; init; } /// Rental period in ISO 8601 format (e.g., P7D for 7 days) @@ -3233,19 +3453,25 @@ public sealed record RequestPurchaseResultPurchases(IReadOnlyList? Val public sealed record RequestVerifyPurchaseWithIapkitResult { - /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. - /// Public product payload when includeClientPayload was requested, the - /// Apple or Google receipt is valid, and a payload exists for that product. + /// + /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. + /// Public product payload when includeClientPayload was requested, the + /// Apple or Google receipt is valid, and a payload exists for that product. + /// [JsonPropertyName("clientPayload")] public IapkitProductClientPayload? ClientPayload { get; init; } - /// True when the purchase is valid and actionable. - /// Only entitled, pending-acknowledgment, or ready-to-consume return true. - /// Callers must still match productId and use the platform plus app-owned product - /// type to choose the fulfillment path. + /// + /// True when the purchase is valid and actionable. + /// Only entitled, pending-acknowledgment, or ready-to-consume return true. + /// Callers must still match productId and use the platform plus app-owned product + /// type to choose the fulfillment path. + /// [JsonPropertyName("isValid")] public required bool IsValid { get; init; } - /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. - /// Store-verified product identifier when the provider returns one. + /// + /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. + /// Store-verified product identifier when the provider returns one. + /// [JsonPropertyName("productId")] public string? ProductId { get; init; } /// The current state of the purchase. @@ -3265,18 +3491,22 @@ public sealed record SubscriptionCommitmentInfoIOS public required double Price { get; init; } } -/// Standardized subscription discount/promotional offer. -/// Provides a unified interface for subscription offers across iOS and Android. -/// -/// Both platforms support subscription offers with different implementations: -/// - iOS: Introductory offers, promotional offers with server-side signatures -/// - Android: Offer tokens with pricing phases -/// -/// @see https://openiap.dev/docs/types/subscription-offer +/// +/// Standardized subscription discount/promotional offer. +/// Provides a unified interface for subscription offers across iOS and Android. +/// +/// Both platforms support subscription offers with different implementations: +/// - iOS: Introductory offers, promotional offers with server-side signatures +/// - Android: Offer tokens with pricing phases +/// +/// @see https://openiap.dev/docs/types/subscription-offer +/// public sealed record SubscriptionOffer { - /// [Android] Base plan identifier. - /// Identifies which base plan this offer belongs to. + /// + /// [Android] Base plan identifier. + /// Identifies which base plan this offer belongs to. + /// [JsonPropertyName("basePlanIdAndroid")] public string? BasePlanIdAndroid { get; init; } /// Currency code (ISO 4217, e.g., "USD") @@ -3285,25 +3515,33 @@ public sealed record SubscriptionOffer /// Formatted display price string (e.g., "$9.99/month") [JsonPropertyName("displayPrice")] public required string DisplayPrice { get; init; } - /// Unique identifier for the offer. - /// - iOS: Discount identifier from App Store Connect - /// - Android: offerId from the Google Play subscription offer + /// + /// Unique identifier for the offer. + /// - iOS: Discount identifier from App Store Connect + /// - Android: offerId from the Google Play subscription offer + /// [JsonPropertyName("id")] public required string Id { get; init; } - /// [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+ + /// + /// [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+ + /// [JsonPropertyName("installmentPlanDetailsAndroid")] public InstallmentPlanDetailsAndroid? InstallmentPlanDetailsAndroid { get; init; } - /// [iOS] Key identifier for signature validation. - /// Used with server-side signature generation for promotional offers. + /// + /// [iOS] Key identifier for signature validation. + /// Used with server-side signature generation for promotional offers. + /// [JsonPropertyName("keyIdentifierIOS")] public string? KeyIdentifierIOS { get; init; } /// [iOS] Localized price string. [JsonPropertyName("localizedPriceIOS")] public string? LocalizedPriceIOS { get; init; } - /// [iOS] Cryptographic nonce (UUID) for signature validation. - /// Must be generated server-side for each purchase attempt. + /// + /// [iOS] Cryptographic nonce (UUID) for signature validation. + /// Must be generated server-side for each purchase attempt. + /// [JsonPropertyName("nonceIOS")] public string? NonceIOS { get; init; } /// [iOS] Number of billing periods for this discount. @@ -3312,8 +3550,10 @@ public sealed record SubscriptionOffer /// [Android] List of tags associated with this offer. [JsonPropertyName("offerTagsAndroid")] public IReadOnlyList? OfferTagsAndroid { get; init; } - /// [Android] Offer token required for purchase. - /// Must be passed to requestPurchase() when purchasing with this offer. + /// + /// [Android] Offer token required for purchase. + /// Must be passed to requestPurchase() when purchasing with this offer. + /// [JsonPropertyName("offerTokenAndroid")] public string? OfferTokenAndroid { get; init; } /// Payment mode during the offer period @@ -3328,16 +3568,22 @@ public sealed record SubscriptionOffer /// Numeric price value [JsonPropertyName("price")] public required double Price { get; init; } - /// [Android] Pricing phases for this subscription offer. - /// Contains detailed pricing information for each phase (trial, intro, regular). + /// + /// [Android] Pricing phases for this subscription offer. + /// Contains detailed pricing information for each phase (trial, intro, regular). + /// [JsonPropertyName("pricingPhasesAndroid")] public PricingPhasesAndroid? PricingPhasesAndroid { get; init; } - /// [iOS] Server-generated signature for promotional offer validation. - /// Required when applying promotional offers on iOS. + /// + /// [iOS] Server-generated signature for promotional offer validation. + /// Required when applying promotional offers on iOS. + /// [JsonPropertyName("signatureIOS")] public string? SignatureIOS { get; init; } - /// [iOS] Timestamp when the signature was generated. - /// Used for signature validation. + /// + /// [iOS] Timestamp when the signature was generated. + /// Used for signature validation. + /// [JsonPropertyName("timestampIOS")] public double? TimestampIOS { get; init; } /// Type of subscription offer (Introductory or Promotional) @@ -3400,22 +3646,28 @@ public sealed record TransactionCommitmentInfoIOS public required int TotalBillingPeriods { get; init; } } -/// User Choice Billing event details (Android) -/// Fired when a user selects alternative billing in the User Choice Billing dialog +/// +/// User Choice Billing event details (Android) +/// Fired when a user selects alternative billing in the User Choice Billing dialog +/// public sealed record UserChoiceBillingDetails { /// Token that must be reported to Google Play within 24 hours [JsonPropertyName("externalTransactionToken")] public required string ExternalTransactionToken { get; init; } - /// External transaction ID of the originating subscription when the user is - /// upgrading or downgrading a developer-billed subscription. Available in - /// OpenIAP Spec 2.3.0 / openiap-google 2.3.1 (requires Play Billing 9.1+). + /// + /// External transaction ID of the originating subscription when the user is + /// upgrading or downgrading a developer-billed subscription. Available in + /// OpenIAP Spec 2.3.0 / openiap-google 2.3.1 (requires Play Billing 9.1+). + /// [JsonPropertyName("originalExternalTransactionId")] public string? OriginalExternalTransactionId { get; init; } - /// Structured product details selected in the user-choice flow, including the - /// product type and offer token. Legacy payloads may omit this field; use - /// products as the product-ID fallback. Available in OpenIAP Spec 2.3.0 / - /// openiap-google 2.3.1 (requires Play Billing 9.1+). + /// + /// Structured product details selected in the user-choice flow, including the + /// product type and offer token. Legacy payloads may omit this field; use + /// products as the product-ID fallback. Available in OpenIAP Spec 2.3.0 / + /// openiap-google 2.3.1 (requires Play Billing 9.1+). + /// [JsonPropertyName("productDetailsAndroid")] public IReadOnlyList? ProductDetailsAndroid { get; init; } /// List of product IDs selected by the user @@ -3423,8 +3675,10 @@ public sealed record UserChoiceBillingDetails public required IReadOnlyList Products { get; init; } } -/// Valid time window for when an offer is available (Android) -/// Available in Google Play Billing Library 8.0+ +/// +/// Valid time window for when an offer is available (Android) +/// Available in Google Play Billing Library 8.0+ +/// public sealed record ValidTimeWindowAndroid { /// End time in milliseconds since epoch @@ -3475,8 +3729,10 @@ public sealed record VerifyPurchaseResultAndroid : VerifyPurchaseResult public required bool TestTransaction { get; init; } } -/// Result from Meta Horizon verify_entitlement API. -/// Returns verification status and grant time for the entitlement. +/// +/// Result from Meta Horizon verify_entitlement API. +/// Returns verification status and grant time for the entitlement. +/// public sealed record VerifyPurchaseResultHorizon : VerifyPurchaseResult { /// Unix timestamp (seconds) when the entitlement was granted. @@ -3539,8 +3795,10 @@ public sealed record AndroidSubscriptionOfferInput public required string OfferToken { get; init; } } -/// Parameters for showing a billing program information dialog (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Parameters for showing a billing program information dialog (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// public sealed record BillingProgramInformationDialogParamsAndroid { /// Billing program. Currently only BILLING_CHOICE is supported. @@ -3561,26 +3819,34 @@ public sealed record DeepLinkOptions public string? PackageNameAndroid { get; init; } } -/// Parameters for a developer billing option in a purchase flow (Android). -/// Used with BillingFlowParams for external payments (8.3.0+) and Billing Choice -/// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+). -/// Only billingProgram is required; link fields are used when the selected program -/// links outside the app. +/// +/// Parameters for a developer billing option in a purchase flow (Android). +/// Used with BillingFlowParams for external payments (8.3.0+) and Billing Choice +/// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+). +/// Only billingProgram is required; link fields are used when the selected program +/// links outside the app. +/// public sealed record DeveloperBillingOptionParamsAndroid { /// The billing program. Use EXTERNAL_PAYMENTS or BILLING_CHOICE. [JsonPropertyName("billingProgram")] public required BillingProgramAndroid BillingProgram { get; init; } - /// The URI where the external payment will be processed. - /// Required only when the selected billing program links outside the app. + /// + /// The URI where the external payment will be processed. + /// Required only when the selected billing program links outside the app. + /// [JsonPropertyName("linkUri")] public string? LinkUri { get; init; } - /// The launch mode for the external payment link. - /// Required only when the selected billing program links outside the app. + /// + /// The launch mode for the external payment link. + /// Required only when the selected billing program links outside the app. + /// [JsonPropertyName("launchMode")] public DeveloperBillingLaunchModeAndroid? LaunchMode { get; init; } - /// A pre-generated external transaction token for a Billing Choice external-link - /// flow. Omit it when Google Play should provide the token in the callback. + /// + /// A pre-generated external transaction token for a Billing Choice external-link + /// flow. Omit it when Google Play should provide the token in the callback. + /// [JsonPropertyName("externalTransactionToken")] public string? ExternalTransactionToken { get; init; } } @@ -3604,8 +3870,10 @@ public sealed record DiscountOfferInputIOS public required double Timestamp { get; init; } } -/// Parameters for fetching Billing Choice display information (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// +/// Parameters for fetching Billing Choice display information (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). +/// public sealed record GetBillingChoiceInfoParamsAndroid { /// Billing program. Currently only BILLING_CHOICE is supported. @@ -3619,9 +3887,11 @@ public sealed record GetBillingChoiceInfoParamsAndroid public string? UserLocale { get; init; } } -/// Parameters for showing Play billing in-app messages (Android) -/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 -/// (upstream API available since Play Billing 4.1.0). +/// +/// Parameters for showing Play billing in-app messages (Android) +/// Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 +/// (upstream API available since Play Billing 4.1.0). +/// public sealed record InAppMessageParamsAndroid { /// In-app message categories to show. Defaults to transactional messages. @@ -3632,31 +3902,37 @@ public sealed record InAppMessageParamsAndroid /// Connection initialization configuration public sealed record InitConnectionConfig { - /// Enable a specific billing program for Android (7.0+) - /// When set, enables the specified billing program for external transactions. - /// - USER_CHOICE_BILLING: User can select between Google Play or alternative (7.0+) - /// - EXTERNAL_CONTENT_LINK: Link to external content (introduced in 8.2.0; use 8.2.1+) - /// - EXTERNAL_OFFER: External offers for digital content (introduced in 8.2.0; use 8.2.1+) - /// - EXTERNAL_PAYMENTS: Developer provided billing, Japan only (8.3.0+) - /// - BILLING_CHOICE: Google-rendered or developer-rendered billing choice - /// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+) + /// + /// Enable a specific billing program for Android (7.0+) + /// When set, enables the specified billing program for external transactions. + /// - USER_CHOICE_BILLING: User can select between Google Play or alternative (7.0+) + /// - EXTERNAL_CONTENT_LINK: Link to external content (introduced in 8.2.0; use 8.2.1+) + /// - EXTERNAL_OFFER: External offers for digital content (introduced in 8.2.0; use 8.2.1+) + /// - EXTERNAL_PAYMENTS: Developer provided billing, Japan only (8.3.0+) + /// - BILLING_CHOICE: Google-rendered or developer-rendered billing choice + /// (OpenIAP Spec 2.1.0 / openiap-google 2.3.0; requires Play Billing 9.1.0+) + /// [JsonPropertyName("enableBillingProgramAndroid")] public BillingProgramAndroid? EnableBillingProgramAndroid { get; init; } - /// Billing Choice renderer configured in Play Console. Available in OpenIAP - /// Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// GOOGLE_RENDERED registers the developer-provided billing listener so OpenIAP - /// can emit the selection event. DEVELOPER_RENDERED omits that listener so the - /// app can render its own choice screen and use the reporting/dialog/link APIs. - /// Must match choiceScreenType returned by isBillingProgramAvailableAndroid. - /// Defaults to GOOGLE_RENDERED. + /// + /// Billing Choice renderer configured in Play Console. Available in OpenIAP + /// Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// GOOGLE_RENDERED registers the developer-provided billing listener so OpenIAP + /// can emit the selection event. DEVELOPER_RENDERED omits that listener so the + /// app can render its own choice screen and use the reporting/dialog/link APIs. + /// Must match choiceScreenType returned by isBillingProgramAvailableAndroid. + /// Defaults to GOOGLE_RENDERED. + /// [JsonPropertyName("billingChoiceScreenTypeAndroid")] public BillingChoiceScreenTypeAndroid? BillingChoiceScreenTypeAndroid { get; init; } = global::OpenIap.BillingChoiceScreenTypeAndroid.GoogleRendered; } -/// Parameters for launching an external link (Android) -/// Used with launchExternalLink to initiate external offer, app install, or -/// developer-rendered Billing Choice flows -/// Available in Google Play Billing Library 8.2.0+ +/// +/// Parameters for launching an external link (Android) +/// Used with launchExternalLink to initiate external offer, app install, or +/// developer-rendered Billing Choice flows +/// Available in Google Play Billing Library 8.2.0+ +/// public sealed record LaunchExternalLinkParamsAndroid { /// The billing program (EXTERNAL_CONTENT_LINK, EXTERNAL_OFFER, or BILLING_CHOICE) @@ -3671,9 +3947,11 @@ public sealed record LaunchExternalLinkParamsAndroid /// The URI where the content will be accessed from [JsonPropertyName("linkUri")] public required string LinkUri { get; init; } - /// External transaction token for a developer-rendered Billing Choice external-link - /// flow. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). Generate it with createBillingProgramReportingDetailsAndroid. + /// + /// External transaction token for a developer-rendered Billing Choice external-link + /// flow. Available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). Generate it with createBillingProgramReportingDetailsAndroid. + /// [JsonPropertyName("externalTransactionToken")] public string? ExternalTransactionToken { get; init; } } @@ -3686,18 +3964,22 @@ public sealed record ProductRequest public ProductQueryType? Type { get; init; } = global::OpenIap.ProductQueryType.InApp; } -/// JWS promotional offer input for iOS 15+ (StoreKit 2, WWDC 2025). -/// New signature format using compact JWS string for promotional offers. -/// This provides a simpler alternative to the legacy signature-based promotional offers. -/// Back-deployed to iOS 15. +/// +/// JWS promotional offer input for iOS 15+ (StoreKit 2, WWDC 2025). +/// New signature format using compact JWS string for promotional offers. +/// This provides a simpler alternative to the legacy signature-based promotional offers. +/// Back-deployed to iOS 15. +/// public sealed record PromotionalOfferJWSInputIOS { /// The promotional offer identifier from App Store Connect [JsonPropertyName("offerId")] public required string OfferId { get; init; } - /// Compact JWS string signed by your server. - /// The JWS should contain the promotional offer signature data. - /// Format: header.payload.signature (base64url encoded) + /// + /// Compact JWS string signed by your server. + /// The JWS should contain the promotional offer signature data. + /// Format: header.payload.signature (base64url encoded) + /// [JsonPropertyName("jws")] public required string Jws { get; init; } } @@ -3714,19 +3996,23 @@ public sealed record PurchaseOptions /// Limit to currently active items on iOS [JsonPropertyName("onlyIncludeActiveItemsIOS")] public bool? OnlyIncludeActiveItemsIOS { get; init; } - /// Include suspended subscriptions in the result (Android 8.1+). - /// Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. - /// Users should be directed to the subscription center to resolve payment issues. - /// Default: false (only active subscriptions are returned) + /// + /// Include suspended subscriptions in the result (Android 8.1+). + /// Suspended subscriptions have isSuspendedAndroid=true and should NOT be granted entitlements. + /// Users should be directed to the subscription center to resolve payment issues. + /// Default: false (only active subscriptions are returned) + /// [JsonPropertyName("includeSuspendedAndroid")] public bool? IncludeSuspendedAndroid { get; init; } } public sealed record PurchaseUpdatedListenerOptions { - /// iOS only. Defaults to true. When false, listener callbacks also receive - /// StoreKit replay events for a transaction ID that was already emitted during - /// the current connection session. Android ignores this option. + /// + /// iOS only. Defaults to true. When false, listener callbacks also receive + /// StoreKit replay events for a transaction ID that was already emitted during + /// the current connection session. Android ignores this option. + /// [JsonPropertyName("dedupeTransactionIOS")] public bool? DedupeTransactionIOS { get; init; } } @@ -3742,18 +4028,24 @@ public sealed record RequestPurchaseAndroidProps /// Obfuscated profile ID [JsonPropertyName("obfuscatedProfileId")] public string? ObfuscatedProfileId { get; init; } - /// Personalized offer flag. - /// When true, indicates the price was customized for this user. + /// + /// Personalized offer flag. + /// When true, indicates the price was customized for this user. + /// [JsonPropertyName("isOfferPersonalized")] public bool? IsOfferPersonalized { get; init; } - /// Offer token for one-time purchase discounts (8.0+). - /// Pass the offerToken from discountOffers - /// to apply a discount offer to the purchase. + /// + /// Offer token for one-time purchase discounts (8.0+). + /// Pass the offerToken from discountOffers + /// to apply a discount offer to the purchase. + /// [JsonPropertyName("offerToken")] public string? OfferToken { get; init; } - /// Developer billing option parameters for external payments and Billing Choice. - /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). + /// + /// Developer billing option parameters for external payments and Billing Choice. + /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). + /// [JsonPropertyName("developerBillingOption")] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -3772,14 +4064,18 @@ public sealed record RequestPurchaseIosProps /// Purchase quantity [JsonPropertyName("quantity")] public int? Quantity { get; init; } - /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). - /// iOS only supports promotional offers for auto-renewable subscriptions. + /// + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. + /// [JsonPropertyName("withOffer")] public DiscountOfferInputIOS? WithOffer { get; init; } - /// Advanced commerce data token (iOS 15+). - /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing - /// campaign tokens, affiliate IDs, or other attribution data. - /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// + /// Advanced commerce data token (iOS 15+). + /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing + /// campaign tokens, affiliate IDs, or other attribution data. + /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// [JsonPropertyName("advancedCommerceData")] public string? AdvancedCommerceData { get; init; } } @@ -3813,13 +4109,15 @@ public void Validate() void IJsonOnDeserialized.OnDeserialized() => Validate(); } -/// Platform-specific purchase request parameters. -/// -/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. -/// - apple: Always targets App Store -/// - google: Targets Play Store by default, Horizon when built with horizon flavor, -/// or Fire OS when built with amazon flavor -/// (determined at build time, not runtime) +/// +/// Platform-specific purchase request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor +/// (determined at build time, not runtime) +/// public sealed record RequestPurchasePropsByPlatforms { /// Apple-specific purchase parameters @@ -3841,31 +4139,39 @@ public sealed record RequestSubscriptionAndroidProps /// Obfuscated profile ID [JsonPropertyName("obfuscatedProfileId")] public string? ObfuscatedProfileId { get; init; } - /// Personalized offer flag. - /// When true, indicates the price was customized for this user. + /// + /// Personalized offer flag. + /// When true, indicates the price was customized for this user. + /// [JsonPropertyName("isOfferPersonalized")] public bool? IsOfferPersonalized { get; init; } /// Purchase token for upgrades/downgrades [JsonPropertyName("purchaseToken")] public string? PurchaseToken { get; init; } - /// Original external transaction ID for replacing a subscription that was - /// purchased through developer billing. Available in OpenIAP Spec 2.1.0 / - /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Original external transaction ID for replacing a subscription that was + /// purchased through developer billing. Available in OpenIAP Spec 2.1.0 / + /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// [JsonPropertyName("originalExternalTransactionId")] public string? OriginalExternalTransactionId { get; init; } /// Subscription offers [JsonPropertyName("subscriptionOffers")] public IReadOnlyList? SubscriptionOffers { get; init; } - /// Product-level replacement parameters (8.1.0+) - /// Use this instead of replacementMode for item-level replacement - /// This singular form requires skus to contain exactly one target product. - /// Multi-item subscription changes need a per-target replacement mapping and - /// are rejected rather than applying one oldProductId to multiple products. + /// + /// Product-level replacement parameters (8.1.0+) + /// Use this instead of replacementMode for item-level replacement + /// This singular form requires skus to contain exactly one target product. + /// Multi-item subscription changes need a per-target replacement mapping and + /// are rejected rather than applying one oldProductId to multiple products. + /// [JsonPropertyName("subscriptionProductReplacementParams")] public SubscriptionProductReplacementParamsAndroid? SubscriptionProductReplacementParams { get; init; } - /// Developer billing option parameters for external payments and Billing Choice. - /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). + /// + /// Developer billing option parameters for external payments and Billing Choice. + /// Billing Choice is available in OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). + /// [JsonPropertyName("developerBillingOption")] public DeveloperBillingOptionParamsAndroid? DeveloperBillingOption { get; init; } } @@ -3880,46 +4186,60 @@ public sealed record RequestSubscriptionIosProps public string? AppAccountToken { get; init; } [JsonPropertyName("quantity")] public int? Quantity { get; init; } - /// Promotional offer to apply for subscription purchases. - /// Requires server-signed offer with nonce, timestamp, keyId, and signature. + /// + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. + /// [JsonPropertyName("withOffer")] public DiscountOfferInputIOS? WithOffer { get; init; } - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// The offer is available when the customer is eligible and can be discovered - /// via StoreKit Message (automatic) or subscription offer APIs. + /// + /// Win-back offer to apply (iOS 18+) + /// Used to re-engage churned subscribers with a discount or free trial. + /// The offer is available when the customer is eligible and can be discovered + /// via StoreKit Message (automatic) or subscription offer APIs. + /// [JsonPropertyName("winBackOffer")] public WinBackOfferInputIOS? WinBackOffer { get; init; } - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. + /// + /// JWS promotional offer (iOS 15+, WWDC 2025). + /// New signature format using compact JWS string for promotional offers. + /// Back-deployed to iOS 15. + /// [JsonPropertyName("promotionalOfferJWS")] public PromotionalOfferJWSInputIOS? PromotionalOfferJws { get; init; } - /// Billing plan to use when purchasing an annual subscription that offers - /// monthly billing with a 12-month commitment (iOS 26.4+). + /// + /// Billing plan to use when purchasing an annual subscription that offers + /// monthly billing with a 12-month commitment (iOS 26.4+). + /// [JsonPropertyName("billingPlanType")] public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; } - /// Compact JWS string for overriding introductory offer eligibility - /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility. - /// Generate the JWS on your server and pass it to StoreKit's - /// introductoryOfferEligibility(compactJWS:) purchase option. + /// + /// Compact JWS string for overriding introductory offer eligibility + /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility. + /// Generate the JWS on your server and pass it to StoreKit's + /// introductoryOfferEligibility(compactJWS:) purchase option. + /// [JsonPropertyName("compactJWS")] public string? CompactJws { get; init; } - /// Advanced commerce data token (iOS 15+). - /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing - /// campaign tokens, affiliate IDs, or other attribution data. - /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// + /// Advanced commerce data token (iOS 15+). + /// Used with StoreKit 2's Product.PurchaseOption.custom API for passing + /// campaign tokens, affiliate IDs, or other attribution data. + /// The data is formatted as JSON: {"signatureInfo": {"token": "<value>"}} + /// [JsonPropertyName("advancedCommerceData")] public string? AdvancedCommerceData { get; init; } } -/// Platform-specific subscription request parameters. -/// -/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. -/// - apple: Always targets App Store -/// - google: Targets Play Store by default, Horizon when built with horizon flavor, -/// or Fire OS when built with amazon flavor -/// (determined at build time, not runtime) +/// +/// Platform-specific subscription request parameters. +/// +/// Note: "Platforms" refers to the SDK/OS level (apple, google), not the store. +/// - apple: Always targets App Store +/// - google: Targets Play Store by default, Horizon when built with horizon flavor, +/// or Fire OS when built with amazon flavor +/// (determined at build time, not runtime) +/// public sealed record RequestSubscriptionPropsByPlatforms { /// Apple-specific subscription parameters @@ -3957,26 +4277,32 @@ public sealed record RequestVerifyPurchaseWithIapkitGoogleProps public required string PurchaseToken { get; init; } } -/// Platform-specific verification parameters for IAPKit. -/// -/// - apple: Verifies via App Store (JWS token) -/// - google: Verifies via Play Store (purchase token) -/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) +/// +/// Platform-specific verification parameters for IAPKit. +/// +/// - apple: Verifies via App Store (JWS token) +/// - google: Verifies via Play Store (purchase token) +/// - amazon: Verifies via Amazon Appstore RVS (userId + receiptId) +/// public sealed record RequestVerifyPurchaseWithIapkitProps { /// API key used for the Authorization header (Bearer {apiKey}). [JsonPropertyName("apiKey")] public string? ApiKey { get; init; } - /// Available in OpenIAP Spec 2.3.1 / openiap-apple 2.4.0 / openiap-google 2.4.0. - /// Base URL for the IAPKit server. Defaults to https://kit.openiap.dev. - /// Set this to a reachable HTTP(S) origin when self-hosting or testing a local IAPKit server. - /// The apiKey must be issued by the same IAPKit/Convex deployment as this server. + /// + /// Available in OpenIAP Spec 2.3.1 / openiap-apple 2.4.0 / openiap-google 2.4.0. + /// Base URL for the IAPKit server. Defaults to https://kit.openiap.dev. + /// Set this to a reachable HTTP(S) origin when self-hosting or testing a local IAPKit server. + /// The apiKey must be issued by the same IAPKit/Convex deployment as this server. + /// [JsonPropertyName("baseUrl")] public string? BaseUrl { get; init; } - /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. - /// Include the product's public IAPKit client payload in a valid Apple or - /// Google verification response. Defaults to false so existing response - /// shapes and bandwidth remain unchanged. + /// + /// Available in OpenIAP Spec 2.4.0 / openiap-apple 2.4.1 / openiap-google 2.4.1. + /// Include the product's public IAPKit client payload in a valid Apple or + /// Google verification response. Defaults to false so existing response + /// shapes and bandwidth remain unchanged. + /// [JsonPropertyName("includeClientPayload")] public bool? IncludeClientPayload { get; init; } /// Apple App Store verification parameters. @@ -3990,9 +4316,11 @@ public sealed record RequestVerifyPurchaseWithIapkitProps public RequestVerifyPurchaseWithIapkitAmazonProps? Amazon { get; init; } } -/// Product-level subscription replacement parameters (Android) -/// Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams -/// Available in Google Play Billing Library 8.1.0+ +/// +/// Product-level subscription replacement parameters (Android) +/// Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams +/// Available in Google Play Billing Library 8.1.0+ +/// public sealed record SubscriptionProductReplacementParamsAndroid { /// The old product ID that needs to be replaced @@ -4003,8 +4331,10 @@ public sealed record SubscriptionProductReplacementParamsAndroid public required SubscriptionReplacementModeAndroid ReplacementMode { get; init; } } -/// Apple App Store verification parameters. -/// Used for server-side receipt validation via App Store Server API. +/// +/// Apple App Store verification parameters. +/// Used for server-side receipt validation via App Store Server API. +/// public sealed record VerifyPurchaseAppleOptions { /// Product SKU to validate @@ -4012,10 +4342,12 @@ public sealed record VerifyPurchaseAppleOptions public required string Sku { get; init; } } -/// Google Play Store verification parameters. -/// Used for server-side receipt validation via Google Play Developer API. -/// -/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. +/// +/// Google Play Store verification parameters. +/// Used for server-side receipt validation via Google Play Developer API. +/// +/// ⚠️ SECURITY: Contains sensitive tokens (accessToken, purchaseToken). Do not log or persist this data. +/// public sealed record VerifyPurchaseGoogleOptions { /// Product SKU to validate @@ -4024,12 +4356,16 @@ public sealed record VerifyPurchaseGoogleOptions /// Android package name (e.g., com.example.app) [JsonPropertyName("packageName")] public required string PackageName { get; init; } - /// Purchase token from the purchase response. - /// ⚠️ Sensitive: Do not log this value. + /// + /// Purchase token from the purchase response. + /// ⚠️ Sensitive: Do not log this value. + /// [JsonPropertyName("purchaseToken")] public required string PurchaseToken { get; init; } - /// Google OAuth2 access token for API authentication. - /// ⚠️ Sensitive: Do not log this value. + /// + /// Google OAuth2 access token for API authentication. + /// ⚠️ Sensitive: Do not log this value. + /// [JsonPropertyName("accessToken")] public required string AccessToken { get; init; } /// Whether this is a subscription purchase (affects API endpoint used) @@ -4037,11 +4373,13 @@ public sealed record VerifyPurchaseGoogleOptions public bool? IsSub { get; init; } } -/// Meta Horizon (Quest) verification parameters. -/// Used for server-side entitlement verification via Meta's S2S API. -/// POST https://graph.oculus.com/$APP_ID/verify_entitlement -/// -/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. +/// +/// Meta Horizon (Quest) verification parameters. +/// Used for server-side entitlement verification via Meta's S2S API. +/// POST https://graph.oculus.com/$APP_ID/verify_entitlement +/// +/// ⚠️ SECURITY: Contains sensitive token (accessToken). Do not log or persist this data. +/// public sealed record VerifyPurchaseHorizonOptions { /// The SKU for the add-on item, defined in Meta Developer Dashboard @@ -4050,17 +4388,21 @@ public sealed record VerifyPurchaseHorizonOptions /// The user ID of the user whose purchase you want to verify [JsonPropertyName("userId")] public required string UserId { get; init; } - /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). - /// ⚠️ Sensitive: Do not log this value. + /// + /// Access token for Meta API authentication (OC|$APP_ID|$APP_SECRET or User Access Token). + /// ⚠️ Sensitive: Do not log this value. + /// [JsonPropertyName("accessToken")] public required string AccessToken { get; init; } } -/// Platform-specific purchase verification parameters. -/// -/// - apple: Verifies via App Store Server API -/// - google: Verifies via Google Play Developer API -/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) +/// +/// Platform-specific purchase verification parameters. +/// +/// - apple: Verifies via App Store Server API +/// - google: Verifies via Google Play Developer API +/// - horizon: Verifies via Meta's S2S API (verify_entitlement endpoint) +/// public sealed record VerifyPurchaseProps { /// Apple App Store verification parameters. @@ -4082,10 +4424,12 @@ public sealed record VerifyPurchaseWithProviderProps public RequestVerifyPurchaseWithIapkitProps? Iapkit { get; init; } } -/// Win-back offer input for iOS 18+ (StoreKit 2) -/// Win-back offers are used to re-engage churned subscribers. -/// The offer is automatically presented via StoreKit Message when eligible, -/// or can be applied programmatically during purchase. +/// +/// Win-back offer input for iOS 18+ (StoreKit 2) +/// Win-back offers are used to re-engage churned subscribers. +/// The offer is automatically presented via StoreKit Message when eligible, +/// or can be applied programmatically during purchase. +/// public sealed record WinBackOfferInputIOS { /// The win-back offer ID from App Store Connect @@ -4100,306 +4444,404 @@ public sealed record WinBackOfferInputIOS /// GraphQL root mutation operations. public interface MutationResolver { - /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. - /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android + /// + /// Acknowledge a non-consumable purchase. Required within 3 days or Google auto-refunds. + /// See: https://openiap.dev/docs/apis/android/acknowledge-purchase-android + /// Task AcknowledgePurchaseAndroidAsync(string purchaseToken); - /// Present the refund request sheet (iOS 15+). See also Features → Refund. - /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios + /// + /// Present the refund request sheet (iOS 15+). See also Features → Refund. + /// See: https://openiap.dev/docs/apis/ios/begin-refund-request-ios + /// Task BeginRefundRequestIOSAsync(string sku); - /// Clear pending transactions in the queue (sandbox helper). - /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios + /// + /// Clear pending transactions in the queue (sandbox helper). + /// See: https://openiap.dev/docs/apis/ios/clear-transaction-ios + /// Task ClearTransactionIOSAsync(); - /// Consume a consumable purchase so it can be re-bought. - /// See: https://openiap.dev/docs/apis/android/consume-purchase-android + /// + /// Consume a consumable purchase so it can be re-bought. + /// See: https://openiap.dev/docs/apis/android/consume-purchase-android + /// Task ConsumePurchaseAndroidAsync(string purchaseToken); - /// Create the reporting details and external transaction token required by a billing program. - /// Introduced in Play Billing 8.2.0. External Offer and External Content Link integrations - /// must use 8.2.1+ and create fresh details immediately before every redirect session; - /// do not cache the token for a later redirect. The same token may report multiple purchases - /// made during one External Offer session. - /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. - /// Returns external transaction token needed for reporting external transactions. - /// developerBillingType is optional. When program is BILLING_CHOICE and developerBillingType is omitted, - /// native Android defaults it to IN_APP. - /// The Billing Choice extension is available in OpenIAP Spec 2.1.0 / - /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// + /// Create the reporting details and external transaction token required by a billing program. + /// Introduced in Play Billing 8.2.0. External Offer and External Content Link integrations + /// must use 8.2.1+ and create fresh details immediately before every redirect session; + /// do not cache the token for a later redirect. The same token may report multiple purchases + /// made during one External Offer session. + /// Replaces the deprecated createExternalOfferReportingDetailsAsync API. + /// Returns external transaction token needed for reporting external transactions. + /// developerBillingType is optional. When program is BILLING_CHOICE and developerBillingType is omitted, + /// native Android defaults it to IN_APP. + /// The Billing Choice extension is available in OpenIAP Spec 2.1.0 / + /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/create-billing-program-reporting-details-android + /// Task CreateBillingProgramReportingDetailsAndroidAsync(BillingProgramAndroid program, DeveloperBillingTypeAndroid? developerBillingType = null); - /// Open the platform's subscription management UI. - /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions + /// + /// Open the platform's subscription management UI. + /// See: https://openiap.dev/docs/apis/deep-link-to-subscriptions + /// Task DeepLinkToSubscriptionsAsync(DeepLinkOptions? options = null); - /// Close the store connection and release resources. - /// See: https://openiap.dev/docs/apis/end-connection + /// + /// Close the store connection and release resources. + /// See: https://openiap.dev/docs/apis/end-connection + /// Task EndConnectionAsync(); - /// Complete a transaction after server-side verification. Required on Android within 3 days. - /// See: https://openiap.dev/docs/apis/finish-transaction + /// + /// Complete a transaction after server-side verification. Required on Android within 3 days. + /// See: https://openiap.dev/docs/apis/finish-transaction + /// Task FinishTransactionAsync(PurchaseInput purchase, bool? isConsumable = null); - /// Initialize the store connection. Call before any IAP API. - /// See: https://openiap.dev/docs/apis/init-connection + /// + /// Initialize the store connection. Call before any IAP API. + /// See: https://openiap.dev/docs/apis/init-connection + /// Task InitConnectionAsync(InitConnectionConfig? config = null); - /// Check whether a billing program (e.g., External Payments) is available for the current user. - /// Replaces the deprecated isExternalOfferAvailableAsync API. - /// Introduced in Google Play Billing Library 8.2.0. External Offer and External - /// Content Link integrations must use 8.2.1+ because 8.2.1 fixes this API. - /// Returns availability result with isAvailable flag. - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android + /// + /// Check whether a billing program (e.g., External Payments) is available for the current user. + /// Replaces the deprecated isExternalOfferAvailableAsync API. + /// Introduced in Google Play Billing Library 8.2.0. External Offer and External + /// Content Link integrations must use 8.2.1+ because 8.2.1 fixes this API. + /// Returns availability result with isAvailable flag. + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/is-billing-program-available-android + /// Task IsBillingProgramAvailableAndroidAsync(BillingProgramAndroid program); - /// Launch an external content/offer link from inside the Billing Programs flow (introduced in - /// Play Billing 8.2.0; External Offer and External Content Link require 8.2.1+), - /// including developer-rendered Billing Choice external-link flows. - /// Billing Choice availability: OpenIAP Spec 2.1.0 / openiap-google 2.3.0 - /// (requires Play Billing 9.1.0+). - /// Replaces the deprecated showExternalOfferInformationDialog API. - /// Shows Play Store dialog and optionally launches external URL. - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/launch-external-link-android + /// + /// Launch an external content/offer link from inside the Billing Programs flow (introduced in + /// Play Billing 8.2.0; External Offer and External Content Link require 8.2.1+), + /// including developer-rendered Billing Choice external-link flows. + /// Billing Choice availability: OpenIAP Spec 2.1.0 / openiap-google 2.3.0 + /// (requires Play Billing 9.1.0+). + /// Replaces the deprecated showExternalOfferInformationDialog API. + /// Shows Play Store dialog and optionally launches external URL. + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/launch-external-link-android + /// Task LaunchExternalLinkAndroidAsync(LaunchExternalLinkParamsAndroid @params); - /// Open the Google Play offer/promo code redemption flow so the user can enter a code. - /// On Google Play builds, launches the Play Store redeem page - /// (https://play.google.com/redeem). A purchase listener can receive the redeemed - /// purchase while the app is running with an active billing connection; always - /// reconcile with getAvailablePurchases when the app resumes. - /// Does not require the billing client to be initialized (no Play Billing version requirement). - /// Available in OpenIAP Spec 2.4.2 / openiap-google 2.5.0. - /// Android counterpart of presentCodeRedemptionSheetIOS. - /// Returns true when the redemption flow was launched, or false when the current - /// store flavor does not provide an equivalent redemption flow. - /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android + /// + /// Open the Google Play offer/promo code redemption flow so the user can enter a code. + /// On Google Play builds, launches the Play Store redeem page + /// (https://play.google.com/redeem). A purchase listener can receive the redeemed + /// purchase while the app is running with an active billing connection; always + /// reconcile with getAvailablePurchases when the app resumes. + /// Does not require the billing client to be initialized (no Play Billing version requirement). + /// Available in OpenIAP Spec 2.4.2 / openiap-google 2.5.0. + /// Android counterpart of presentCodeRedemptionSheetIOS. + /// Returns true when the redemption flow was launched, or false when the current + /// store flavor does not provide an equivalent redemption flow. + /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android + /// Task OpenRedeemOfferCodeAndroidAsync(); - /// Show the App Store offer code redemption sheet. - /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or - /// visionOS 27+, returns the verified transaction produced by the redemption. - /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, - /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. - /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the - /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 - /// call has no effect and returns null. Reconcile null results from a presented - /// sheet through the normal transaction listener or an explicit - /// available-purchases refresh. - /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// + /// Show the App Store offer code redemption sheet. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit + /// available-purchases refresh. + /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + /// Task PresentCodeRedemptionSheetIOSAsync(); - /// Present an external purchase link, StoreKit External (iOS 16+). - /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// + /// Present an external purchase link, StoreKit External (iOS 16+). + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-link-ios + /// Task PresentExternalPurchaseLinkIOSAsync(string url); - /// Present the external purchase notice sheet (iOS 17.4+). - /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() - /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// + /// Present the external purchase notice sheet (iOS 17.4+). + /// Uses ExternalPurchase.presentNoticeSheet() which returns a token when the user continues. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchase/presentnoticesheet() + /// See: https://openiap.dev/docs/apis/ios/present-external-purchase-notice-sheet-ios + /// Task PresentExternalPurchaseNoticeSheetIOSAsync(); - /// Initiate a purchase or subscription flow; rely on events for final state. - /// See: https://openiap.dev/docs/apis/request-purchase + /// + /// Initiate a purchase or subscription flow; rely on events for final state. + /// See: https://openiap.dev/docs/apis/request-purchase + /// Task RequestPurchaseAsync(RequestPurchaseProps @params); - /// Restore non-consumable and active subscription purchases. - /// See: https://openiap.dev/docs/apis/restore-purchases + /// + /// Restore non-consumable and active subscription purchases. + /// See: https://openiap.dev/docs/apis/restore-purchases + /// Task RestorePurchasesAsync(); - /// Show Google's mandatory information dialog before a developer-rendered, - /// in-app Billing Choice screen. - /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/show-billing-program-information-dialog-android + /// + /// Show Google's mandatory information dialog before a developer-rendered, + /// in-app Billing Choice screen. + /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/show-billing-program-information-dialog-android + /// Task ShowBillingProgramInformationDialogAndroidAsync(BillingProgramInformationDialogParamsAndroid @params); - /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). - /// Call this after a deliberate customer interaction before linking out to external purchases. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) - /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios - /// Parameter noticeType: Notice type determining the style of disclosure + /// + /// Present the disclosure sheet required before linking out via ExternalPurchaseCustomLink (iOS 18.1+). + /// Call this after a deliberate customer interaction before linking out to external purchases. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/shownotice(type:) + /// See: https://openiap.dev/docs/apis/ios/show-external-purchase-custom-link-notice-ios + /// Parameter noticeType: Notice type determining the style of disclosure + /// Task ShowExternalPurchaseCustomLinkNoticeIOSAsync(ExternalPurchaseCustomLinkNoticeTypeIOS noticeType); - /// Overlay Play billing in-app messages, such as payment issues or subscription price-change confirmations. - /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 - /// (upstream API available since Play Billing 4.1.0). - /// Returns a response code and, when the subscription status changes, the related purchase token. - /// Throws OpenIapError.NotPrepared if billing client not ready. - /// See: https://openiap.dev/docs/apis/android/show-in-app-messages-android + /// + /// Overlay Play billing in-app messages, such as payment issues or subscription price-change confirmations. + /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 + /// (upstream API available since Play Billing 4.1.0). + /// Returns a response code and, when the subscription status changes, the related purchase token. + /// Throws OpenIapError.NotPrepared if billing client not ready. + /// See: https://openiap.dev/docs/apis/android/show-in-app-messages-android + /// Task ShowInAppMessagesAndroidAsync(InAppMessageParamsAndroid? @params = null); - /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// + /// Present the manage-subscriptions sheet and return changed purchases (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/show-manage-subscriptions-ios + /// Task> ShowManageSubscriptionsIOSAsync(); - /// Force sync transactions with the App Store (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/sync-ios + /// + /// Force sync transactions with the App Store (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/sync-ios + /// Task SyncIOSAsync(); - /// Verify a purchase against your own backend. Returns a platform-specific - /// variant of VerifyPurchaseResult — VerifyPurchaseResultIOS exposes isValid - /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store - /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. - /// Inspect the concrete variant before reading fields. - /// See: https://openiap.dev/docs/features/validation#verify-purchase + /// + /// Verify a purchase against your own backend. Returns a platform-specific + /// variant of VerifyPurchaseResult — VerifyPurchaseResultIOS exposes isValid + /// + receipt/JWS metadata, VerifyPurchaseResultAndroid carries Play Store + /// receipt fields (no isValid), and VerifyPurchaseResultHorizon uses success. + /// Inspect the concrete variant before reading fields. + /// See: https://openiap.dev/docs/features/validation#verify-purchase + /// Task VerifyPurchaseAsync(VerifyPurchaseProps options); - /// Verify via a managed provider without standing up your own server. The - /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform - /// availability may differ by implementation. - /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider + /// + /// Verify via a managed provider without standing up your own server. The + /// PurchaseVerificationProvider enum currently exposes only IAPKit; platform + /// availability may differ by implementation. + /// See: https://openiap.dev/docs/features/validation#verify-purchase-with-provider + /// Task VerifyPurchaseWithProviderAsync(VerifyPurchaseWithProviderProps options); } /// GraphQL root query operations. public interface QueryResolver { - /// Check eligibility for the external purchase notice sheet (iOS 17.4+). - /// Uses ExternalPurchase.canPresent. - /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// + /// Check eligibility for the external purchase notice sheet (iOS 17.4+). + /// Uses ExternalPurchase.canPresent. + /// See: https://openiap.dev/docs/apis/ios/can-present-external-purchase-notice-ios + /// Task CanPresentExternalPurchaseNoticeIOSAsync(); - /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios + /// + /// Get the user's current entitlement for a product, using StoreKit 2 (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/current-entitlement-ios + /// Task CurrentEntitlementIOSAsync(string sku); - /// Fetch products or subscriptions from the store. - /// See: https://openiap.dev/docs/apis/fetch-products + /// + /// Fetch products or subscriptions from the store. + /// See: https://openiap.dev/docs/apis/fetch-products + /// Task FetchProductsAsync(ProductRequest @params); - /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). - /// See: https://openiap.dev/docs/apis/get-active-subscriptions + /// + /// Get details of all currently active subscriptions (filters by subscriptionIds when provided). + /// See: https://openiap.dev/docs/apis/get-active-subscriptions + /// Task> GetActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); - /// List every StoreKit transaction (finished + unfinished) for the current user. - /// Requires the SKIncludeConsumableInAppPurchaseHistory Info.plist key in the host app - /// for finished consumables to be included (iOS 18+). - /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. - /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios + /// + /// List every StoreKit transaction (finished + unfinished) for the current user. + /// Requires the SKIncludeConsumableInAppPurchaseHistory Info.plist key in the host app + /// for finished consumables to be included (iOS 18+). + /// Unlike getAvailablePurchases, always returns the iOS-specific PurchaseIOS shape. + /// See: https://openiap.dev/docs/apis/ios/get-all-transactions-ios + /// Task> GetAllTransactionsIOSAsync(); - /// Fetch the app transaction (iOS 16+). - /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios + /// + /// Fetch the app transaction (iOS 16+). + /// See: https://openiap.dev/docs/apis/ios/get-app-transaction-ios + /// Task GetAppTransactionIOSAsync(); - /// List active purchases for the current user. - /// See: https://openiap.dev/docs/apis/get-available-purchases + /// + /// List active purchases for the current user. + /// See: https://openiap.dev/docs/apis/get-available-purchases + /// Task> GetAvailablePurchasesAsync(PurchaseOptions? options = null); - /// Fetch Play Billing assets and loyalty text for developer-rendered Billing Choice screens. - /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). - /// Throws OpenIapError.NotPrepared if billing client is not ready. - /// See: https://openiap.dev/docs/apis/android/get-billing-choice-info-android + /// + /// Fetch Play Billing assets and loyalty text for developer-rendered Billing Choice screens. + /// OpenIAP availability: Spec 2.1.0 / openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Throws OpenIapError.NotPrepared if billing client is not ready. + /// See: https://openiap.dev/docs/apis/android/get-billing-choice-info-android + /// Task GetBillingChoiceInfoAndroidAsync(GetBillingChoiceInfoParamsAndroid @params); - /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). - /// Use this token to report transactions made through ExternalPurchaseCustomLink. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) - /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios - /// Parameter tokenType: Token type: acquisition (new customers) or services (existing customers) + /// + /// Fetch a token for Apple's External Purchase Server reporting API (iOS 18.1+). + /// Use this token to report transactions made through ExternalPurchaseCustomLink. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/token(for:) + /// See: https://openiap.dev/docs/apis/ios/get-external-purchase-custom-link-token-ios + /// Parameter tokenType: Token type: acquisition (new customers) or services (existing customers) + /// Task GetExternalPurchaseCustomLinkTokenIOSAsync(ExternalPurchaseCustomLinkTokenTypeIOS tokenType); - /// List unfinished StoreKit transactions in the queue. - /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// + /// List unfinished StoreKit transactions in the queue. + /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios + /// Task> GetPendingTransactionsIOSAsync(); - /// Read the App Store-promoted product, if any (iOS 15+). - /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the - /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an - /// externally redeemed win-back offer, OpenIAP preserves it for the next - /// matching requestPurchase unless the caller supplies an explicit win-back or - /// promotional offer. - /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios + /// + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. + /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios + /// Task GetPromotedProductIOSAsync(); - /// Get base64-encoded receipt data (legacy validation). - /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios + /// + /// Get base64-encoded receipt data (legacy validation). + /// See: https://openiap.dev/docs/apis/ios/get-receipt-data-ios + /// Task GetReceiptDataIOSAsync(); - /// Return the store-authoritative country code: ISO 3166-1 alpha-3 on Apple - /// platforms and alpha-2 on Android. The operation fails when the store cannot - /// provide a value; implementations must not synthesize a locale fallback. - /// See: https://openiap.dev/docs/apis/get-storefront + /// + /// Return the store-authoritative country code: ISO 3166-1 alpha-3 on Apple + /// platforms and alpha-2 on Android. The operation fails when the store cannot + /// provide a value; implementations must not synthesize a locale fallback. + /// See: https://openiap.dev/docs/apis/get-storefront + /// Task GetStorefrontAsync(); - /// Return the JWS string for a transaction (StoreKit 2). - /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// + /// Return the JWS string for a transaction (StoreKit 2). + /// See: https://openiap.dev/docs/apis/ios/get-transaction-jws-ios + /// Task GetTransactionJwsIOSAsync(string sku); - /// Check whether the user has any active subscription. - /// See: https://openiap.dev/docs/apis/has-active-subscriptions + /// + /// Check whether the user has any active subscription. + /// See: https://openiap.dev/docs/apis/has-active-subscriptions + /// Task HasActiveSubscriptionsAsync(IReadOnlyList? subscriptionIds = null); - /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). - /// Returns true if the app can use custom external purchase links. - /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible - /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// + /// Check eligibility for the custom-link variant of external purchase (iOS 18.1+). + /// Returns true if the app can use custom external purchase links. + /// Reference: https://developer.apple.com/documentation/storekit/externalpurchasecustomlink/iseligible + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios + /// Task IsEligibleForExternalPurchaseCustomLinkIOSAsync(); - /// Check intro-offer eligibility for a subscription group. - /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// + /// Check intro-offer eligibility for a subscription group. + /// See: https://openiap.dev/docs/apis/ios/is-eligible-for-intro-offer-ios + /// Task IsEligibleForIntroOfferIOSAsync(string groupId); - /// Check whether a transaction's JWS verification passed (StoreKit 2). - /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// + /// Check whether a transaction's JWS verification passed (StoreKit 2). + /// See: https://openiap.dev/docs/apis/ios/is-transaction-verified-ios + /// Task IsTransactionVerifiedIOSAsync(string sku); - /// Get the latest verified transaction for a product, using StoreKit 2. - /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios + /// + /// Get the latest verified transaction for a product, using StoreKit 2. + /// See: https://openiap.dev/docs/apis/ios/latest-transaction-ios + /// Task LatestTransactionIOSAsync(string sku); - /// Get subscription status objects from StoreKit 2 (iOS 15+). - /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios + /// + /// Get subscription status objects from StoreKit 2 (iOS 15+). + /// See: https://openiap.dev/docs/apis/ios/subscription-status-ios + /// Task> SubscriptionStatusIOSAsync(string sku); } /// GraphQL root subscription operations. public interface SubscriptionResolver { - /// Fires when a user selects developer billing in an External Payments or - /// Billing Choice flow (Android only). The payload can contain an external - /// transaction token, link URI, original transaction ID, and selected products. - /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / - /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// + /// Fires when a user selects developer billing in an External Payments or + /// Billing Choice flow (Android only). The payload can contain an external + /// transaction token, link URI, original transaction ID, and selected products. + /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / + /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). + /// Task DeveloperProvidedBillingAndroidAsync(); - /// Fires when the App Store surfaces a promoted product (iOS only). - /// A win-back offer attached to PurchaseIntent is preserved for the next - /// matching requestPurchase unless the caller supplies an explicit win-back or - /// promotional offer. + /// + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. + /// Task PromotedProductIOSAsync(); /// Fires when a purchase fails or is cancelled Task PurchaseErrorAsync(); - /// Fires when a purchase completes successfully or a pending purchase resolves - /// Options can opt iOS listeners into duplicate StoreKit transaction replays - /// for diagnostics; default listeners receive one event per transaction ID - /// during a single connection session. + /// + /// Fires when a purchase completes successfully or a pending purchase resolves + /// Options can opt iOS listeners into duplicate StoreKit transaction replays + /// for diagnostics; default listeners receive one event per transaction ID + /// during a single connection session. + /// Task PurchaseUpdatedAsync(PurchaseUpdatedListenerOptions? options = null); - /// Fires when a subscription enters a billing-issue state that needs user action - /// (payment method failed, card expired, etc.). Cross-platform unification: - /// - /// - iOS 16.4+ / Mac Catalyst 16.4+ / visionOS 1.0+: delivered via StoreKit 2 - /// `Message.Reason.billingIssue`. - /// - Android (Play flavor, Billing 8.1+): emitted when `isSuspended == true` is first detected - /// on a previously healthy subscription. Requires Google Play Billing Library 8.1.0 or newer. - /// - Android (Horizon flavor): NOT emitted. The Horizon Billing Compatibility SDK implements - /// the Play Billing 7.0 API surface which does not expose a suspended-subscription signal. - /// - Android (Amazon flavor): NOT emitted. Amazon Appstore IAP does not expose an - /// equivalent subscription billing-issue signal. - /// - /// Listeners should not assume the event will fire on every store. Direct users to the - /// platform subscription management UI (`deepLinkToSubscriptions`) to resolve the issue. + /// + /// Fires when a subscription enters a billing-issue state that needs user action + /// (payment method failed, card expired, etc.). Cross-platform unification: + /// + /// - iOS 16.4+ / Mac Catalyst 16.4+ / visionOS 1.0+: delivered via StoreKit 2 + /// `Message.Reason.billingIssue`. + /// - Android (Play flavor, Billing 8.1+): emitted when `isSuspended == true` is first detected + /// on a previously healthy subscription. Requires Google Play Billing Library 8.1.0 or newer. + /// - Android (Horizon flavor): NOT emitted. The Horizon Billing Compatibility SDK implements + /// the Play Billing 7.0 API surface which does not expose a suspended-subscription signal. + /// - Android (Amazon flavor): NOT emitted. Amazon Appstore IAP does not expose an + /// equivalent subscription billing-issue signal. + /// + /// Listeners should not assume the event will fire on every store. Direct users to the + /// platform subscription management UI (`deepLinkToSubscriptions`) to resolve the issue. + /// Task SubscriptionBillingIssueAsync(); - /// Fires when a user selects alternative billing in the User Choice Billing dialog (Android only) - /// Only triggered when the user selects alternative billing instead of Google Play billing + /// + /// Fires when a user selects alternative billing in the User Choice Billing dialog (Android only) + /// Only triggered when the user selects alternative billing instead of Google Play billing + /// Task UserChoiceBillingAndroidAsync(); }