= []
if let quantity = props.quantity, quantity > 1 {
options.insert(.quantity(quantity))
@@ -377,6 +377,90 @@ enum StoreKitTypesBridge {
}
options.insert(option)
}
+ // Win-back offers (iOS 18+)
+ // Used to re-engage churned subscribers
+ if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
+ if let winBackInput = props.winBackOffer {
+ guard let product = product else {
+ OpenIapLog.error("❌ Win-back offer requires product context")
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "Win-back offer requires product context. Fetch the product before calling requestPurchase."
+ )
+ }
+ // Find the win-back offer from the product's promotional offers
+ if let subscription = product.subscription {
+ let winBackOffer = subscription.promotionalOffers.first { offer in
+ offer.id == winBackInput.offerId && offer.type == .winBack
+ }
+ if let offer = winBackOffer {
+ options.insert(.winBackOffer(offer))
+ OpenIapLog.debug("✅ Added win-back offer: \(winBackInput.offerId)")
+ } else {
+ OpenIapLog.error("❌ Win-back offer not found: \(winBackInput.offerId)")
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "Win-back offer not found: \(winBackInput.offerId). Ensure the user is eligible and the offer ID is correct."
+ )
+ }
+ } else {
+ OpenIapLog.error("❌ Win-back offer requires a subscription product")
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "Win-back offers can only be applied to subscription products"
+ )
+ }
+ }
+ } else if props.winBackOffer != nil {
+ // Fail fast when win-back offers are used on unsupported OS versions
+ OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+")
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+."
+ )
+ }
+ // JWS Promotional Offer (iOS 15+, WWDC 2025)
+ // New signature format using compact JWS string for promotional offers
+ // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile
+ if let jwsOffer = props.promotionalOfferJWS {
+ #if swift(>=6.1)
+ // Swift 6.1+ implementation
+ options.insert(.promotionalOffer(jwsOffer.jws))
+ OpenIapLog.debug("✅ Added JWS promotional offer: \(jwsOffer.offerId)")
+ #else
+ // Swift < 6.1: API not available, throw error to fail fast
+ OpenIapLog.error("❌ JWS promotional offers require Xcode 16.4+ / Swift 6.1+")
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "JWS promotional offers require Xcode 16.4+ / Swift 6.1+. Use withOffer with signature-based promotional offers instead."
+ )
+ #endif
+ }
+
+ // Introductory Offer Eligibility Override (iOS 15+, WWDC 2025)
+ // Allows overriding the system's eligibility check for introductory offers
+ // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile
+ if let eligibility = props.introductoryOfferEligibility {
+ #if swift(>=6.1)
+ // Swift 6.1+ implementation
+ options.insert(.introductoryOfferEligibility(eligibility))
+ OpenIapLog.debug("✅ Added introductory offer eligibility override: \(eligibility)")
+ #else
+ // Swift < 6.1: API not available, throw error to fail fast
+ OpenIapLog.error("❌ Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+")
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+. The system will determine eligibility automatically."
+ )
+ #endif
+ }
+
// Advanced Commerce Data (iOS 15+)
// Used with StoreKit 2's Product.PurchaseOption.custom API for passing
// campaign tokens, affiliate IDs, or other attribution data
diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift
index 656e0b20..f176fbf0 100644
--- a/packages/apple/Sources/Models/Types.swift
+++ b/packages/apple/Sources/Models/Types.swift
@@ -299,6 +299,21 @@ public enum ProductQueryType: String, Codable, CaseIterable {
case all = "all"
}
+/// 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+
+public enum ProductStatusAndroid: String, Codable, CaseIterable {
+ /// Product was successfully fetched
+ case ok = "ok"
+ /// Product not found - the SKU doesn't exist in the Play Console
+ case notFound = "not-found"
+ /// No offers available for the user - product exists but user is not eligible for any offers
+ case noOffersAvailable = "no-offers-available"
+ /// Unknown error occurred while fetching the product
+ case unknown = "unknown"
+}
+
public enum ProductType: String, Codable, CaseIterable {
case inApp = "in-app"
case subs = "subs"
@@ -321,9 +336,23 @@ public enum PurchaseVerificationProvider: String, Codable, CaseIterable {
case iapkit = "iapkit"
}
+/// Sub-response codes for more granular purchase error information (Android)
+/// Available in Google Play Billing Library 8.0.0+
+public enum SubResponseCodeAndroid: String, Codable, CaseIterable {
+ /// No specific sub-response code applies
+ case noApplicableSubResponseCode = "no-applicable-sub-response-code"
+ /// User's payment method has insufficient funds
+ case paymentDeclinedDueToInsufficientFunds = "payment-declined-due-to-insufficient-funds"
+ /// User doesn't meet subscription offer eligibility requirements
+ case userIneligible = "user-ineligible"
+}
+
public enum SubscriptionOfferTypeIOS: String, Codable, CaseIterable {
case introductory = "introductory"
case promotional = "promotional"
+ /// Win-back offer type (iOS 18+)
+ /// Used to re-engage churned subscribers with a discount or free trial.
+ case winBack = "win-back"
}
public enum SubscriptionPeriodIOS: String, Codable, CaseIterable {
@@ -463,6 +492,18 @@ public struct BillingProgramReportingDetailsAndroid: Codable {
public var externalTransactionToken: String
}
+/// Extended billing result with sub-response code (Android)
+/// Available in Google Play Billing Library 8.0.0+
+public struct BillingResultAndroid: Codable {
+ /// Debug message from the billing library
+ public var debugMessage: String?
+ /// The response code from the billing operation
+ public var responseCode: Int
+ /// Sub-response code for more granular error information (8.0+).
+ /// Provides additional context when responseCode indicates an error.
+ public var subResponseCode: SubResponseCodeAndroid?
+}
+
/// Details provided when user selects developer billing option (Android)
/// Received via DeveloperProvidedBillingListener callback
/// Available in Google Play Billing Library 8.3.0+
@@ -668,6 +709,12 @@ public struct ProductAndroid: Codable, ProductCommon {
public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]?
public var platform: IapPlatform = .android
public var price: Double?
+ /// 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+
+ public var productStatusAndroid: ProductStatusAndroid?
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]?
/// Standardized subscription offers.
@@ -751,6 +798,12 @@ public struct ProductSubscriptionAndroid: Codable, ProductCommon {
public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]?
public var platform: IapPlatform = .android
public var price: Double?
+ /// 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+
+ public var productStatusAndroid: ProductStatusAndroid?
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]
/// Standardized subscription offers.
@@ -1280,19 +1333,47 @@ public struct ProductRequest: Codable {
}
}
+/// 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 struct PromotionalOfferJWSInputIOS: Codable {
+ /// Compact JWS string signed by your server.
+ /// The JWS should contain the promotional offer signature data.
+ /// Format: header.payload.signature (base64url encoded)
+ public var jws: String
+ /// The promotional offer identifier from App Store Connect
+ public var offerId: String
+
+ public init(
+ jws: String,
+ offerId: String
+ ) {
+ self.jws = jws
+ self.offerId = offerId
+ }
+}
+
public typealias PurchaseInput = Purchase
public struct PurchaseOptions: Codable {
/// Also emit results through the iOS event listeners
public var alsoPublishToEventListenerIOS: Bool?
+ /// 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)
+ public var includeSuspendedAndroid: Bool?
/// Limit to currently active items on iOS
public var onlyIncludeActiveItemsIOS: Bool?
public init(
alsoPublishToEventListenerIOS: Bool? = nil,
+ includeSuspendedAndroid: Bool? = nil,
onlyIncludeActiveItemsIOS: Bool? = nil
) {
self.alsoPublishToEventListenerIOS = alsoPublishToEventListenerIOS
+ self.includeSuspendedAndroid = includeSuspendedAndroid
self.onlyIncludeActiveItemsIOS = onlyIncludeActiveItemsIOS
}
}
@@ -1336,10 +1417,23 @@ public struct RequestPurchaseIosProps: Codable {
public var andDangerouslyFinishTransactionAutomatically: Bool?
/// App account token for user tracking
public var appAccountToken: String?
+ /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ /// Set to true to indicate the user is eligible for introductory offer,
+ /// or false to indicate they are not. When nil, the system determines eligibility.
+ /// Back-deployed to iOS 15.
+ public var introductoryOfferEligibility: Bool?
+ /// JWS promotional offer (iOS 15+, WWDC 2025).
+ /// New signature format using compact JWS string for promotional offers.
+ /// Back-deployed to iOS 15.
+ public var promotionalOfferJWS: PromotionalOfferJWSInputIOS?
/// Purchase quantity
public var quantity: Int?
/// Product SKU
public var sku: String
+ /// Win-back offer to apply (iOS 18+)
+ /// Used to re-engage churned subscribers with a discount or free trial.
+ /// Note: Win-back offers only apply to subscription products.
+ public var winBackOffer: WinBackOfferInputIOS?
/// Discount offer to apply
public var withOffer: DiscountOfferInputIOS?
@@ -1347,15 +1441,21 @@ public struct RequestPurchaseIosProps: Codable {
advancedCommerceData: String? = nil,
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
+ introductoryOfferEligibility: Bool? = nil,
+ promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
sku: String,
+ winBackOffer: WinBackOfferInputIOS? = nil,
withOffer: DiscountOfferInputIOS? = nil
) {
self.advancedCommerceData = advancedCommerceData
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
+ self.introductoryOfferEligibility = introductoryOfferEligibility
+ self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
self.sku = sku
+ self.winBackOffer = winBackOffer
self.withOffer = withOffer
}
}
@@ -1514,23 +1614,43 @@ public struct RequestSubscriptionIosProps: Codable {
public var advancedCommerceData: String?
public var andDangerouslyFinishTransactionAutomatically: Bool?
public var appAccountToken: String?
+ /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ /// Set to true to indicate the user is eligible for introductory offer,
+ /// or false to indicate they are not. When nil, the system determines eligibility.
+ /// Back-deployed to iOS 15.
+ public var introductoryOfferEligibility: Bool?
+ /// JWS promotional offer (iOS 15+, WWDC 2025).
+ /// New signature format using compact JWS string for promotional offers.
+ /// Back-deployed to iOS 15.
+ public var promotionalOfferJWS: PromotionalOfferJWSInputIOS?
public var quantity: Int?
public var sku: String
+ /// 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.
+ public var winBackOffer: WinBackOfferInputIOS?
public var withOffer: DiscountOfferInputIOS?
public init(
advancedCommerceData: String? = nil,
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
+ introductoryOfferEligibility: Bool? = nil,
+ promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
sku: String,
+ winBackOffer: WinBackOfferInputIOS? = nil,
withOffer: DiscountOfferInputIOS? = nil
) {
self.advancedCommerceData = advancedCommerceData
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
+ self.introductoryOfferEligibility = introductoryOfferEligibility
+ self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
self.sku = sku
+ self.winBackOffer = winBackOffer
self.withOffer = withOffer
}
}
@@ -1735,6 +1855,21 @@ public struct VerifyPurchaseWithProviderProps: Codable {
}
}
+/// 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 struct WinBackOfferInputIOS: Codable {
+ /// The win-back offer ID from App Store Connect
+ public var offerId: String
+
+ public init(
+ offerId: String
+ ) {
+ self.offerId = offerId
+ }
+}
+
// MARK: - Unions
public enum Product: Codable, ProductCommon {
diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift
index d91fa5f8..2741bcd2 100644
--- a/packages/apple/Sources/OpenIapModule.swift
+++ b/packages/apple/Sources/OpenIapModule.swift
@@ -218,7 +218,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
let iosProps = try resolveIosPurchaseProps(from: params)
let sku = iosProps.sku
let product = try await storeProduct(for: sku)
- let options = try StoreKitTypesBridge.purchaseOptions(from: iosProps)
+ let options = try StoreKitTypesBridge.purchaseOptions(from: iosProps, product: product)
// Check if subscription is already owned before attempting purchase
// This prevents iOS from showing "You're already subscribed" alert
@@ -1275,8 +1275,11 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
advancedCommerceData: ios.advancedCommerceData,
andDangerouslyFinishTransactionAutomatically: ios.andDangerouslyFinishTransactionAutomatically,
appAccountToken: ios.appAccountToken,
+ introductoryOfferEligibility: ios.introductoryOfferEligibility,
+ promotionalOfferJWS: ios.promotionalOfferJWS,
quantity: ios.quantity,
sku: ios.sku,
+ winBackOffer: ios.winBackOffer,
withOffer: ios.withOffer
)
}
diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt
index bda0fb02..4b8d40cf 100644
--- a/packages/docs/public/llms-full.txt
+++ b/packages/docs/public/llms-full.txt
@@ -3,7 +3,7 @@
> OpenIAP: Unified in-app purchase specification for iOS & Android
> Documentation: https://openiap.dev
> Quick Reference: https://openiap.dev/llms.txt
-> Generated: 2026-01-18T10:51:44.321Z
+> Generated: 2026-01-18T13:00:35.102Z
## Table of Contents
1. Installation
@@ -430,13 +430,25 @@ await endConnection();
# Google Play Billing Library API Reference
-> Reference documentation for Google Play Billing Library 7.x
+> Reference documentation for Google Play Billing Library 8.x
> Adapt all patterns to match OpenIAP internal conventions.
## Overview
Google Play Billing Library enables in-app purchases and subscriptions on Android devices.
+## Version History
+
+| Version | Release Date | Key Features |
+|---------|--------------|--------------|
+| 8.0 | 2025-06-30 | Auto-reconnect, product-level status codes, one-time products with multiple offers, sub-response codes |
+| 8.1 | 2025-11-06 | Suspended subscriptions (`isSuspended`), `includeSuspended` parameter, pre-order details, product-level subscription replacement, `KEEP_EXISTING` mode |
+| 8.2 | 2025-12-09 | Billing Programs API (external content links, external offers), deprecates old External Offers API |
+| 8.2.1 | 2025-12-15 | Bug fix for `isBillingProgramAvailableAsync()` and `createBillingProgramReportingDetailsAsync()` |
+| 8.3 | 2025-12-23 | External Payments program (Japan only), developer billing options |
+
+**Current Version**: 8.3.0 (as of January 2026)
+
## Core Classes
### BillingClient
@@ -447,9 +459,24 @@ The main interface for communicating with Google Play Billing.
val billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
+ // New in 8.0: Auto-reconnect on service disconnect
+ .enableAutoServiceReconnection()
+ .build()
+```
+
+### Auto Service Reconnection (8.0+)
+
+```kotlin
+// Enables automatic reconnection when service disconnects
+BillingClient.newBuilder(context)
+ .enableAutoServiceReconnection()
.build()
```
+When enabled, the library automatically re-establishes the connection if an API call is made while disconnected. This reduces `SERVICE_DISCONNECTED` errors.
+
+> **OpenIAP Note**: Auto-reconnection is **always enabled** internally since OpenIAP uses Billing Library 8.3.0+. No configuration needed.
+
### Connection Management
```kotlin
@@ -667,13 +694,126 @@ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
- `PRICE_CHANGE_CONFIRMATION` - Price change confirmation
- `PRODUCT_DETAILS` - Product details API
+## Product-Level Status Codes (8.0+)
+
+In Billing Library 8.0+, `queryProductDetailsAsync()` returns products that couldn't be fetched with a status code explaining why.
+
+```kotlin
+billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
+ productDetailsList.forEach { productDetails ->
+ when (productDetails.productStatus) {
+ ProductDetails.ProductStatus.OK -> {
+ // Product fetched successfully
+ }
+ ProductDetails.ProductStatus.NOT_FOUND -> {
+ // SKU doesn't exist in Play Console
+ }
+ ProductDetails.ProductStatus.NO_OFFERS_AVAILABLE -> {
+ // User not eligible for any offers
+ }
+ }
+ }
+}
+```
+
+| Status | Description |
+|--------|-------------|
+| `OK` | Product fetched successfully |
+| `NOT_FOUND` | SKU doesn't exist in Play Console |
+| `NO_OFFERS_AVAILABLE` | User not eligible for any offers |
+
+## Suspended Subscriptions (8.1+)
+
+```kotlin
+val purchase: Purchase
+
+// Check if subscription is suspended due to billing issue
+if (purchase.isSuspended) {
+ // User's payment method failed
+ // Do NOT grant entitlements
+ // Direct user to subscription center to fix payment
+}
+```
+
+### Query Suspended Subscriptions (8.1+)
+
+```kotlin
+// Include suspended subscriptions in query results
+val params = QueryPurchasesParams.newBuilder()
+ .setProductType(BillingClient.ProductType.SUBS)
+ .setIncludeSuspended(true) // New in 8.1
+ .build()
+
+billingClient.queryPurchasesAsync(params) { billingResult, purchases ->
+ purchases.forEach { purchase ->
+ if (purchase.isSuspended) {
+ // Handle suspended subscription
+ }
+ }
+}
+```
+
+> **OpenIAP Note**: Use `includeSuspendedAndroid: true` in `PurchaseOptions` when calling `getAvailablePurchases()`. The `isSuspendedAndroid` field on purchases indicates suspension status.
+
+## Sub-Response Codes (8.0+)
+
+`BillingResult` includes a sub-response code for more granular error information:
+
+```kotlin
+val result = billingClient.launchBillingFlow(activity, params)
+when (result.subResponseCode) {
+ BillingResult.SUB_RESPONSE_CODE_INSUFFICIENT_FUNDS -> {
+ // User's payment method has insufficient funds
+ }
+ BillingResult.SUB_RESPONSE_CODE_USER_INELIGIBLE -> {
+ // User doesn't meet offer eligibility requirements
+ }
+}
+```
+
+| Sub-Response Code | Description |
+|-------------------|-------------|
+| `PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS` | User's payment method has insufficient funds |
+| `USER_INELIGIBLE` | User doesn't meet subscription offer eligibility |
+| `NO_APPLICABLE_SUB_RESPONSE_CODE` | No specific sub-code applies |
+
+## Subscription Product Replacement (8.1+)
+
+Product-level replacement parameters for subscription upgrades/downgrades:
+
+```kotlin
+val replacementParams = SubscriptionProductReplacementParams.newBuilder()
+ .setOldProductId("old_subscription_id")
+ .setReplacementMode(ReplacementMode.WITH_TIME_PRORATION)
+ .build()
+
+val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
+ .setProductDetails(newProductDetails)
+ .setOfferToken(offerToken)
+ .setSubscriptionProductReplacementParams(replacementParams) // New in 8.1
+ .build()
+```
+
+### Replacement Modes
+
+| Mode | Description |
+|------|-------------|
+| `WITH_TIME_PRORATION` | Immediate, expiration time prorated |
+| `CHARGE_PRORATED_PRICE` | Immediate, same billing cycle |
+| `CHARGE_FULL_PRICE` | Immediate, full price charged |
+| `WITHOUT_PRORATION` | Takes effect on old plan expiration |
+| `DEFERRED` | Deferred, no charge |
+| `KEEP_EXISTING` | Keep existing payment schedule (8.1+) |
+
## Best Practices
1. **Always acknowledge purchases** within 3 days or they will be refunded
2. **Verify purchases server-side** using Google Play Developer API
3. **Handle pending purchases** for payment methods that require additional steps
-4. **Reconnect on disconnect** - billing service can disconnect anytime
-5. **Cache product details** to avoid repeated queries
+4. **Auto-reconnect is enabled by default** in OpenIAP (8.0+)
+5. **Check product status codes** (8.0+) to understand why products weren't fetched
+6. **Check isSuspended** (8.1+) before granting entitlements
+7. **Cache product details** to avoid repeated queries
---
@@ -681,7 +821,7 @@ if (result.responseCode == BillingClient.BillingResponseCode.OK) {
# Meta Horizon IAP API Reference
> External reference for Meta Horizon Store in-app purchase APIs.
-> Source: https://developers.meta.com/horizon/documentation/
+> Source: [Meta Horizon Documentation](https://developers.meta.com/horizon/documentation/)
## Overview
@@ -694,12 +834,17 @@ Meta Horizon provides IAP functionality for Quest VR applications. There are two
| Library | Version | Compatible With |
|---------|---------|-----------------|
-| horizon-billing-compatibility | 1.1.1 | Google Play Billing **7.0** API |
-| Google Play Billing (Play flavor) | 8.3.0 | N/A |
-| react-native-iap | v14+ | Billing 7.0+ |
-| expo-iap | latest | Billing 7.0+ |
+| horizon-billing-compatibility | **1.1.1** (latest) | Google Play Billing **7.0** API |
+| Google Play Billing (Play flavor) | **8.3.0** (latest) | N/A |
+| react-native-iap | v14+ | Billing 7.0+, RN 0.79+, Kotlin 2.0+ |
+| expo-iap | latest | Billing 7.0+, Kotlin 2.0+ |
+
+**CRITICAL**: Horizon Billing Compatibility SDK implements Google Play Billing **7.0** API surface, NOT 8.x.
-**IMPORTANT**: Horizon Billing Compatibility SDK implements Google Play Billing **7.0** API surface, NOT 8.x. When writing shared code for both Play and Horizon flavors, use only APIs that exist in both Billing 7.0 and 8.x.
+When writing shared code for both Play and Horizon flavors:
+- Use only APIs that exist in **both** Billing 7.0 and 8.x
+- Horizon SDK does NOT support Billing 8.x features like auto-reconnect, product status codes, or `includeSuspended`
+- OpenIAP handles this automatically with flavor-specific implementations
### APIs Available in Both (Safe to use in shared code)
@@ -712,9 +857,15 @@ Meta Horizon provides IAP functionality for Quest VR applications. There are two
### APIs Only in Billing 8.x (DO NOT use in shared code)
-- `enableAutoServiceReconnection()` - Auto reconnect feature
-- Product-level status codes in `queryProductDetailsAsync()` response
-- One-time products with multiple offers
+- `enableAutoServiceReconnection()` - Auto reconnect feature (8.0+)
+- Product-level status codes in `queryProductDetailsAsync()` response (8.0+)
+- One-time products with multiple offers (8.0+)
+- Sub-response codes in `BillingResult` (8.0+)
+- `isSuspended` on Purchase (8.1+)
+- `includeSuspended` parameter in `QueryPurchasesParams` (8.1+)
+- `SubscriptionProductReplacementParams` (8.1+)
+- Billing Programs API (`isBillingProgramAvailableAsync`, etc.) (8.2+)
+- External Payments / Developer Billing Options (8.3+)
## Billing Compatibility SDK
@@ -764,7 +915,8 @@ Access token format: `OC|App_ID|App_Secret`
Verify that a user owns an item (app or add-on).
**Endpoint:**
-```
+
+```http
POST https://graph.oculus.com/$APP_ID/verify_entitlement
```
@@ -772,7 +924,7 @@ POST https://graph.oculus.com/$APP_ID/verify_entitlement
| Parameter | Type | Description |
|-----------|------|-------------|
-| `access_token` | string | `OC|App_ID|App_Secret` format |
+| `access_token` | string | `OC\|App_ID\|App_Secret` format |
| `user_id` | string | The user ID to verify |
| `sku` | string | (Optional) SKU for add-on verification |
@@ -803,7 +955,8 @@ curl -d "access_token=OC|$APP_ID|$APP_SECRET" \
Refund a DURABLE or CONSUMABLE entitlement (not yet consumed).
**Endpoint:**
-```
+
+```http
POST https://graph.oculus.com/$APP_ID/refund_iap_entitlement
```
@@ -811,7 +964,7 @@ POST https://graph.oculus.com/$APP_ID/refund_iap_entitlement
| Parameter | Type | Description |
|-----------|------|-------------|
-| `access_token` | string | `OC|App_ID|App_Secret` format |
+| `access_token` | string | `OC\|App_ID\|App_Secret` format |
| `user_id` | string | The user ID |
| `sku` | string | SKU of item to refund |
@@ -1349,6 +1502,30 @@ export default withIAPContext(Store);
This document provides external API reference for Apple's StoreKit 2 framework.
+## iOS 18+ Features
+
+| Feature | iOS Version | Description |
+|---------|-------------|-------------|
+| Win-back offers | iOS 18.0 | Re-engage churned subscribers |
+| Consumable transaction history | iOS 18.0 | History includes finished consumables |
+| Billing issue messages | iOS 18.0 | Automatic billing issue notifications via StoreKit Message |
+| UI context for purchases | iOS 18.2 | Required for proper payment sheet display |
+| External purchase notice | iOS 18.2 | `presentExternalPurchaseNoticeSheetIOS` |
+| `appTransactionID` | iOS 18.4 | Globally unique app transaction identifier (back-deployed to iOS 15) |
+| `originalPlatform` | iOS 18.4 | Original purchase platform (back-deployed to iOS 15) |
+| `Offer.Period` | iOS 18.4 | Offer period information |
+| `advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data |
+| Expanded offer codes | iOS 18.4 | For consumables/non-consumables |
+| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format |
+| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option |
+
+### WWDC 2025 Updates
+
+- **SubscriptionStatus by Transaction ID**: Query subscription status using any transaction ID
+- **JWS-based promotional offers**: New `promotionalOffer` purchase option with compact JWS string
+- **Introductory offer eligibility**: Override eligibility check with `introductoryOfferEligibility` purchase option
+- Both new purchase options are back-deployed to iOS 15
+
## Product
A type that describes an in-app purchase product.
@@ -1456,6 +1633,124 @@ static func beginRefundRequest(for transactionID: UInt64, in scene: UIWindowScen
Begins a refund request for a transaction.
+## Win-Back Offers (iOS 18+)
+
+Win-back offers are a new offer type to re-engage churned subscribers.
+
+### Automatic Presentation
+
+StoreKit Message automatically presents win-back offers when a user is eligible:
+
+```swift
+// Message reason for win-back offers
+StoreKit.Message.Reason.winBackOffer
+```
+
+### Manual Application
+
+Apply a win-back offer during purchase:
+
+```swift
+let product: Product
+let winBackOffer: Product.SubscriptionOffer
+
+let result = try await product.purchase(options: [
+ .winBackOffer(winBackOffer)
+])
+```
+
+### Checking Eligibility
+
+```swift
+// Win-back offers are available in subscription.promotionalOffers
+// with type == .winBack
+let winBackOffers = product.subscription?.promotionalOffers.filter {
+ $0.type == .winBack
+}
+```
+
+### RenewalInfo
+
+Win-back offer information is available in renewal info:
+
+```swift
+let renewalInfo: Product.SubscriptionInfo.RenewalInfo
+
+// Check if win-back offer is applied to next renewal
+if renewalInfo.renewalOfferType == .winBack {
+ // Win-back offer will be applied
+}
+```
+
+## UI Context for Purchases (iOS 18.2+)
+
+Beginning in iOS 18.2, purchase methods require a UI context to properly display payment sheets:
+
+```swift
+// iOS/iPadOS/tvOS/visionOS: UIViewController
+let result = try await product.purchase(confirmIn: viewController)
+
+// macOS: NSWindow
+let result = try await product.purchase(confirmIn: window)
+
+// watchOS: No UI context required
+```
+
+> **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene.
+
+## AppTransaction Updates (iOS 18.4+)
+
+```swift
+let appTransaction = try await AppTransaction.shared
+
+// New in iOS 18.4 (back-deployed to iOS 15)
+let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account
+let originalPlatform = appTransaction.originalPlatform // Original purchase platform
+```
+
+### appTransactionID
+
+- Globally unique identifier for each Apple Account that downloads your app
+- Remains consistent across redownloads, refunds, repurchases, and storefront changes
+- Works with Family Sharing (each family member gets unique ID)
+- Back-deployed to iOS 15
+
+## Advanced Commerce API (iOS 18.4+)
+
+For apps with large product catalogs:
+
+```swift
+// Check if product has advanced commerce info
+if let advancedInfo = product.advancedCommerceInfo {
+ // Handle large catalog monetization
+}
+```
+
+## External Purchase Support (iOS 18.2+)
+
+### Present External Purchase Notice
+
+```swift
+// Check if external purchase notice can be presented
+if await ExternalPurchase.canPresent {
+ let result = try await ExternalPurchase.presentNoticeSheet()
+ switch result {
+ case .continue:
+ // User wants to continue to external purchase
+ case .dismissed:
+ // User dismissed the notice
+ }
+}
+```
+
+### Present External Purchase Link
+
+```swift
+let result = try await ExternalPurchase.open(url: externalURL)
+```
+
+> **OpenIAP Note**: `presentExternalPurchaseNoticeSheetIOS` and `presentExternalPurchaseLinkIOS` are available in the iOS package.
+
---
diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt
index 8c4ce8da..3e545b9a 100644
--- a/packages/docs/public/llms.txt
+++ b/packages/docs/public/llms.txt
@@ -3,7 +3,7 @@
> OpenIAP: Unified in-app purchase specification for iOS & Android
> Documentation: https://openiap.dev
> Full Reference: https://openiap.dev/llms-full.txt
-> Generated: 2026-01-18T10:51:44.321Z
+> Generated: 2026-01-18T13:00:35.102Z
## Installation
diff --git a/packages/docs/src/pages/docs/types/offer.tsx b/packages/docs/src/pages/docs/types/offer.tsx
index 8063bb65..0871b6fb 100644
--- a/packages/docs/src/pages/docs/types/offer.tsx
+++ b/packages/docs/src/pages/docs/types/offer.tsx
@@ -125,7 +125,7 @@ function TypesOffer() {
DiscountOfferType!
|
- Type of offer (Introductory, Promotional, OneTime) |
+ Type of offer: Introductory, Promotional, WinBack (iOS 18+), or OneTime |
@@ -268,6 +268,7 @@ function TypesOffer() {
enum DiscountOfferType {
Introductory = 'Introductory',
Promotional = 'Promotional',
+ WinBack = 'WinBack', // iOS 18+
OneTime = 'OneTime',
}`}
),
@@ -296,6 +297,7 @@ enum DiscountOfferType {
enum DiscountOfferType: String, Codable {
case introductory = "Introductory"
case promotional = "Promotional"
+ case winBack = "WinBack" // iOS 18+
case oneTime = "OneTime"
}`}
),
@@ -324,6 +326,7 @@ enum DiscountOfferType: String, Codable {
enum class DiscountOfferType {
Introductory,
Promotional,
+ WinBack, // iOS 18+
OneTime
}`}
),
@@ -370,6 +373,7 @@ enum class DiscountOfferType {
enum DiscountOfferType {
introductory,
promotional,
+ winBack, // iOS 18+
oneTime,
}`}
),
@@ -398,6 +402,7 @@ var rental_details_android: RentalDetailsAndroid
enum DiscountOfferType {
INTRODUCTORY,
PROMOTIONAL,
+ WIN_BACK, # iOS 18+
ONE_TIME
}`}
),
@@ -470,7 +475,7 @@ enum DiscountOfferType {
DiscountOfferType!
|
- Introductory or Promotional |
+ Introductory, Promotional, or WinBack (iOS 18+) |
diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx
index 3e808696..5bba51a9 100644
--- a/packages/docs/src/pages/docs/types/product.tsx
+++ b/packages/docs/src/pages/docs/types/product.tsx
@@ -250,6 +250,25 @@ function TypesProduct() {
offerToken, pricingPhases
|
+
+
+ productStatusAndroid
+ |
+
+ Product fetch status code. Values: OK (success),{' '}
+ NOT_FOUND (SKU doesn't exist),{' '}
+ NO_OFFERS_AVAILABLE (user not eligible for any offers),{' '}
+ UNKNOWN.
+ Requires{' '}
+
+ Billing Library 8.0+
+
+ |
+
>
diff --git a/packages/docs/src/pages/docs/updates/notes.tsx b/packages/docs/src/pages/docs/updates/notes.tsx
index 48f29599..0f315d6a 100644
--- a/packages/docs/src/pages/docs/updates/notes.tsx
+++ b/packages/docs/src/pages/docs/updates/notes.tsx
@@ -26,6 +26,91 @@ function Notes() {
useScrollToHash();
const allNotes: Note[] = [
+ // GQL 1.3.13 / Google 1.3.24 / Apple 1.3.11 - Jan 18, 2026
+ {
+ id: 'gql-1-3-13-google-1-3-24-apple-1-3-11',
+ date: new Date('2026-01-18'),
+ element: (
+
+
+ 📅 openiap-gql v1.3.13 / openiap-google v1.3.24 / openiap-apple v1.3.11 - Platform API Gap Analysis
+
+
+
iOS - Win-Back Offers (iOS 18+):
+
+ Added support for win-back offers to re-engage churned subscribers.
+
+
+ winBackOffer - New field in RequestPurchaseIosProps and RequestSubscriptionIosProps
+ WinBackOfferInputIOS - Input type with offerId field
+ SubscriptionOfferTypeIOS.WinBack - New enum value
+
+
+{`// Apply win-back offer to subscription purchase
+requestSubscription({
+ sku: 'premium_monthly',
+ winBackOffer: { offerId: 'winback_50_off' }
+});`}
+
+
+
iOS - JWS Promotional Offers (iOS 15+, WWDC 2025):
+
+ New signature format using compact JWS string for promotional offers. Back-deployed to iOS 15.
+
+
+ promotionalOfferJWS - New field in purchase props
+ PromotionalOfferJWSInputIOS - Input type with offerId and jws fields
+
+
+ Note: Requires Xcode 16.4+ to compile. Falls back to legacy signature-based offers until then.
+
+
+
iOS - Introductory Offer Eligibility Override (iOS 15+, WWDC 2025):
+
+ introductoryOfferEligibility - Override system eligibility check for intro offers
+ - Set
true to indicate eligible, false for not eligible, nil for system default
+
+
+ Note: Requires Xcode 16.4+ to compile. System determines eligibility automatically until then.
+
+
+
+
+
Android - Product Status Codes (Billing 8.0+):
+
+ Product-level status codes indicating why products couldn't be fetched.
+
+
+ ProductStatusAndroid - New enum with values: Ok, NotFound, NoOffersAvailable, Unknown
+ productStatusAndroid - New field on ProductAndroid and ProductSubscriptionAndroid
+
+
+{`// Check product fetch status
+val product = fetchProducts(skus).firstOrNull()
+when (product?.productStatusAndroid) {
+ ProductStatusAndroid.Ok -> { /* Success */ }
+ ProductStatusAndroid.NotFound -> { /* SKU doesn't exist */ }
+ ProductStatusAndroid.NoOffersAvailable -> { /* User not eligible */ }
+ ProductStatusAndroid.Unknown -> { /* Unknown status */ }
+ null -> { /* No product or status */ }
+}`}
+
+
+
Android - Auto Service Reconnection:
+
+ enableAutoServiceReconnection() is now always enabled internally since OpenIAP uses Billing Library 8.3.0+.
+ No configuration needed - the library automatically re-establishes connection if disconnected.
+
+
+
References:
+
+
+ ),
+ },
// GQL 1.3.12 / Google 1.3.22 / Apple 1.3.10 - Jan 17, 2026
{
id: 'gql-1-3-12-google-1-3-22-apple-1-3-10',
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 cf3db7b4..b319f92f 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
@@ -608,6 +608,47 @@ public enum class ProductQueryType(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * 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+
+ */
+public enum class ProductStatusAndroid(val rawValue: String) {
+ /**
+ * Product was successfully fetched
+ */
+ Ok("ok"),
+ /**
+ * Product not found - the SKU doesn't exist in the Play Console
+ */
+ NotFound("not-found"),
+ /**
+ * No offers available for the user - product exists but user is not eligible for any offers
+ */
+ NoOffersAvailable("no-offers-available"),
+ /**
+ * Unknown error occurred while fetching the product
+ */
+ Unknown("unknown");
+
+ companion object {
+ fun fromJson(value: String): ProductStatusAndroid = when (value) {
+ "ok" -> ProductStatusAndroid.Ok
+ "Ok" -> ProductStatusAndroid.Ok
+ "not-found" -> ProductStatusAndroid.NotFound
+ "NotFound" -> ProductStatusAndroid.NotFound
+ "no-offers-available" -> ProductStatusAndroid.NoOffersAvailable
+ "NoOffersAvailable" -> ProductStatusAndroid.NoOffersAvailable
+ "unknown" -> ProductStatusAndroid.Unknown
+ "Unknown" -> ProductStatusAndroid.Unknown
+ else -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class ProductType(val rawValue: String) {
InApp("in-app"),
Subs("subs");
@@ -682,9 +723,47 @@ public enum class PurchaseVerificationProvider(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Sub-response codes for more granular purchase error information (Android)
+ * Available in Google Play Billing Library 8.0.0+
+ */
+public enum class SubResponseCodeAndroid(val rawValue: String) {
+ /**
+ * No specific sub-response code applies
+ */
+ NoApplicableSubResponseCode("no-applicable-sub-response-code"),
+ /**
+ * User's payment method has insufficient funds
+ */
+ PaymentDeclinedDueToInsufficientFunds("payment-declined-due-to-insufficient-funds"),
+ /**
+ * User doesn't meet subscription offer eligibility requirements
+ */
+ UserIneligible("user-ineligible");
+
+ companion object {
+ fun fromJson(value: String): SubResponseCodeAndroid = when (value) {
+ "no-applicable-sub-response-code" -> SubResponseCodeAndroid.NoApplicableSubResponseCode
+ "NoApplicableSubResponseCode" -> SubResponseCodeAndroid.NoApplicableSubResponseCode
+ "payment-declined-due-to-insufficient-funds" -> SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds
+ "PaymentDeclinedDueToInsufficientFunds" -> SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds
+ "user-ineligible" -> SubResponseCodeAndroid.UserIneligible
+ "UserIneligible" -> SubResponseCodeAndroid.UserIneligible
+ else -> throw IllegalArgumentException("Unknown SubResponseCodeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
Introductory("introductory"),
- Promotional("promotional");
+ Promotional("promotional"),
+ /**
+ * Win-back offer type (iOS 18+)
+ * Used to re-engage churned subscribers with a discount or free trial.
+ */
+ WinBack("win-back");
companion object {
fun fromJson(value: String): SubscriptionOfferTypeIOS = when (value) {
@@ -692,6 +771,8 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
"Introductory" -> SubscriptionOfferTypeIOS.Introductory
"promotional" -> SubscriptionOfferTypeIOS.Promotional
"Promotional" -> SubscriptionOfferTypeIOS.Promotional
+ "win-back" -> SubscriptionOfferTypeIOS.WinBack
+ "WinBack" -> SubscriptionOfferTypeIOS.WinBack
else -> throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS value: $value")
}
}
@@ -1048,6 +1129,44 @@ public data class BillingProgramReportingDetailsAndroid(
)
}
+/**
+ * Extended billing result with sub-response code (Android)
+ * Available in Google Play Billing Library 8.0.0+
+ */
+public data class BillingResultAndroid(
+ /**
+ * Debug message from the billing library
+ */
+ val debugMessage: String? = null,
+ /**
+ * The response code from the billing operation
+ */
+ val responseCode: Int,
+ /**
+ * Sub-response code for more granular error information (8.0+).
+ * Provides additional context when responseCode indicates an error.
+ */
+ val subResponseCode: SubResponseCodeAndroid? = null
+) {
+
+ companion object {
+ fun fromJson(json: Map): BillingResultAndroid {
+ return BillingResultAndroid(
+ debugMessage = json["debugMessage"] as? String,
+ responseCode = (json["responseCode"] as? Number)?.toInt() ?: 0,
+ subResponseCode = (json["subResponseCode"] as? String)?.let { SubResponseCodeAndroid.fromJson(it) },
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "BillingResultAndroid",
+ "debugMessage" to debugMessage,
+ "responseCode" to responseCode,
+ "subResponseCode" to subResponseCode?.toJson(),
+ )
+}
+
/**
* Details provided when user selects developer billing option (Android)
* Received via DeveloperProvidedBillingListener callback
@@ -1639,6 +1758,14 @@ public data class ProductAndroid(
val oneTimePurchaseOfferDetailsAndroid: List? = null,
override val platform: IapPlatform = IapPlatform.Android,
override val price: Double? = null,
+ /**
+ * 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+
+ */
+ val productStatusAndroid: ProductStatusAndroid? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -1667,6 +1794,7 @@ public data class ProductAndroid(
oneTimePurchaseOfferDetailsAndroid = (json["oneTimePurchaseOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductAndroidOneTimePurchaseOfferDetail.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductAndroidOneTimePurchaseOfferDetail") },
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) },
subscriptionOfferDetailsAndroid = (json["subscriptionOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductSubscriptionAndroidOfferDetails.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductSubscriptionAndroidOfferDetails") },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
title = json["title"] as? String ?: "",
@@ -1688,6 +1816,7 @@ public data class ProductAndroid(
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseOfferDetailsAndroid?.map { it.toJson() },
"platform" to platform.toJson(),
"price" to price,
+ "productStatusAndroid" to productStatusAndroid?.toJson(),
"subscriptionOfferDetailsAndroid" to subscriptionOfferDetailsAndroid?.map { it.toJson() },
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
"title" to title,
@@ -1876,6 +2005,14 @@ public data class ProductSubscriptionAndroid(
val oneTimePurchaseOfferDetailsAndroid: List? = null,
override val platform: IapPlatform = IapPlatform.Android,
override val price: Double? = null,
+ /**
+ * 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+
+ */
+ val productStatusAndroid: ProductStatusAndroid? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -1904,6 +2041,7 @@ public data class ProductSubscriptionAndroid(
oneTimePurchaseOfferDetailsAndroid = (json["oneTimePurchaseOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductAndroidOneTimePurchaseOfferDetail.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductAndroidOneTimePurchaseOfferDetail") },
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) },
subscriptionOfferDetailsAndroid = (json["subscriptionOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductSubscriptionAndroidOfferDetails.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductSubscriptionAndroidOfferDetails") } ?: emptyList(),
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(),
title = json["title"] as? String ?: "",
@@ -1925,6 +2063,7 @@ public data class ProductSubscriptionAndroid(
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseOfferDetailsAndroid?.map { it.toJson() },
"platform" to platform.toJson(),
"price" to price,
+ "productStatusAndroid" to productStatusAndroid?.toJson(),
"subscriptionOfferDetailsAndroid" to subscriptionOfferDetailsAndroid.map { it.toJson() },
"subscriptionOffers" to subscriptionOffers.map { it.toJson() },
"title" to title,
@@ -3258,6 +3397,39 @@ public data class ProductRequest(
)
}
+/**
+ * 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 data class PromotionalOfferJWSInputIOS(
+ /**
+ * Compact JWS string signed by your server.
+ * The JWS should contain the promotional offer signature data.
+ * Format: header.payload.signature (base64url encoded)
+ */
+ val jws: String,
+ /**
+ * The promotional offer identifier from App Store Connect
+ */
+ val offerId: String
+) {
+ companion object {
+ fun fromJson(json: Map): PromotionalOfferJWSInputIOS {
+ return PromotionalOfferJWSInputIOS(
+ jws = json["jws"] as? String ?: "",
+ offerId = json["offerId"] as? String ?: "",
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "jws" to jws,
+ "offerId" to offerId,
+ )
+}
+
public typealias PurchaseInput = Purchase
public data class PurchaseOptions(
@@ -3265,6 +3437,13 @@ public data class PurchaseOptions(
* Also emit results through the iOS event listeners
*/
val alsoPublishToEventListenerIOS: Boolean? = null,
+ /**
+ * 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)
+ */
+ val includeSuspendedAndroid: Boolean? = null,
/**
* Limit to currently active items on iOS
*/
@@ -3274,6 +3453,7 @@ public data class PurchaseOptions(
fun fromJson(json: Map): PurchaseOptions {
return PurchaseOptions(
alsoPublishToEventListenerIOS = json["alsoPublishToEventListenerIOS"] as? Boolean,
+ includeSuspendedAndroid = json["includeSuspendedAndroid"] as? Boolean,
onlyIncludeActiveItemsIOS = json["onlyIncludeActiveItemsIOS"] as? Boolean,
)
}
@@ -3281,6 +3461,7 @@ public data class PurchaseOptions(
fun toJson(): Map = mapOf(
"alsoPublishToEventListenerIOS" to alsoPublishToEventListenerIOS,
+ "includeSuspendedAndroid" to includeSuspendedAndroid,
"onlyIncludeActiveItemsIOS" to onlyIncludeActiveItemsIOS,
)
}
@@ -3346,6 +3527,19 @@ public data class RequestPurchaseIosProps(
* App account token for user tracking
*/
val appAccountToken: String? = null,
+ /**
+ * Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ * Set to true to indicate the user is eligible for introductory offer,
+ * or false to indicate they are not. When nil, the system determines eligibility.
+ * Back-deployed to iOS 15.
+ */
+ val introductoryOfferEligibility: Boolean? = null,
+ /**
+ * JWS promotional offer (iOS 15+, WWDC 2025).
+ * New signature format using compact JWS string for promotional offers.
+ * Back-deployed to iOS 15.
+ */
+ val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null,
/**
* Purchase quantity
*/
@@ -3354,6 +3548,12 @@ public data class RequestPurchaseIosProps(
* Product SKU
*/
val sku: String,
+ /**
+ * Win-back offer to apply (iOS 18+)
+ * Used to re-engage churned subscribers with a discount or free trial.
+ * Note: Win-back offers only apply to subscription products.
+ */
+ val winBackOffer: WinBackOfferInputIOS? = null,
/**
* Discount offer to apply
*/
@@ -3365,8 +3565,11 @@ public data class RequestPurchaseIosProps(
advancedCommerceData = json["advancedCommerceData"] as? String,
andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean,
appAccountToken = json["appAccountToken"] as? String,
+ introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean,
+ promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) },
quantity = (json["quantity"] as? Number)?.toInt(),
sku = json["sku"] as? String ?: "",
+ winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) },
withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) },
)
}
@@ -3376,8 +3579,11 @@ public data class RequestPurchaseIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
+ "winBackOffer" to winBackOffer?.toJson(),
"withOffer" to withOffer?.toJson(),
)
}
@@ -3561,8 +3767,28 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData: String? = null,
val andDangerouslyFinishTransactionAutomatically: Boolean? = null,
val appAccountToken: String? = null,
+ /**
+ * Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ * Set to true to indicate the user is eligible for introductory offer,
+ * or false to indicate they are not. When nil, the system determines eligibility.
+ * Back-deployed to iOS 15.
+ */
+ val introductoryOfferEligibility: Boolean? = null,
+ /**
+ * JWS promotional offer (iOS 15+, WWDC 2025).
+ * New signature format using compact JWS string for promotional offers.
+ * Back-deployed to iOS 15.
+ */
+ val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null,
val quantity: Int? = null,
val sku: String,
+ /**
+ * 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.
+ */
+ val winBackOffer: WinBackOfferInputIOS? = null,
val withOffer: DiscountOfferInputIOS? = null
) {
companion object {
@@ -3571,8 +3797,11 @@ public data class RequestSubscriptionIosProps(
advancedCommerceData = json["advancedCommerceData"] as? String,
andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean,
appAccountToken = json["appAccountToken"] as? String,
+ introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean,
+ promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) },
quantity = (json["quantity"] as? Number)?.toInt(),
sku = json["sku"] as? String ?: "",
+ winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) },
withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) },
)
}
@@ -3582,8 +3811,11 @@ public data class RequestSubscriptionIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
+ "winBackOffer" to winBackOffer?.toJson(),
"withOffer" to withOffer?.toJson(),
)
}
@@ -3908,6 +4140,31 @@ public data class VerifyPurchaseWithProviderProps(
)
}
+/**
+ * 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 data class WinBackOfferInputIOS(
+ /**
+ * The win-back offer ID from App Store Connect
+ */
+ val offerId: String
+) {
+ companion object {
+ fun fromJson(json: Map): WinBackOfferInputIOS {
+ return WinBackOfferInputIOS(
+ offerId = json["offerId"] as? String ?: "",
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "offerId" to offerId,
+ )
+}
+
// MARK: - Unions
public sealed interface Product : ProductCommon {
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
index a59d6b0c..bb33a16c 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
@@ -226,8 +226,11 @@ class OpenIapModule(
}
}
}
- override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { _ ->
- withContext(Dispatchers.IO) { restorePurchasesHelper(billingClient) }
+ override val getAvailablePurchases: QueryGetAvailablePurchasesHandler = { options ->
+ withContext(Dispatchers.IO) {
+ val includeSuspended = options?.includeSuspendedAndroid == true
+ restorePurchasesHelper(billingClient, includeSuspended)
+ }
}
override val getActiveSubscriptions: QueryGetActiveSubscriptionsHandler = { subscriptionIds ->
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
index 457371fe..87f4e406 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
@@ -46,23 +46,43 @@ internal suspend fun onPurchaseError(
continuation.invokeOnCancellation { removeListener(listener) }
}
-internal suspend fun restorePurchases(client: BillingClient?): List {
+internal suspend fun restorePurchases(
+ client: BillingClient?,
+ includeSuspended: Boolean = false
+): List {
if (client == null) return emptyList()
val purchases = mutableListOf()
- purchases += queryPurchases(client, BillingClient.ProductType.INAPP)
- purchases += queryPurchases(client, BillingClient.ProductType.SUBS)
+ purchases += queryPurchases(client, BillingClient.ProductType.INAPP, includeSuspended = false)
+ purchases += queryPurchases(client, BillingClient.ProductType.SUBS, includeSuspended)
return purchases
}
internal suspend fun queryPurchases(
client: BillingClient?,
- productType: String
+ productType: String,
+ includeSuspended: Boolean = false
): List = suspendCancellableCoroutine { continuation ->
val billingClient = client ?: run {
continuation.resume(emptyList())
return@suspendCancellableCoroutine
}
- val params = QueryPurchasesParams.newBuilder().setProductType(productType).build()
+ val paramsBuilder = QueryPurchasesParams.newBuilder().setProductType(productType)
+
+ // Include suspended subscriptions (Google Play Billing Library 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.
+ if (productType == BillingClient.ProductType.SUBS && includeSuspended) {
+ runCatching {
+ // Use reflection to maintain backward compatibility with older billing library versions
+ val setIncludeSuspendedMethod = paramsBuilder::class.java.getMethod(
+ "setIncludeSuspended",
+ Boolean::class.javaPrimitiveType
+ )
+ setIncludeSuspendedMethod.invoke(paramsBuilder, true)
+ }
+ }
+
+ val params = paramsBuilder.build()
billingClient.queryPurchasesAsync(params) { result, purchaseList ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
val mapped = purchaseList.map { billingPurchase ->
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
index c3569b08..25b153b3 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
@@ -15,6 +15,7 @@ import dev.hyo.openiap.Product
import dev.hyo.openiap.ProductAndroid
import dev.hyo.openiap.PreorderDetailsAndroid
import dev.hyo.openiap.ProductAndroidOneTimePurchaseOfferDetail
+import dev.hyo.openiap.ProductStatusAndroid
import dev.hyo.openiap.ProductSubscriptionAndroid
import dev.hyo.openiap.ProductSubscriptionAndroidOfferDetails
import dev.hyo.openiap.ProductType
@@ -32,6 +33,24 @@ import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase as BillingPurchase
internal object BillingConverters {
+ /**
+ * Gets the product status from ProductDetails (Billing Library 8.0+).
+ * Returns null for older billing library versions.
+ */
+ private fun ProductDetails.getProductStatus(): ProductStatusAndroid? {
+ return runCatching {
+ // ProductDetails.productStatus is available in Billing Library 8.0+
+ val statusMethod = this::class.java.getMethod("getProductStatus")
+ val status = statusMethod.invoke(this) as? Int
+ when (status) {
+ 0 -> ProductStatusAndroid.Ok // ProductDetails.ProductStatus.OK
+ 1 -> ProductStatusAndroid.NotFound // ProductDetails.ProductStatus.NOT_FOUND
+ 2 -> ProductStatusAndroid.NoOffersAvailable // ProductDetails.ProductStatus.NO_OFFERS_AVAILABLE
+ else -> ProductStatusAndroid.Unknown
+ }
+ }.getOrNull()
+ }
+
/**
* Converts a ProductDetails.OneTimePurchaseOfferDetails to ProductAndroidOneTimePurchaseOfferDetail
* This includes all discount-related fields available in Billing Library 7.0+
@@ -266,6 +285,7 @@ internal object BillingConverters {
oneTimePurchaseOfferDetailsAndroid = offerDetailsList,
platform = IapPlatform.Android,
price = priceAmountMicros.toDouble() / 1_000_000.0,
+ productStatusAndroid = getProductStatus(),
subscriptionOfferDetailsAndroid = null,
subscriptionOffers = null,
title = title,
@@ -331,6 +351,7 @@ internal object BillingConverters {
oneTimePurchaseOfferDetailsAndroid = oneTimeOfferDetailsList,
platform = IapPlatform.Android,
price = firstPhase?.priceAmountMicros?.toDouble()?.div(1_000_000.0),
+ productStatusAndroid = getProductStatus(),
subscriptionOfferDetailsAndroid = pricingDetails,
subscriptionOffers = subscriptionOffers,
title = title,
diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt
index b0f91981..3a0d4987 100644
--- a/packages/gql/src/generated/Types.kt
+++ b/packages/gql/src/generated/Types.kt
@@ -668,6 +668,47 @@ public enum class ProductQueryType(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * 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+
+ */
+public enum class ProductStatusAndroid(val rawValue: String) {
+ /**
+ * Product was successfully fetched
+ */
+ Ok("ok"),
+ /**
+ * Product not found - the SKU doesn't exist in the Play Console
+ */
+ NotFound("not-found"),
+ /**
+ * No offers available for the user - product exists but user is not eligible for any offers
+ */
+ NoOffersAvailable("no-offers-available"),
+ /**
+ * Unknown error occurred while fetching the product
+ */
+ Unknown("unknown")
+
+ companion object {
+ fun fromJson(value: String): ProductStatusAndroid = when (value) {
+ "ok" -> ProductStatusAndroid.Ok
+ "OK" -> ProductStatusAndroid.Ok
+ "not-found" -> ProductStatusAndroid.NotFound
+ "NOT_FOUND" -> ProductStatusAndroid.NotFound
+ "no-offers-available" -> ProductStatusAndroid.NoOffersAvailable
+ "NO_OFFERS_AVAILABLE" -> ProductStatusAndroid.NoOffersAvailable
+ "unknown" -> ProductStatusAndroid.Unknown
+ "UNKNOWN" -> ProductStatusAndroid.Unknown
+ else -> throw IllegalArgumentException("Unknown ProductStatusAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class ProductType(val rawValue: String) {
InApp("in-app"),
Subs("subs")
@@ -752,9 +793,47 @@ public enum class PurchaseVerificationProvider(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Sub-response codes for more granular purchase error information (Android)
+ * Available in Google Play Billing Library 8.0.0+
+ */
+public enum class SubResponseCodeAndroid(val rawValue: String) {
+ /**
+ * No specific sub-response code applies
+ */
+ NoApplicableSubResponseCode("no-applicable-sub-response-code"),
+ /**
+ * User's payment method has insufficient funds
+ */
+ PaymentDeclinedDueToInsufficientFunds("payment-declined-due-to-insufficient-funds"),
+ /**
+ * User doesn't meet subscription offer eligibility requirements
+ */
+ UserIneligible("user-ineligible")
+
+ companion object {
+ fun fromJson(value: String): SubResponseCodeAndroid = when (value) {
+ "no-applicable-sub-response-code" -> SubResponseCodeAndroid.NoApplicableSubResponseCode
+ "NO_APPLICABLE_SUB_RESPONSE_CODE" -> SubResponseCodeAndroid.NoApplicableSubResponseCode
+ "payment-declined-due-to-insufficient-funds" -> SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds
+ "PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS" -> SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds
+ "user-ineligible" -> SubResponseCodeAndroid.UserIneligible
+ "USER_INELIGIBLE" -> SubResponseCodeAndroid.UserIneligible
+ else -> throw IllegalArgumentException("Unknown SubResponseCodeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
Introductory("introductory"),
- Promotional("promotional")
+ Promotional("promotional"),
+ /**
+ * Win-back offer type (iOS 18+)
+ * Used to re-engage churned subscribers with a discount or free trial.
+ */
+ WinBack("win-back")
companion object {
fun fromJson(value: String): SubscriptionOfferTypeIOS = when (value) {
@@ -764,6 +843,9 @@ public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
"promotional" -> SubscriptionOfferTypeIOS.Promotional
"PROMOTIONAL" -> SubscriptionOfferTypeIOS.Promotional
"Promotional" -> SubscriptionOfferTypeIOS.Promotional
+ "win-back" -> SubscriptionOfferTypeIOS.WinBack
+ "WIN_BACK" -> SubscriptionOfferTypeIOS.WinBack
+ "WinBack" -> SubscriptionOfferTypeIOS.WinBack
else -> throw IllegalArgumentException("Unknown SubscriptionOfferTypeIOS value: $value")
}
}
@@ -1130,6 +1212,44 @@ public data class BillingProgramReportingDetailsAndroid(
)
}
+/**
+ * Extended billing result with sub-response code (Android)
+ * Available in Google Play Billing Library 8.0.0+
+ */
+public data class BillingResultAndroid(
+ /**
+ * Debug message from the billing library
+ */
+ val debugMessage: String? = null,
+ /**
+ * The response code from the billing operation
+ */
+ val responseCode: Int,
+ /**
+ * Sub-response code for more granular error information (8.0+).
+ * Provides additional context when responseCode indicates an error.
+ */
+ val subResponseCode: SubResponseCodeAndroid? = null
+) {
+
+ companion object {
+ fun fromJson(json: Map): BillingResultAndroid {
+ return BillingResultAndroid(
+ debugMessage = json["debugMessage"] as? String,
+ responseCode = (json["responseCode"] as? Number)?.toInt() ?: 0,
+ subResponseCode = (json["subResponseCode"] as? String)?.let { SubResponseCodeAndroid.fromJson(it) },
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "BillingResultAndroid",
+ "debugMessage" to debugMessage,
+ "responseCode" to responseCode,
+ "subResponseCode" to subResponseCode?.toJson(),
+ )
+}
+
/**
* Details provided when user selects developer billing option (Android)
* Received via DeveloperProvidedBillingListener callback
@@ -1721,6 +1841,14 @@ public data class ProductAndroid(
val oneTimePurchaseOfferDetailsAndroid: List? = null,
override val platform: IapPlatform = IapPlatform.Android,
override val price: Double? = null,
+ /**
+ * 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+
+ */
+ val productStatusAndroid: ProductStatusAndroid? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -1749,6 +1877,7 @@ public data class ProductAndroid(
oneTimePurchaseOfferDetailsAndroid = (json["oneTimePurchaseOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductAndroidOneTimePurchaseOfferDetail.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductAndroidOneTimePurchaseOfferDetail") },
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) },
subscriptionOfferDetailsAndroid = (json["subscriptionOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductSubscriptionAndroidOfferDetails.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductSubscriptionAndroidOfferDetails") },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
title = json["title"] as? String ?: "",
@@ -1770,6 +1899,7 @@ public data class ProductAndroid(
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseOfferDetailsAndroid?.map { it.toJson() },
"platform" to platform.toJson(),
"price" to price,
+ "productStatusAndroid" to productStatusAndroid?.toJson(),
"subscriptionOfferDetailsAndroid" to subscriptionOfferDetailsAndroid?.map { it.toJson() },
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
"title" to title,
@@ -1958,6 +2088,14 @@ public data class ProductSubscriptionAndroid(
val oneTimePurchaseOfferDetailsAndroid: List? = null,
override val platform: IapPlatform = IapPlatform.Android,
override val price: Double? = null,
+ /**
+ * 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+
+ */
+ val productStatusAndroid: ProductStatusAndroid? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -1986,6 +2124,7 @@ public data class ProductSubscriptionAndroid(
oneTimePurchaseOfferDetailsAndroid = (json["oneTimePurchaseOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductAndroidOneTimePurchaseOfferDetail.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductAndroidOneTimePurchaseOfferDetail") },
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ productStatusAndroid = (json["productStatusAndroid"] as? String)?.let { ProductStatusAndroid.fromJson(it) },
subscriptionOfferDetailsAndroid = (json["subscriptionOfferDetailsAndroid"] as? List<*>)?.mapNotNull { (it as? Map)?.let { ProductSubscriptionAndroidOfferDetails.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for ProductSubscriptionAndroidOfferDetails") } ?: emptyList(),
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") } ?: emptyList(),
title = json["title"] as? String ?: "",
@@ -2007,6 +2146,7 @@ public data class ProductSubscriptionAndroid(
"oneTimePurchaseOfferDetailsAndroid" to oneTimePurchaseOfferDetailsAndroid?.map { it.toJson() },
"platform" to platform.toJson(),
"price" to price,
+ "productStatusAndroid" to productStatusAndroid?.toJson(),
"subscriptionOfferDetailsAndroid" to subscriptionOfferDetailsAndroid.map { it.toJson() },
"subscriptionOffers" to subscriptionOffers.map { it.toJson() },
"title" to title,
@@ -3340,6 +3480,39 @@ public data class ProductRequest(
)
}
+/**
+ * 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 data class PromotionalOfferJWSInputIOS(
+ /**
+ * Compact JWS string signed by your server.
+ * The JWS should contain the promotional offer signature data.
+ * Format: header.payload.signature (base64url encoded)
+ */
+ val jws: String,
+ /**
+ * The promotional offer identifier from App Store Connect
+ */
+ val offerId: String
+) {
+ companion object {
+ fun fromJson(json: Map): PromotionalOfferJWSInputIOS {
+ return PromotionalOfferJWSInputIOS(
+ jws = json["jws"] as? String ?: "",
+ offerId = json["offerId"] as? String ?: "",
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "jws" to jws,
+ "offerId" to offerId,
+ )
+}
+
public typealias PurchaseInput = Purchase
public data class PurchaseOptions(
@@ -3347,6 +3520,13 @@ public data class PurchaseOptions(
* Also emit results through the iOS event listeners
*/
val alsoPublishToEventListenerIOS: Boolean? = null,
+ /**
+ * 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)
+ */
+ val includeSuspendedAndroid: Boolean? = null,
/**
* Limit to currently active items on iOS
*/
@@ -3356,6 +3536,7 @@ public data class PurchaseOptions(
fun fromJson(json: Map): PurchaseOptions {
return PurchaseOptions(
alsoPublishToEventListenerIOS = json["alsoPublishToEventListenerIOS"] as? Boolean,
+ includeSuspendedAndroid = json["includeSuspendedAndroid"] as? Boolean,
onlyIncludeActiveItemsIOS = json["onlyIncludeActiveItemsIOS"] as? Boolean,
)
}
@@ -3363,6 +3544,7 @@ public data class PurchaseOptions(
fun toJson(): Map = mapOf(
"alsoPublishToEventListenerIOS" to alsoPublishToEventListenerIOS,
+ "includeSuspendedAndroid" to includeSuspendedAndroid,
"onlyIncludeActiveItemsIOS" to onlyIncludeActiveItemsIOS,
)
}
@@ -3428,6 +3610,19 @@ public data class RequestPurchaseIosProps(
* App account token for user tracking
*/
val appAccountToken: String? = null,
+ /**
+ * Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ * Set to true to indicate the user is eligible for introductory offer,
+ * or false to indicate they are not. When nil, the system determines eligibility.
+ * Back-deployed to iOS 15.
+ */
+ val introductoryOfferEligibility: Boolean? = null,
+ /**
+ * JWS promotional offer (iOS 15+, WWDC 2025).
+ * New signature format using compact JWS string for promotional offers.
+ * Back-deployed to iOS 15.
+ */
+ val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null,
/**
* Purchase quantity
*/
@@ -3436,6 +3631,12 @@ public data class RequestPurchaseIosProps(
* Product SKU
*/
val sku: String,
+ /**
+ * Win-back offer to apply (iOS 18+)
+ * Used to re-engage churned subscribers with a discount or free trial.
+ * Note: Win-back offers only apply to subscription products.
+ */
+ val winBackOffer: WinBackOfferInputIOS? = null,
/**
* Discount offer to apply
*/
@@ -3447,8 +3648,11 @@ public data class RequestPurchaseIosProps(
advancedCommerceData = json["advancedCommerceData"] as? String,
andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean,
appAccountToken = json["appAccountToken"] as? String,
+ introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean,
+ promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) },
quantity = (json["quantity"] as? Number)?.toInt(),
sku = json["sku"] as? String ?: "",
+ winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) },
withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) },
)
}
@@ -3458,8 +3662,11 @@ public data class RequestPurchaseIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
+ "winBackOffer" to winBackOffer?.toJson(),
"withOffer" to withOffer?.toJson(),
)
}
@@ -3643,8 +3850,28 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData: String? = null,
val andDangerouslyFinishTransactionAutomatically: Boolean? = null,
val appAccountToken: String? = null,
+ /**
+ * Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ * Set to true to indicate the user is eligible for introductory offer,
+ * or false to indicate they are not. When nil, the system determines eligibility.
+ * Back-deployed to iOS 15.
+ */
+ val introductoryOfferEligibility: Boolean? = null,
+ /**
+ * JWS promotional offer (iOS 15+, WWDC 2025).
+ * New signature format using compact JWS string for promotional offers.
+ * Back-deployed to iOS 15.
+ */
+ val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null,
val quantity: Int? = null,
val sku: String,
+ /**
+ * 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.
+ */
+ val winBackOffer: WinBackOfferInputIOS? = null,
val withOffer: DiscountOfferInputIOS? = null
) {
companion object {
@@ -3653,8 +3880,11 @@ public data class RequestSubscriptionIosProps(
advancedCommerceData = json["advancedCommerceData"] as? String,
andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean,
appAccountToken = json["appAccountToken"] as? String,
+ introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean,
+ promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) },
quantity = (json["quantity"] as? Number)?.toInt(),
sku = json["sku"] as? String ?: "",
+ winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) },
withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) },
)
}
@@ -3664,8 +3894,11 @@ public data class RequestSubscriptionIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
+ "winBackOffer" to winBackOffer?.toJson(),
"withOffer" to withOffer?.toJson(),
)
}
@@ -3990,6 +4223,31 @@ public data class VerifyPurchaseWithProviderProps(
)
}
+/**
+ * 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 data class WinBackOfferInputIOS(
+ /**
+ * The win-back offer ID from App Store Connect
+ */
+ val offerId: String
+) {
+ companion object {
+ fun fromJson(json: Map): WinBackOfferInputIOS {
+ return WinBackOfferInputIOS(
+ offerId = json["offerId"] as? String ?: "",
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "offerId" to offerId,
+ )
+}
+
// MARK: - Unions
public sealed interface Product : ProductCommon {
diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift
index 656e0b20..f176fbf0 100644
--- a/packages/gql/src/generated/Types.swift
+++ b/packages/gql/src/generated/Types.swift
@@ -299,6 +299,21 @@ public enum ProductQueryType: String, Codable, CaseIterable {
case all = "all"
}
+/// 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+
+public enum ProductStatusAndroid: String, Codable, CaseIterable {
+ /// Product was successfully fetched
+ case ok = "ok"
+ /// Product not found - the SKU doesn't exist in the Play Console
+ case notFound = "not-found"
+ /// No offers available for the user - product exists but user is not eligible for any offers
+ case noOffersAvailable = "no-offers-available"
+ /// Unknown error occurred while fetching the product
+ case unknown = "unknown"
+}
+
public enum ProductType: String, Codable, CaseIterable {
case inApp = "in-app"
case subs = "subs"
@@ -321,9 +336,23 @@ public enum PurchaseVerificationProvider: String, Codable, CaseIterable {
case iapkit = "iapkit"
}
+/// Sub-response codes for more granular purchase error information (Android)
+/// Available in Google Play Billing Library 8.0.0+
+public enum SubResponseCodeAndroid: String, Codable, CaseIterable {
+ /// No specific sub-response code applies
+ case noApplicableSubResponseCode = "no-applicable-sub-response-code"
+ /// User's payment method has insufficient funds
+ case paymentDeclinedDueToInsufficientFunds = "payment-declined-due-to-insufficient-funds"
+ /// User doesn't meet subscription offer eligibility requirements
+ case userIneligible = "user-ineligible"
+}
+
public enum SubscriptionOfferTypeIOS: String, Codable, CaseIterable {
case introductory = "introductory"
case promotional = "promotional"
+ /// Win-back offer type (iOS 18+)
+ /// Used to re-engage churned subscribers with a discount or free trial.
+ case winBack = "win-back"
}
public enum SubscriptionPeriodIOS: String, Codable, CaseIterable {
@@ -463,6 +492,18 @@ public struct BillingProgramReportingDetailsAndroid: Codable {
public var externalTransactionToken: String
}
+/// Extended billing result with sub-response code (Android)
+/// Available in Google Play Billing Library 8.0.0+
+public struct BillingResultAndroid: Codable {
+ /// Debug message from the billing library
+ public var debugMessage: String?
+ /// The response code from the billing operation
+ public var responseCode: Int
+ /// Sub-response code for more granular error information (8.0+).
+ /// Provides additional context when responseCode indicates an error.
+ public var subResponseCode: SubResponseCodeAndroid?
+}
+
/// Details provided when user selects developer billing option (Android)
/// Received via DeveloperProvidedBillingListener callback
/// Available in Google Play Billing Library 8.3.0+
@@ -668,6 +709,12 @@ public struct ProductAndroid: Codable, ProductCommon {
public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]?
public var platform: IapPlatform = .android
public var price: Double?
+ /// 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+
+ public var productStatusAndroid: ProductStatusAndroid?
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]?
/// Standardized subscription offers.
@@ -751,6 +798,12 @@ public struct ProductSubscriptionAndroid: Codable, ProductCommon {
public var oneTimePurchaseOfferDetailsAndroid: [ProductAndroidOneTimePurchaseOfferDetail]?
public var platform: IapPlatform = .android
public var price: Double?
+ /// 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+
+ public var productStatusAndroid: ProductStatusAndroid?
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
public var subscriptionOfferDetailsAndroid: [ProductSubscriptionAndroidOfferDetails]
/// Standardized subscription offers.
@@ -1280,19 +1333,47 @@ public struct ProductRequest: Codable {
}
}
+/// 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 struct PromotionalOfferJWSInputIOS: Codable {
+ /// Compact JWS string signed by your server.
+ /// The JWS should contain the promotional offer signature data.
+ /// Format: header.payload.signature (base64url encoded)
+ public var jws: String
+ /// The promotional offer identifier from App Store Connect
+ public var offerId: String
+
+ public init(
+ jws: String,
+ offerId: String
+ ) {
+ self.jws = jws
+ self.offerId = offerId
+ }
+}
+
public typealias PurchaseInput = Purchase
public struct PurchaseOptions: Codable {
/// Also emit results through the iOS event listeners
public var alsoPublishToEventListenerIOS: Bool?
+ /// 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)
+ public var includeSuspendedAndroid: Bool?
/// Limit to currently active items on iOS
public var onlyIncludeActiveItemsIOS: Bool?
public init(
alsoPublishToEventListenerIOS: Bool? = nil,
+ includeSuspendedAndroid: Bool? = nil,
onlyIncludeActiveItemsIOS: Bool? = nil
) {
self.alsoPublishToEventListenerIOS = alsoPublishToEventListenerIOS
+ self.includeSuspendedAndroid = includeSuspendedAndroid
self.onlyIncludeActiveItemsIOS = onlyIncludeActiveItemsIOS
}
}
@@ -1336,10 +1417,23 @@ public struct RequestPurchaseIosProps: Codable {
public var andDangerouslyFinishTransactionAutomatically: Bool?
/// App account token for user tracking
public var appAccountToken: String?
+ /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ /// Set to true to indicate the user is eligible for introductory offer,
+ /// or false to indicate they are not. When nil, the system determines eligibility.
+ /// Back-deployed to iOS 15.
+ public var introductoryOfferEligibility: Bool?
+ /// JWS promotional offer (iOS 15+, WWDC 2025).
+ /// New signature format using compact JWS string for promotional offers.
+ /// Back-deployed to iOS 15.
+ public var promotionalOfferJWS: PromotionalOfferJWSInputIOS?
/// Purchase quantity
public var quantity: Int?
/// Product SKU
public var sku: String
+ /// Win-back offer to apply (iOS 18+)
+ /// Used to re-engage churned subscribers with a discount or free trial.
+ /// Note: Win-back offers only apply to subscription products.
+ public var winBackOffer: WinBackOfferInputIOS?
/// Discount offer to apply
public var withOffer: DiscountOfferInputIOS?
@@ -1347,15 +1441,21 @@ public struct RequestPurchaseIosProps: Codable {
advancedCommerceData: String? = nil,
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
+ introductoryOfferEligibility: Bool? = nil,
+ promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
sku: String,
+ winBackOffer: WinBackOfferInputIOS? = nil,
withOffer: DiscountOfferInputIOS? = nil
) {
self.advancedCommerceData = advancedCommerceData
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
+ self.introductoryOfferEligibility = introductoryOfferEligibility
+ self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
self.sku = sku
+ self.winBackOffer = winBackOffer
self.withOffer = withOffer
}
}
@@ -1514,23 +1614,43 @@ public struct RequestSubscriptionIosProps: Codable {
public var advancedCommerceData: String?
public var andDangerouslyFinishTransactionAutomatically: Bool?
public var appAccountToken: String?
+ /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ /// Set to true to indicate the user is eligible for introductory offer,
+ /// or false to indicate they are not. When nil, the system determines eligibility.
+ /// Back-deployed to iOS 15.
+ public var introductoryOfferEligibility: Bool?
+ /// JWS promotional offer (iOS 15+, WWDC 2025).
+ /// New signature format using compact JWS string for promotional offers.
+ /// Back-deployed to iOS 15.
+ public var promotionalOfferJWS: PromotionalOfferJWSInputIOS?
public var quantity: Int?
public var sku: String
+ /// 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.
+ public var winBackOffer: WinBackOfferInputIOS?
public var withOffer: DiscountOfferInputIOS?
public init(
advancedCommerceData: String? = nil,
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
+ introductoryOfferEligibility: Bool? = nil,
+ promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
sku: String,
+ winBackOffer: WinBackOfferInputIOS? = nil,
withOffer: DiscountOfferInputIOS? = nil
) {
self.advancedCommerceData = advancedCommerceData
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
+ self.introductoryOfferEligibility = introductoryOfferEligibility
+ self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
self.sku = sku
+ self.winBackOffer = winBackOffer
self.withOffer = withOffer
}
}
@@ -1735,6 +1855,21 @@ public struct VerifyPurchaseWithProviderProps: Codable {
}
}
+/// 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 struct WinBackOfferInputIOS: Codable {
+ /// The win-back offer ID from App Store Connect
+ public var offerId: String
+
+ public init(
+ offerId: String
+ ) {
+ self.offerId = offerId
+ }
+}
+
// MARK: - Unions
public enum Product: Codable, ProductCommon {
diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart
index 7c8c0201..78c49cec 100644
--- a/packages/gql/src/generated/types.dart
+++ b/packages/gql/src/generated/types.dart
@@ -577,6 +577,41 @@ enum ProductQueryType {
String toJson() => 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+
+enum ProductStatusAndroid {
+ /// Product was successfully fetched
+ Ok('ok'),
+ /// Product not found - the SKU doesn't exist in the Play Console
+ NotFound('not-found'),
+ /// No offers available for the user - product exists but user is not eligible for any offers
+ NoOffersAvailable('no-offers-available'),
+ /// Unknown error occurred while fetching the product
+ Unknown('unknown');
+
+ const ProductStatusAndroid(this.value);
+ final String value;
+
+ factory ProductStatusAndroid.fromJson(String value) {
+ final normalized = value.toLowerCase().replaceAll('_', '-');
+ switch (normalized) {
+ case 'ok':
+ return ProductStatusAndroid.Ok;
+ case 'not-found':
+ return ProductStatusAndroid.NotFound;
+ case 'no-offers-available':
+ return ProductStatusAndroid.NoOffersAvailable;
+ case 'unknown':
+ return ProductStatusAndroid.Unknown;
+ }
+ throw ArgumentError('Unknown ProductStatusAndroid value: $value');
+ }
+
+ String toJson() => value;
+}
+
enum ProductType {
InApp('in-app'),
Subs('subs');
@@ -667,9 +702,41 @@ enum PurchaseVerificationProvider {
String toJson() => value;
}
+/// Sub-response codes for more granular purchase error information (Android)
+/// Available in Google Play Billing Library 8.0.0+
+enum SubResponseCodeAndroid {
+ /// No specific sub-response code applies
+ NoApplicableSubResponseCode('no-applicable-sub-response-code'),
+ /// User's payment method has insufficient funds
+ PaymentDeclinedDueToInsufficientFunds('payment-declined-due-to-insufficient-funds'),
+ /// User doesn't meet subscription offer eligibility requirements
+ UserIneligible('user-ineligible');
+
+ const SubResponseCodeAndroid(this.value);
+ final String value;
+
+ factory SubResponseCodeAndroid.fromJson(String value) {
+ final normalized = value.toLowerCase().replaceAll('_', '-');
+ switch (normalized) {
+ case 'no-applicable-sub-response-code':
+ return SubResponseCodeAndroid.NoApplicableSubResponseCode;
+ case 'payment-declined-due-to-insufficient-funds':
+ return SubResponseCodeAndroid.PaymentDeclinedDueToInsufficientFunds;
+ case 'user-ineligible':
+ return SubResponseCodeAndroid.UserIneligible;
+ }
+ throw ArgumentError('Unknown SubResponseCodeAndroid value: $value');
+ }
+
+ String toJson() => value;
+}
+
enum SubscriptionOfferTypeIOS {
Introductory('introductory'),
- Promotional('promotional');
+ Promotional('promotional'),
+ /// Win-back offer type (iOS 18+)
+ /// Used to re-engage churned subscribers with a discount or free trial.
+ WinBack('win-back');
const SubscriptionOfferTypeIOS(this.value);
final String value;
@@ -681,6 +748,8 @@ enum SubscriptionOfferTypeIOS {
return SubscriptionOfferTypeIOS.Introductory;
case 'promotional':
return SubscriptionOfferTypeIOS.Promotional;
+ case 'win-back':
+ return SubscriptionOfferTypeIOS.WinBack;
}
throw ArgumentError('Unknown SubscriptionOfferTypeIOS value: $value');
}
@@ -1044,6 +1113,41 @@ class BillingProgramReportingDetailsAndroid {
}
}
+/// Extended billing result with sub-response code (Android)
+/// Available in Google Play Billing Library 8.0.0+
+class BillingResultAndroid {
+ const BillingResultAndroid({
+ this.debugMessage,
+ required this.responseCode,
+ this.subResponseCode,
+ });
+
+ /// Debug message from the billing library
+ final String? debugMessage;
+ /// The response code from the billing operation
+ final int responseCode;
+ /// Sub-response code for more granular error information (8.0+).
+ /// Provides additional context when responseCode indicates an error.
+ final SubResponseCodeAndroid? subResponseCode;
+
+ factory BillingResultAndroid.fromJson(Map json) {
+ return BillingResultAndroid(
+ debugMessage: json['debugMessage'] as String?,
+ responseCode: json['responseCode'] as int,
+ subResponseCode: json['subResponseCode'] != null ? SubResponseCodeAndroid.fromJson(json['subResponseCode'] as String) : null,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'BillingResultAndroid',
+ 'debugMessage': debugMessage,
+ 'responseCode': responseCode,
+ 'subResponseCode': subResponseCode?.toJson(),
+ };
+ }
+}
+
/// Details provided when user selects developer billing option (Android)
/// Received via DeveloperProvidedBillingListener callback
/// Available in Google Play Billing Library 8.3.0+
@@ -1626,6 +1730,7 @@ class ProductAndroid extends Product implements ProductCommon {
this.oneTimePurchaseOfferDetailsAndroid,
this.platform = IapPlatform.Android,
this.price,
+ this.productStatusAndroid,
this.subscriptionOfferDetailsAndroid,
this.subscriptionOffers,
required this.title,
@@ -1649,6 +1754,12 @@ class ProductAndroid extends Product implements ProductCommon {
final List? oneTimePurchaseOfferDetailsAndroid;
final IapPlatform platform;
final double? price;
+ /// 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+
+ final ProductStatusAndroid? productStatusAndroid;
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
final List? subscriptionOfferDetailsAndroid;
/// Standardized subscription offers.
@@ -1671,6 +1782,7 @@ class ProductAndroid extends Product implements ProductCommon {
oneTimePurchaseOfferDetailsAndroid: (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null ? null : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)!.map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson(e as Map)).toList(),
platform: IapPlatform.fromJson(json['platform'] as String),
price: (json['price'] as num?)?.toDouble(),
+ productStatusAndroid: json['productStatusAndroid'] != null ? ProductStatusAndroid.fromJson(json['productStatusAndroid'] as String) : null,
subscriptionOfferDetailsAndroid: (json['subscriptionOfferDetailsAndroid'] as List?) == null ? null : (json['subscriptionOfferDetailsAndroid'] as List?)!.map((e) => ProductSubscriptionAndroidOfferDetails.fromJson(e as Map)).toList(),
subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
title: json['title'] as String,
@@ -1693,6 +1805,7 @@ class ProductAndroid extends Product implements ProductCommon {
'oneTimePurchaseOfferDetailsAndroid': oneTimePurchaseOfferDetailsAndroid == null ? null : oneTimePurchaseOfferDetailsAndroid!.map((e) => e.toJson()).toList(),
'platform': platform.toJson(),
'price': price,
+ 'productStatusAndroid': productStatusAndroid?.toJson(),
'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid == null ? null : subscriptionOfferDetailsAndroid!.map((e) => e.toJson()).toList(),
'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
'title': title,
@@ -1882,6 +1995,7 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC
this.oneTimePurchaseOfferDetailsAndroid,
this.platform = IapPlatform.Android,
this.price,
+ this.productStatusAndroid,
required this.subscriptionOfferDetailsAndroid,
required this.subscriptionOffers,
required this.title,
@@ -1905,6 +2019,12 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC
final List? oneTimePurchaseOfferDetailsAndroid;
final IapPlatform platform;
final double? price;
+ /// 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+
+ final ProductStatusAndroid? productStatusAndroid;
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
final List subscriptionOfferDetailsAndroid;
/// Standardized subscription offers.
@@ -1927,6 +2047,7 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC
oneTimePurchaseOfferDetailsAndroid: (json['oneTimePurchaseOfferDetailsAndroid'] as List?) == null ? null : (json['oneTimePurchaseOfferDetailsAndroid'] as List?)!.map((e) => ProductAndroidOneTimePurchaseOfferDetail.fromJson(e as Map)).toList(),
platform: IapPlatform.fromJson(json['platform'] as String),
price: (json['price'] as num?)?.toDouble(),
+ productStatusAndroid: json['productStatusAndroid'] != null ? ProductStatusAndroid.fromJson(json['productStatusAndroid'] as String) : null,
subscriptionOfferDetailsAndroid: (json['subscriptionOfferDetailsAndroid'] as List).map((e) => ProductSubscriptionAndroidOfferDetails.fromJson(e as Map)).toList(),
subscriptionOffers: (json['subscriptionOffers'] as List).map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
title: json['title'] as String,
@@ -1949,6 +2070,7 @@ class ProductSubscriptionAndroid extends ProductSubscription implements ProductC
'oneTimePurchaseOfferDetailsAndroid': oneTimePurchaseOfferDetailsAndroid == null ? null : oneTimePurchaseOfferDetailsAndroid!.map((e) => e.toJson()).toList(),
'platform': platform.toJson(),
'price': price,
+ 'productStatusAndroid': productStatusAndroid?.toJson(),
'subscriptionOfferDetailsAndroid': subscriptionOfferDetailsAndroid.map((e) => e.toJson()).toList(),
'subscriptionOffers': subscriptionOffers.map((e) => e.toJson()).toList(),
'title': title,
@@ -3408,22 +3530,61 @@ class ProductRequest {
}
}
+/// 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.
+class PromotionalOfferJWSInputIOS {
+ const PromotionalOfferJWSInputIOS({
+ required this.jws,
+ required this.offerId,
+ });
+
+ /// Compact JWS string signed by your server.
+ /// The JWS should contain the promotional offer signature data.
+ /// Format: header.payload.signature (base64url encoded)
+ final String jws;
+ /// The promotional offer identifier from App Store Connect
+ final String offerId;
+
+ factory PromotionalOfferJWSInputIOS.fromJson(Map json) {
+ return PromotionalOfferJWSInputIOS(
+ jws: json['jws'] as String,
+ offerId: json['offerId'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'jws': jws,
+ 'offerId': offerId,
+ };
+ }
+}
+
typedef PurchaseInput = Purchase;
class PurchaseOptions {
const PurchaseOptions({
this.alsoPublishToEventListenerIOS,
+ this.includeSuspendedAndroid,
this.onlyIncludeActiveItemsIOS,
});
/// Also emit results through the iOS event listeners
final bool? alsoPublishToEventListenerIOS;
+ /// 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)
+ final bool? includeSuspendedAndroid;
/// Limit to currently active items on iOS
final bool? onlyIncludeActiveItemsIOS;
factory PurchaseOptions.fromJson(Map json) {
return PurchaseOptions(
alsoPublishToEventListenerIOS: json['alsoPublishToEventListenerIOS'] as bool?,
+ includeSuspendedAndroid: json['includeSuspendedAndroid'] as bool?,
onlyIncludeActiveItemsIOS: json['onlyIncludeActiveItemsIOS'] as bool?,
);
}
@@ -3431,6 +3592,7 @@ class PurchaseOptions {
Map toJson() {
return {
'alsoPublishToEventListenerIOS': alsoPublishToEventListenerIOS,
+ 'includeSuspendedAndroid': includeSuspendedAndroid,
'onlyIncludeActiveItemsIOS': onlyIncludeActiveItemsIOS,
};
}
@@ -3484,8 +3646,11 @@ class RequestPurchaseIosProps {
this.advancedCommerceData,
this.andDangerouslyFinishTransactionAutomatically,
this.appAccountToken,
+ this.introductoryOfferEligibility,
+ this.promotionalOfferJWS,
this.quantity,
required this.sku,
+ this.winBackOffer,
this.withOffer,
});
@@ -3498,10 +3663,23 @@ class RequestPurchaseIosProps {
final bool? andDangerouslyFinishTransactionAutomatically;
/// App account token for user tracking
final String? appAccountToken;
+ /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ /// Set to true to indicate the user is eligible for introductory offer,
+ /// or false to indicate they are not. When nil, the system determines eligibility.
+ /// Back-deployed to iOS 15.
+ final bool? introductoryOfferEligibility;
+ /// JWS promotional offer (iOS 15+, WWDC 2025).
+ /// New signature format using compact JWS string for promotional offers.
+ /// Back-deployed to iOS 15.
+ final PromotionalOfferJWSInputIOS? promotionalOfferJWS;
/// Purchase quantity
final int? quantity;
/// Product SKU
final String sku;
+ /// Win-back offer to apply (iOS 18+)
+ /// Used to re-engage churned subscribers with a discount or free trial.
+ /// Note: Win-back offers only apply to subscription products.
+ final WinBackOfferInputIOS? winBackOffer;
/// Discount offer to apply
final DiscountOfferInputIOS? withOffer;
@@ -3510,8 +3688,11 @@ class RequestPurchaseIosProps {
advancedCommerceData: json['advancedCommerceData'] as String?,
andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?,
appAccountToken: json['appAccountToken'] as String?,
+ introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?,
+ promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null,
quantity: json['quantity'] as int?,
sku: json['sku'] as String,
+ winBackOffer: json['winBackOffer'] != null ? WinBackOfferInputIOS.fromJson(json['winBackOffer'] as Map) : null,
withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null,
);
}
@@ -3521,8 +3702,11 @@ class RequestPurchaseIosProps {
'advancedCommerceData': advancedCommerceData,
'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically,
'appAccountToken': appAccountToken,
+ 'introductoryOfferEligibility': introductoryOfferEligibility,
+ 'promotionalOfferJWS': promotionalOfferJWS?.toJson(),
'quantity': quantity,
'sku': sku,
+ 'winBackOffer': winBackOffer?.toJson(),
'withOffer': withOffer?.toJson(),
};
}
@@ -3700,8 +3884,11 @@ class RequestSubscriptionIosProps {
this.advancedCommerceData,
this.andDangerouslyFinishTransactionAutomatically,
this.appAccountToken,
+ this.introductoryOfferEligibility,
+ this.promotionalOfferJWS,
this.quantity,
required this.sku,
+ this.winBackOffer,
this.withOffer,
});
@@ -3712,8 +3899,22 @@ class RequestSubscriptionIosProps {
final String? advancedCommerceData;
final bool? andDangerouslyFinishTransactionAutomatically;
final String? appAccountToken;
+ /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ /// Set to true to indicate the user is eligible for introductory offer,
+ /// or false to indicate they are not. When nil, the system determines eligibility.
+ /// Back-deployed to iOS 15.
+ final bool? introductoryOfferEligibility;
+ /// JWS promotional offer (iOS 15+, WWDC 2025).
+ /// New signature format using compact JWS string for promotional offers.
+ /// Back-deployed to iOS 15.
+ final PromotionalOfferJWSInputIOS? promotionalOfferJWS;
final int? quantity;
final String sku;
+ /// 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.
+ final WinBackOfferInputIOS? winBackOffer;
final DiscountOfferInputIOS? withOffer;
factory RequestSubscriptionIosProps.fromJson(Map json) {
@@ -3721,8 +3922,11 @@ class RequestSubscriptionIosProps {
advancedCommerceData: json['advancedCommerceData'] as String?,
andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?,
appAccountToken: json['appAccountToken'] as String?,
+ introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?,
+ promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null,
quantity: json['quantity'] as int?,
sku: json['sku'] as String,
+ winBackOffer: json['winBackOffer'] != null ? WinBackOfferInputIOS.fromJson(json['winBackOffer'] as Map) : null,
withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null,
);
}
@@ -3732,8 +3936,11 @@ class RequestSubscriptionIosProps {
'advancedCommerceData': advancedCommerceData,
'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically,
'appAccountToken': appAccountToken,
+ 'introductoryOfferEligibility': introductoryOfferEligibility,
+ 'promotionalOfferJWS': promotionalOfferJWS?.toJson(),
'quantity': quantity,
'sku': sku,
+ 'winBackOffer': winBackOffer?.toJson(),
'withOffer': withOffer?.toJson(),
};
}
@@ -4054,6 +4261,31 @@ class VerifyPurchaseWithProviderProps {
}
}
+/// 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.
+class WinBackOfferInputIOS {
+ const WinBackOfferInputIOS({
+ required this.offerId,
+ });
+
+ /// The win-back offer ID from App Store Connect
+ final String offerId;
+
+ factory WinBackOfferInputIOS.fromJson(Map json) {
+ return WinBackOfferInputIOS(
+ offerId: json['offerId'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'offerId': offerId,
+ };
+ }
+}
+
// MARK: - Unions
sealed class Product implements ProductCommon {
@@ -4367,6 +4599,7 @@ abstract class QueryResolver {
/// Get all available purchases for the current user
Future> getAvailablePurchases({
bool? alsoPublishToEventListenerIOS,
+ bool? includeSuspendedAndroid,
bool? onlyIncludeActiveItemsIOS,
});
/// Retrieve all pending transactions in the StoreKit queue
@@ -4541,6 +4774,7 @@ typedef QueryGetActiveSubscriptionsHandler = Future> Fu
typedef QueryGetAppTransactionIOSHandler = Future Function();
typedef QueryGetAvailablePurchasesHandler = Future> Function({
bool? alsoPublishToEventListenerIOS,
+ bool? includeSuspendedAndroid,
bool? onlyIncludeActiveItemsIOS,
});
typedef QueryGetPendingTransactionsIOSHandler = Future> Function();
diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd
index 830bf179..eb3932ed 100644
--- a/packages/gql/src/generated/types.gd
+++ b/packages/gql/src/generated/types.gd
@@ -191,6 +191,18 @@ enum ProductQueryType {
ALL = 2,
}
+## 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+
+enum ProductStatusAndroid {
+ ## Product was successfully fetched
+ OK = 0,
+ ## Product not found - the SKU doesn't exist in the Play Console
+ NOT_FOUND = 1,
+ ## No offers available for the user - product exists but user is not eligible for any offers
+ NO_OFFERS_AVAILABLE = 2,
+ ## Unknown error occurred while fetching the product
+ UNKNOWN = 3,
+}
+
enum ProductType {
IN_APP = 0,
SUBS = 1,
@@ -213,9 +225,21 @@ enum PurchaseVerificationProvider {
IAPKIT = 0,
}
+## Sub-response codes for more granular purchase error information (Android) Available in Google Play Billing Library 8.0.0+
+enum SubResponseCodeAndroid {
+ ## No specific sub-response code applies
+ NO_APPLICABLE_SUB_RESPONSE_CODE = 0,
+ ## User's payment method has insufficient funds
+ PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS = 1,
+ ## User doesn't meet subscription offer eligibility requirements
+ USER_INELIGIBLE = 2,
+}
+
enum SubscriptionOfferTypeIOS {
INTRODUCTORY = 0,
PROMOTIONAL = 1,
+ ## Win-back offer type (iOS 18+) Used to re-engage churned subscribers with a discount or free trial.
+ WIN_BACK = 2,
}
enum SubscriptionPeriodIOS {
@@ -443,6 +467,35 @@ class BillingProgramReportingDetailsAndroid:
dict["externalTransactionToken"] = external_transaction_token
return dict
+## Extended billing result with sub-response code (Android) Available in Google Play Billing Library 8.0.0+
+class BillingResultAndroid:
+ ## The response code from the billing operation
+ var response_code: int
+ ## Debug message from the billing library
+ var debug_message: String
+ ## Sub-response code for more granular error information (8.0+).
+ var sub_response_code: SubResponseCodeAndroid
+
+ static func from_dict(data: Dictionary) -> BillingResultAndroid:
+ var obj = BillingResultAndroid.new()
+ if data.has("responseCode") and data["responseCode"] != null:
+ obj.response_code = data["responseCode"]
+ if data.has("debugMessage") and data["debugMessage"] != null:
+ obj.debug_message = data["debugMessage"]
+ if data.has("subResponseCode") and data["subResponseCode"] != null:
+ obj.sub_response_code = data["subResponseCode"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["responseCode"] = response_code
+ dict["debugMessage"] = debug_message
+ if SUB_RESPONSE_CODE_ANDROID_VALUES.has(sub_response_code):
+ dict["subResponseCode"] = SUB_RESPONSE_CODE_ANDROID_VALUES[sub_response_code]
+ else:
+ dict["subResponseCode"] = sub_response_code
+ return dict
+
## Details provided when user selects developer billing option (Android) Received via DeveloperProvidedBillingListener callback Available in Google Play Billing Library 8.3.0+
class DeveloperProvidedBillingDetailsAndroid:
## External transaction token used to report transactions made through developer billing.
@@ -918,6 +971,8 @@ class ProductAndroid:
var debug_description: String
var platform: IapPlatform
var name_android: String
+ ## Product-level status code indicating fetch result (Android 8.0+)
+ var product_status_android: ProductStatusAndroid
## Standardized discount offers for one-time products.
var discount_offers: Array[DiscountOffer]
## Standardized subscription offers.
@@ -951,6 +1006,8 @@ class ProductAndroid:
obj.platform = data["platform"]
if data.has("nameAndroid") and data["nameAndroid"] != null:
obj.name_android = data["nameAndroid"]
+ if data.has("productStatusAndroid") and data["productStatusAndroid"] != null:
+ obj.product_status_android = data["productStatusAndroid"]
if data.has("discountOffers") and data["discountOffers"] != null:
var arr = []
for item in data["discountOffers"]:
@@ -1004,6 +1061,10 @@ class ProductAndroid:
else:
dict["platform"] = platform
dict["nameAndroid"] = name_android
+ if PRODUCT_STATUS_ANDROID_VALUES.has(product_status_android):
+ dict["productStatusAndroid"] = PRODUCT_STATUS_ANDROID_VALUES[product_status_android]
+ else:
+ dict["productStatusAndroid"] = product_status_android
if discount_offers != null:
var arr = []
for item in discount_offers:
@@ -1262,6 +1323,8 @@ class ProductSubscriptionAndroid:
var debug_description: String
var platform: IapPlatform
var name_android: String
+ ## Product-level status code indicating fetch result (Android 8.0+)
+ var product_status_android: ProductStatusAndroid
## Standardized discount offers for one-time products.
var discount_offers: Array[DiscountOffer]
## Standardized subscription offers.
@@ -1295,6 +1358,8 @@ class ProductSubscriptionAndroid:
obj.platform = data["platform"]
if data.has("nameAndroid") and data["nameAndroid"] != null:
obj.name_android = data["nameAndroid"]
+ if data.has("productStatusAndroid") and data["productStatusAndroid"] != null:
+ obj.product_status_android = data["productStatusAndroid"]
if data.has("discountOffers") and data["discountOffers"] != null:
var arr = []
for item in data["discountOffers"]:
@@ -1348,6 +1413,10 @@ class ProductSubscriptionAndroid:
else:
dict["platform"] = platform
dict["nameAndroid"] = name_android
+ if PRODUCT_STATUS_ANDROID_VALUES.has(product_status_android):
+ dict["productStatusAndroid"] = PRODUCT_STATUS_ANDROID_VALUES[product_status_android]
+ else:
+ dict["productStatusAndroid"] = product_status_android
if discount_offers != null:
var arr = []
for item in discount_offers:
@@ -2808,6 +2877,29 @@ class ProductRequest:
dict["type"] = type
return dict
+## 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.
+class PromotionalOfferJWSInputIOS:
+ ## The promotional offer identifier from App Store Connect
+ var offer_id: String
+ ## Compact JWS string signed by your server.
+ var jws: String
+
+ static func from_dict(data: Dictionary) -> PromotionalOfferJWSInputIOS:
+ var obj = PromotionalOfferJWSInputIOS.new()
+ if data.has("offerId") and data["offerId"] != null:
+ obj.offer_id = data["offerId"]
+ if data.has("jws") and data["jws"] != null:
+ obj.jws = data["jws"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ if offer_id != null:
+ dict["offerId"] = offer_id
+ if jws != null:
+ dict["jws"] = jws
+ return dict
+
class PurchaseInput:
var id: String
var product_id: String
@@ -2884,6 +2976,8 @@ class PurchaseOptions:
var also_publish_to_event_listener_ios: bool
## Limit to currently active items on iOS
var only_include_active_items_ios: bool
+ ## Include suspended subscriptions in the result (Android 8.1+).
+ var include_suspended_android: bool
static func from_dict(data: Dictionary) -> PurchaseOptions:
var obj = PurchaseOptions.new()
@@ -2891,6 +2985,8 @@ class PurchaseOptions:
obj.also_publish_to_event_listener_ios = data["alsoPublishToEventListenerIOS"]
if data.has("onlyIncludeActiveItemsIOS") and data["onlyIncludeActiveItemsIOS"] != null:
obj.only_include_active_items_ios = data["onlyIncludeActiveItemsIOS"]
+ if data.has("includeSuspendedAndroid") and data["includeSuspendedAndroid"] != null:
+ obj.include_suspended_android = data["includeSuspendedAndroid"]
return obj
func to_dict() -> Dictionary:
@@ -2899,6 +2995,8 @@ class PurchaseOptions:
dict["alsoPublishToEventListenerIOS"] = also_publish_to_event_listener_ios
if only_include_active_items_ios != null:
dict["onlyIncludeActiveItemsIOS"] = only_include_active_items_ios
+ if include_suspended_android != null:
+ dict["includeSuspendedAndroid"] = include_suspended_android
return dict
class RequestPurchaseAndroidProps:
@@ -2958,6 +3056,12 @@ class RequestPurchaseIosProps:
var quantity: int
## Discount offer to apply
var with_offer: DiscountOfferInputIOS
+ ## Win-back offer to apply (iOS 18+)
+ var win_back_offer: WinBackOfferInputIOS
+ ## JWS promotional offer (iOS 15+, WWDC 2025).
+ var promotional_offer_jws: PromotionalOfferJWSInputIOS
+ ## Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ var introductory_offer_eligibility: bool
## Advanced commerce data token (iOS 15+).
var advanced_commerce_data: String
@@ -2976,6 +3080,18 @@ class RequestPurchaseIosProps:
obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"])
else:
obj.with_offer = data["withOffer"]
+ if data.has("winBackOffer") and data["winBackOffer"] != null:
+ if data["winBackOffer"] is Dictionary:
+ obj.win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"])
+ else:
+ obj.win_back_offer = data["winBackOffer"]
+ if data.has("promotionalOfferJWS") and data["promotionalOfferJWS"] != null:
+ if data["promotionalOfferJWS"] is Dictionary:
+ obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"])
+ else:
+ obj.promotional_offer_jws = data["promotionalOfferJWS"]
+ if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
+ obj.introductory_offer_eligibility = data["introductoryOfferEligibility"]
if data.has("advancedCommerceData") and data["advancedCommerceData"] != null:
obj.advanced_commerce_data = data["advancedCommerceData"]
return obj
@@ -2995,6 +3111,18 @@ class RequestPurchaseIosProps:
dict["withOffer"] = with_offer.to_dict()
else:
dict["withOffer"] = with_offer
+ if win_back_offer != null:
+ if win_back_offer.has_method("to_dict"):
+ dict["winBackOffer"] = win_back_offer.to_dict()
+ else:
+ dict["winBackOffer"] = win_back_offer
+ if promotional_offer_jws != null:
+ if promotional_offer_jws.has_method("to_dict"):
+ dict["promotionalOfferJWS"] = promotional_offer_jws.to_dict()
+ else:
+ dict["promotionalOfferJWS"] = promotional_offer_jws
+ if introductory_offer_eligibility != null:
+ dict["introductoryOfferEligibility"] = introductory_offer_eligibility
if advanced_commerce_data != null:
dict["advancedCommerceData"] = advanced_commerce_data
return dict
@@ -3201,6 +3329,12 @@ class RequestSubscriptionIosProps:
var app_account_token: String
var quantity: int
var with_offer: DiscountOfferInputIOS
+ ## Win-back offer to apply (iOS 18+)
+ var win_back_offer: WinBackOfferInputIOS
+ ## JWS promotional offer (iOS 15+, WWDC 2025).
+ var promotional_offer_jws: PromotionalOfferJWSInputIOS
+ ## Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ var introductory_offer_eligibility: bool
## Advanced commerce data token (iOS 15+).
var advanced_commerce_data: String
@@ -3219,6 +3353,18 @@ class RequestSubscriptionIosProps:
obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"])
else:
obj.with_offer = data["withOffer"]
+ if data.has("winBackOffer") and data["winBackOffer"] != null:
+ if data["winBackOffer"] is Dictionary:
+ obj.win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"])
+ else:
+ obj.win_back_offer = data["winBackOffer"]
+ if data.has("promotionalOfferJWS") and data["promotionalOfferJWS"] != null:
+ if data["promotionalOfferJWS"] is Dictionary:
+ obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"])
+ else:
+ obj.promotional_offer_jws = data["promotionalOfferJWS"]
+ if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
+ obj.introductory_offer_eligibility = data["introductoryOfferEligibility"]
if data.has("advancedCommerceData") and data["advancedCommerceData"] != null:
obj.advanced_commerce_data = data["advancedCommerceData"]
return obj
@@ -3238,6 +3384,18 @@ class RequestSubscriptionIosProps:
dict["withOffer"] = with_offer.to_dict()
else:
dict["withOffer"] = with_offer
+ if win_back_offer != null:
+ if win_back_offer.has_method("to_dict"):
+ dict["winBackOffer"] = win_back_offer.to_dict()
+ else:
+ dict["winBackOffer"] = win_back_offer
+ if promotional_offer_jws != null:
+ if promotional_offer_jws.has_method("to_dict"):
+ dict["promotionalOfferJWS"] = promotional_offer_jws.to_dict()
+ else:
+ dict["promotionalOfferJWS"] = promotional_offer_jws
+ if introductory_offer_eligibility != null:
+ dict["introductoryOfferEligibility"] = introductory_offer_eligibility
if advanced_commerce_data != null:
dict["advancedCommerceData"] = advanced_commerce_data
return dict
@@ -3563,6 +3721,23 @@ class VerifyPurchaseWithProviderProps:
dict["iapkit"] = iapkit
return dict
+## 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.
+class WinBackOfferInputIOS:
+ ## The win-back offer ID from App Store Connect
+ var offer_id: String
+
+ static func from_dict(data: Dictionary) -> WinBackOfferInputIOS:
+ var obj = WinBackOfferInputIOS.new()
+ if data.has("offerId") and data["offerId"] != null:
+ obj.offer_id = data["offerId"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ if offer_id != null:
+ dict["offerId"] = offer_id
+ return dict
+
# ============================================================================
# Enum String Helpers
# ============================================================================
@@ -3702,6 +3877,13 @@ const PRODUCT_QUERY_TYPE_VALUES = {
ProductQueryType.ALL: "all"
}
+const PRODUCT_STATUS_ANDROID_VALUES = {
+ ProductStatusAndroid.OK: "ok",
+ ProductStatusAndroid.NOT_FOUND: "not-found",
+ ProductStatusAndroid.NO_OFFERS_AVAILABLE: "no-offers-available",
+ ProductStatusAndroid.UNKNOWN: "unknown"
+}
+
const PRODUCT_TYPE_VALUES = {
ProductType.IN_APP: "in-app",
ProductType.SUBS: "subs"
@@ -3724,9 +3906,16 @@ const PURCHASE_VERIFICATION_PROVIDER_VALUES = {
PurchaseVerificationProvider.IAPKIT: "iapkit"
}
+const SUB_RESPONSE_CODE_ANDROID_VALUES = {
+ SubResponseCodeAndroid.NO_APPLICABLE_SUB_RESPONSE_CODE: "no-applicable-sub-response-code",
+ SubResponseCodeAndroid.PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS: "payment-declined-due-to-insufficient-funds",
+ SubResponseCodeAndroid.USER_INELIGIBLE: "user-ineligible"
+}
+
const SUBSCRIPTION_OFFER_TYPE_IOS_VALUES = {
SubscriptionOfferTypeIOS.INTRODUCTORY: "introductory",
- SubscriptionOfferTypeIOS.PROMOTIONAL: "promotional"
+ SubscriptionOfferTypeIOS.PROMOTIONAL: "promotional",
+ SubscriptionOfferTypeIOS.WIN_BACK: "win-back"
}
const SUBSCRIPTION_PERIOD_IOS_VALUES = {
diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts
index 3f6634b4..28e59152 100644
--- a/packages/gql/src/generated/types.ts
+++ b/packages/gql/src/generated/types.ts
@@ -99,6 +99,22 @@ export interface BillingProgramReportingDetailsAndroid {
externalTransactionToken: string;
}
+/**
+ * Extended billing result with sub-response code (Android)
+ * Available in Google Play Billing Library 8.0.0+
+ */
+export interface BillingResultAndroid {
+ /** Debug message from the billing library */
+ debugMessage?: (string | null);
+ /** The response code from the billing operation */
+ responseCode: number;
+ /**
+ * Sub-response code for more granular error information (8.0+).
+ * Provides additional context when responseCode indicates an error.
+ */
+ subResponseCode?: (SubResponseCodeAndroid | null);
+}
+
export interface DeepLinkOptions {
/** Android package name to target (required on Android) */
packageNameAndroid?: (string | null);
@@ -664,6 +680,14 @@ export interface ProductAndroid extends ProductCommon {
oneTimePurchaseOfferDetailsAndroid?: (ProductAndroidOneTimePurchaseOfferDetail[] | null);
platform: 'android';
price?: (number | null);
+ /**
+ * 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+
+ */
+ productStatusAndroid?: (ProductStatusAndroid | null);
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
* @deprecated Use subscriptionOffers instead
@@ -769,6 +793,14 @@ export interface ProductRequest {
type?: (ProductQueryType | null);
}
+/**
+ * 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+
+ */
+export type ProductStatusAndroid = 'ok' | 'not-found' | 'no-offers-available' | 'unknown';
+
export type ProductSubscription = ProductSubscriptionAndroid | ProductSubscriptionIOS;
export interface ProductSubscriptionAndroid extends ProductCommon {
@@ -794,6 +826,14 @@ export interface ProductSubscriptionAndroid extends ProductCommon {
oneTimePurchaseOfferDetailsAndroid?: (ProductAndroidOneTimePurchaseOfferDetail[] | null);
platform: 'android';
price?: (number | null);
+ /**
+ * 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+
+ */
+ productStatusAndroid?: (ProductStatusAndroid | null);
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
* @deprecated Use subscriptionOffers instead
@@ -866,6 +906,23 @@ export type ProductType = 'in-app' | 'subs';
export type ProductTypeIOS = 'consumable' | 'non-consumable' | 'auto-renewable-subscription' | 'non-renewing-subscription';
+/**
+ * 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.
+ */
+export interface PromotionalOfferJwsInputIOS {
+ /**
+ * Compact JWS string signed by your server.
+ * The JWS should contain the promotional offer signature data.
+ * Format: header.payload.signature (base64url encoded)
+ */
+ jws: string;
+ /** The promotional offer identifier from App Store Connect */
+ offerId: string;
+}
+
export type Purchase = PurchaseAndroid | PurchaseIOS;
export interface PurchaseAndroid extends PurchaseCommon {
@@ -980,6 +1037,13 @@ export interface PurchaseOfferIOS {
export interface PurchaseOptions {
/** Also emit results through the iOS event listeners */
alsoPublishToEventListenerIOS?: (boolean | null);
+ /**
+ * 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)
+ */
+ includeSuspendedAndroid?: (boolean | null);
/** Limit to currently active items on iOS */
onlyIncludeActiveItemsIOS?: (boolean | null);
}
@@ -1152,10 +1216,29 @@ export interface RequestPurchaseIosProps {
andDangerouslyFinishTransactionAutomatically?: (boolean | null);
/** App account token for user tracking */
appAccountToken?: (string | null);
+ /**
+ * Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ * Set to true to indicate the user is eligible for introductory offer,
+ * or false to indicate they are not. When nil, the system determines eligibility.
+ * Back-deployed to iOS 15.
+ */
+ introductoryOfferEligibility?: (boolean | null);
+ /**
+ * JWS promotional offer (iOS 15+, WWDC 2025).
+ * New signature format using compact JWS string for promotional offers.
+ * Back-deployed to iOS 15.
+ */
+ promotionalOfferJWS?: (PromotionalOfferJwsInputIOS | null);
/** Purchase quantity */
quantity?: (number | null);
/** Product SKU */
sku: string;
+ /**
+ * Win-back offer to apply (iOS 18+)
+ * Used to re-engage churned subscribers with a discount or free trial.
+ * Note: Win-back offers only apply to subscription products.
+ */
+ winBackOffer?: (WinBackOfferInputIOS | null);
/** Discount offer to apply */
withOffer?: (DiscountOfferInputIOS | null);
}
@@ -1238,8 +1321,28 @@ export interface RequestSubscriptionIosProps {
advancedCommerceData?: (string | null);
andDangerouslyFinishTransactionAutomatically?: (boolean | null);
appAccountToken?: (string | null);
+ /**
+ * Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ * Set to true to indicate the user is eligible for introductory offer,
+ * or false to indicate they are not. When nil, the system determines eligibility.
+ * Back-deployed to iOS 15.
+ */
+ introductoryOfferEligibility?: (boolean | null);
+ /**
+ * JWS promotional offer (iOS 15+, WWDC 2025).
+ * New signature format using compact JWS string for promotional offers.
+ * Back-deployed to iOS 15.
+ */
+ promotionalOfferJWS?: (PromotionalOfferJwsInputIOS | null);
quantity?: (number | null);
sku: string;
+ /**
+ * 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.
+ */
+ winBackOffer?: (WinBackOfferInputIOS | null);
withOffer?: (DiscountOfferInputIOS | null);
}
@@ -1295,6 +1398,12 @@ export interface RequestVerifyPurchaseWithIapkitResult {
store: IapStore;
}
+/**
+ * Sub-response codes for more granular purchase error information (Android)
+ * Available in Google Play Billing Library 8.0.0+
+ */
+export type SubResponseCodeAndroid = 'no-applicable-sub-response-code' | 'payment-declined-due-to-insufficient-funds' | 'user-ineligible';
+
export interface Subscription {
/**
* Fires when a user selects developer billing in the External Payments flow (Android only)
@@ -1415,7 +1524,7 @@ export interface SubscriptionOfferIOS {
type: SubscriptionOfferTypeIOS;
}
-export type SubscriptionOfferTypeIOS = 'introductory' | 'promotional';
+export type SubscriptionOfferTypeIOS = 'introductory' | 'promotional' | 'win-back';
/** Subscription period value combining unit and count. */
export interface SubscriptionPeriod {
@@ -1615,6 +1724,16 @@ export interface VerifyPurchaseWithProviderResult {
export type VoidResult = void;
+/**
+ * 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.
+ */
+export interface WinBackOfferInputIOS {
+ /** The win-back offer ID from App Store Connect */
+ offerId: string;
+}
// -- Query helper types (auto-generated)
export type QueryArgsMap = {
canPresentExternalPurchaseNoticeIOS: never;
diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql
index 2c197edc..128c6875 100644
--- a/packages/gql/src/type-android.graphql
+++ b/packages/gql/src/type-android.graphql
@@ -1,5 +1,31 @@
# Android-specific GraphQL Types
+# Product-level status codes (Google Play Billing Library 8.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+
+"""
+enum ProductStatusAndroid {
+ """
+ Product was successfully fetched
+ """
+ OK
+ """
+ Product not found - the SKU doesn't exist in the Play Console
+ """
+ NOT_FOUND
+ """
+ No offers available for the user - product exists but user is not eligible for any offers
+ """
+ NO_OFFERS_AVAILABLE
+ """
+ Unknown error occurred while fetching the product
+ """
+ UNKNOWN
+}
+
# Android pricing phases
type PricingPhasesAndroid {
pricingPhaseList: [PricingPhaseAndroid!]!
@@ -190,6 +216,14 @@ type ProductAndroid implements ProductCommon {
# Android-specific
nameAndroid: String!
+ """
+ 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+
+ """
+ productStatusAndroid: ProductStatusAndroid
# Standardized cross-platform fields
"""
@@ -236,6 +270,14 @@ type ProductSubscriptionAndroid implements ProductCommon {
# Android-specific
nameAndroid: String!
+ """
+ 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+
+ """
+ productStatusAndroid: ProductStatusAndroid
# Standardized cross-platform fields
"""
@@ -795,3 +837,45 @@ type ExternalOfferAvailabilityResultAndroid
"""
isAvailable: Boolean!
}
+
+# Sub-Response Codes (Google Play Billing Library 8.0+)
+# See: https://developer.android.com/google/play/billing/release-notes
+
+"""
+Sub-response codes for more granular purchase error information (Android)
+Available in Google Play Billing Library 8.0.0+
+"""
+enum SubResponseCodeAndroid {
+ """
+ No specific sub-response code applies
+ """
+ NO_APPLICABLE_SUB_RESPONSE_CODE
+ """
+ User's payment method has insufficient funds
+ """
+ PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS
+ """
+ User doesn't meet subscription offer eligibility requirements
+ """
+ USER_INELIGIBLE
+}
+
+"""
+Extended billing result with sub-response code (Android)
+Available in Google Play Billing Library 8.0.0+
+"""
+type BillingResultAndroid {
+ """
+ The response code from the billing operation
+ """
+ responseCode: Int!
+ """
+ Debug message from the billing library
+ """
+ debugMessage: String
+ """
+ Sub-response code for more granular error information (8.0+).
+ Provides additional context when responseCode indicates an error.
+ """
+ subResponseCode: SubResponseCodeAndroid
+}
diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql
index 3c2c445c..e5e3dd1c 100644
--- a/packages/gql/src/type-ios.graphql
+++ b/packages/gql/src/type-ios.graphql
@@ -30,6 +30,11 @@ enum PaymentModeIOS {
enum SubscriptionOfferTypeIOS {
Introductory
Promotional
+ """
+ Win-back offer type (iOS 18+)
+ Used to re-engage churned subscribers with a discount or free trial.
+ """
+ WinBack
}
# iOS subscription period (unit + value)
@@ -236,6 +241,25 @@ input RequestPurchaseIosProps {
"""
withOffer: DiscountOfferInputIOS
"""
+ Win-back offer to apply (iOS 18+)
+ Used to re-engage churned subscribers with a discount or free trial.
+ Note: Win-back offers only apply to subscription products.
+ """
+ winBackOffer: WinBackOfferInputIOS
+ """
+ JWS promotional offer (iOS 15+, WWDC 2025).
+ New signature format using compact JWS string for promotional offers.
+ Back-deployed to iOS 15.
+ """
+ promotionalOfferJWS: PromotionalOfferJWSInputIOS
+ """
+ Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ Set to true to indicate the user is eligible for introductory offer,
+ or false to indicate they are not. When nil, the system determines eligibility.
+ Back-deployed to iOS 15.
+ """
+ introductoryOfferEligibility: Boolean
+ """
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.
@@ -252,6 +276,26 @@ input RequestSubscriptionIosProps {
quantity: Int
withOffer: DiscountOfferInputIOS
"""
+ 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.
+ """
+ winBackOffer: WinBackOfferInputIOS
+ """
+ JWS promotional offer (iOS 15+, WWDC 2025).
+ New signature format using compact JWS string for promotional offers.
+ Back-deployed to iOS 15.
+ """
+ promotionalOfferJWS: PromotionalOfferJWSInputIOS
+ """
+ Override introductory offer eligibility (iOS 15+, WWDC 2025).
+ Set to true to indicate the user is eligible for introductory offer,
+ or false to indicate they are not. When nil, the system determines eligibility.
+ Back-deployed to iOS 15.
+ """
+ introductoryOfferEligibility: Boolean
+ """
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.
@@ -284,6 +328,38 @@ input DiscountOfferInputIOS {
timestamp: Float!
}
+"""
+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.
+"""
+input WinBackOfferInputIOS {
+ """
+ The win-back offer ID from App Store Connect
+ """
+ offerId: String!
+}
+
+"""
+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.
+"""
+input PromotionalOfferJWSInputIOS {
+ """
+ The promotional offer identifier from App Store Connect
+ """
+ offerId: String!
+ """
+ Compact JWS string signed by your server.
+ The JWS should contain the promotional offer signature data.
+ Format: header.payload.signature (base64url encoded)
+ """
+ jws: String!
+}
+
"""
Apple App Store verification parameters.
Used for server-side receipt validation via App Store Server API.
diff --git a/packages/gql/src/type.graphql b/packages/gql/src/type.graphql
index d183cdcb..4c7acd76 100644
--- a/packages/gql/src/type.graphql
+++ b/packages/gql/src/type.graphql
@@ -135,6 +135,13 @@ input PurchaseOptions {
Limit to currently active items on iOS
"""
onlyIncludeActiveItemsIOS: Boolean
+ """
+ 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)
+ """
+ includeSuspendedAndroid: Boolean
}
# Parameters for requestPurchase
@@ -734,4 +741,6 @@ input InitConnectionConfig {
- EXTERNAL_PAYMENTS: Developer provided billing, Japan only (8.3.0+)
"""
enableBillingProgramAndroid: BillingProgramAndroid
+ # Note: enableAutoServiceReconnection is always enabled internally (Billing 8.0+)
+ # since OpenIAP uses Billing Library 8.3.0+
}