From 734b2a7b159d07468b1ea56828b6727e47586f9b Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:10:22 +0900 Subject: [PATCH 1/8] feat(gql): expose commerce period metadata Add the Advanced Commerce subscription period, document the current StoreKit intent contracts, and regenerate every synchronized platform type. --- libraries/expo-iap/src/types.ts | 29 +++++++--- .../flutter_inapp_purchase/lib/types.dart | 30 +++++++--- libraries/godot-iap/addons/godot-iap/types.gd | 19 +++++-- .../io/github/hyochan/kmpiap/openiap/Types.kt | 56 ++++++++++++++----- libraries/maui-iap/src/OpenIap.Maui/Types.cs | 28 +++++++--- libraries/react-native-iap/src/types.ts | 29 +++++++--- packages/apple/Sources/Models/Types.swift | 27 ++++++--- .../src/main/java/dev/hyo/openiap/Types.kt | 56 ++++++++++++++----- packages/gql/src/api-ios.graphql | 20 +++++-- packages/gql/src/event.graphql | 5 +- packages/gql/src/generated/Types.cs | 28 +++++++--- packages/gql/src/generated/Types.kt | 56 ++++++++++++++----- packages/gql/src/generated/Types.swift | 27 ++++++--- packages/gql/src/generated/types.dart | 30 +++++++--- packages/gql/src/generated/types.gd | 19 +++++-- packages/gql/src/generated/types.ts | 29 +++++++--- packages/gql/src/type-ios.graphql | 4 ++ 17 files changed, 372 insertions(+), 120 deletions(-) diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index ff1896b4f..c8de204f6 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -46,6 +46,8 @@ export interface AdvancedCommerceInfoIOS { estimatedTax?: (string | null); /** The items purchased as part of this transaction */ items: AdvancedCommerceItemIOS[]; + /** Subscription period for this transaction */ + period?: (SubscriptionPeriodValueIOS | null); /** Request reference identifier for tracking */ requestReferenceId?: (string | null); /** Tax code for the transaction */ @@ -809,11 +811,14 @@ export interface Mutation { openRedeemOfferCodeAndroid: Promise; /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ presentCodeRedemptionSheetIOS?: Promise<(PurchaseIOS | null)>; @@ -1427,7 +1432,12 @@ export interface Query { */ getPendingTransactionsIOS: Promise; /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ getPromotedProductIOS?: Promise<(ProductIOS | null)>; @@ -1864,7 +1874,12 @@ export interface Subscription { * openiap-google 2.3.0 (requires Play Billing 9.1.0+). */ developerProvidedBillingAndroid: DeveloperProvidedBillingDetailsAndroid; - /** Fires when the App Store surfaces a promoted product (iOS only) */ + /** + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. + */ promotedProductIOS: string; /** Fires when a purchase fails or is cancelled */ purchaseError: PurchaseError; diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 5ae6eddd4..6db39b914 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -1223,6 +1223,7 @@ class AdvancedCommerceInfoIOS { this.displayName, this.estimatedTax, required this.items, + this.period, this.requestReferenceId, this.taxCode, this.taxExclusivePrice, @@ -1237,6 +1238,8 @@ class AdvancedCommerceInfoIOS { final String? estimatedTax; /// The items purchased as part of this transaction final List items; + /// Subscription period for this transaction + final SubscriptionPeriodValueIOS? period; /// Request reference identifier for tracking final String? requestReferenceId; /// Tax code for the transaction @@ -1252,6 +1255,7 @@ class AdvancedCommerceInfoIOS { displayName: json['displayName'] as String?, estimatedTax: json['estimatedTax'] as String?, items: (json['items'] as List).map((e) => AdvancedCommerceItemIOS.fromJson(e as Map)).toList(), + period: json['period'] != null ? SubscriptionPeriodValueIOS.fromJson(json['period'] as Map) : null, requestReferenceId: json['requestReferenceId'] as String?, taxCode: json['taxCode'] as String?, taxExclusivePrice: json['taxExclusivePrice'] as String?, @@ -1266,6 +1270,7 @@ class AdvancedCommerceInfoIOS { 'displayName': displayName, 'estimatedTax': estimatedTax, 'items': items.map((e) => e.toJson()).toList(), + 'period': period?.toJson(), 'requestReferenceId': requestReferenceId, 'taxCode': taxCode, 'taxExclusivePrice': taxExclusivePrice, @@ -5385,11 +5390,14 @@ abstract class MutationResolver { /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android Future openRedeemOfferCodeAndroid(); /// Show the App Store offer code redemption sheet. - /// On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - /// transaction produced by the redemption. Earlier iOS and Mac Catalyst - /// versions present the legacy sheet and return null; reconcile purchases - /// through the normal transaction listener or an explicit available-purchases - /// refresh. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// Other supported paths present the system sheet and return null: StoreKit 2's + /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + /// Catalyst 15. + /// Reconcile null results through the normal transaction listener or an explicit + /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Future presentCodeRedemptionSheetIOS(); /// Present an external purchase link, StoreKit External (iOS 16+). @@ -5509,7 +5517,12 @@ abstract class QueryResolver { /// List unfinished StoreKit transactions in the queue. /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Future> getPendingTransactionsIOS(); - /// Read the App Store-promoted product, if any (iOS 11+). + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Future getPromotedProductIOS(); /// Get base64-encoded receipt data (legacy validation). @@ -5553,7 +5566,10 @@ abstract class SubscriptionResolver { /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). Future developerProvidedBillingAndroid(); - /// Fires when the App Store surfaces a promoted product (iOS only) + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. Future promotedProductIOS(); /// Fires when a purchase fails or is cancelled Future purchaseError(); diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 376a5e40d..163cad13a 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -439,6 +439,8 @@ class ActiveSubscription: class AdvancedCommerceInfoIOS: ## The items purchased as part of this transaction var items: Array[AdvancedCommerceItemIOS] = [] + ## Subscription period for this transaction + var period: SubscriptionPeriodValueIOS ## Request reference identifier for tracking var request_reference_id: Variant = null ## Tax code for the transaction @@ -465,6 +467,11 @@ class AdvancedCommerceInfoIOS: elif item is AdvancedCommerceItemIOS: arr.append(item) obj.items = arr + if data.has("period") and data["period"] != null: + if data["period"] is Dictionary: + obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) + else: + obj.period = data["period"] if data.has("requestReferenceId") and data["requestReferenceId"] != null: obj.request_reference_id = data["requestReferenceId"] if data.has("taxCode") and data["taxCode"] != null: @@ -493,6 +500,10 @@ class AdvancedCommerceInfoIOS: dict["items"] = arr else: dict["items"] = null + if period != null and period.has_method("to_dict"): + dict["period"] = period.to_dict() + else: + dict["period"] = period if request_reference_id != null: dict["requestReferenceId"] = request_reference_id if tax_code != null: @@ -5348,7 +5359,7 @@ class Query: const return_type = "String" const is_array = false - ## Read the App Store-promoted product, if any (iOS 11+). See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios + ## Read the App Store-promoted product, if any (iOS 15+). OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an externally redeemed win-back offer, OpenIAP preserves it for the next matching requestPurchase unless the caller supplies an explicit win-back or promotional offer. See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios class getPromotedProductIOSField: const name = "getPromotedProductIOS" const snake_name = "get_promoted_product_ios" @@ -5785,7 +5796,7 @@ class Mutation: const return_type = "Boolean" const is_array = false - ## Show the App Store offer code redemption sheet. On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified transaction produced by the redemption. Earlier iOS and Mac Catalyst versions present the legacy sheet and return null; reconcile purchases through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + ## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios class presentCodeRedemptionSheetIOSField: const name = "presentCodeRedemptionSheetIOS" const snake_name = "present_code_redemption_sheet_ios" @@ -6073,7 +6084,7 @@ static func has_active_subscriptions_args(subscription_ids: Variant = null) -> D static func get_storefront_args() -> Dictionary: return {} -## Read the App Store-promoted product, if any (iOS 11+). See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios +## Read the App Store-promoted product, if any (iOS 15+). OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an externally redeemed win-back offer, OpenIAP preserves it for the next matching requestPurchase unless the caller supplies an explicit win-back or promotional offer. See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios static func get_promoted_product_ios_args() -> Dictionary: return {} @@ -6246,7 +6257,7 @@ static func begin_refund_request_ios_args(sku: String) -> Dictionary: static func sync_ios_args() -> Dictionary: return {} -## Show the App Store offer code redemption sheet. On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified transaction produced by the redemption. Earlier iOS and Mac Catalyst versions present the legacy sheet and return null; reconcile purchases through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios +## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios static func present_code_redemption_sheet_ios_args() -> Dictionary: return {} diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt index 5c505fe1d..44de509b4 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt @@ -1386,6 +1386,10 @@ public data class AdvancedCommerceInfoIOS( * The items purchased as part of this transaction */ val items: List, + /** + * Subscription period for this transaction + */ + val period: SubscriptionPeriodValueIOS? = null, /** * Request reference identifier for tracking */ @@ -1411,6 +1415,7 @@ public data class AdvancedCommerceInfoIOS( displayName = json["displayName"] as? String, estimatedTax = json["estimatedTax"] as? String, items = (json["items"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AdvancedCommerceItemIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AdvancedCommerceItemIOS") } ?: emptyList(), + period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) }, requestReferenceId = json["requestReferenceId"] as? String, taxCode = json["taxCode"] as? String, taxExclusivePrice = json["taxExclusivePrice"] as? String, @@ -1425,6 +1430,7 @@ public data class AdvancedCommerceInfoIOS( "displayName" to displayName, "estimatedTax" to estimatedTax, "items" to items.map { it.toJson() }, + "period" to period?.toJson(), "requestReferenceId" to requestReferenceId, "taxCode" to taxCode, "taxExclusivePrice" to taxExclusivePrice, @@ -5522,11 +5528,14 @@ public interface MutationResolver { suspend fun openRedeemOfferCodeAndroid(): Boolean /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ suspend fun presentCodeRedemptionSheetIOS(): PurchaseIOS? @@ -5669,7 +5678,12 @@ public interface QueryResolver { */ suspend fun getPendingTransactionsIOS(): List /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ suspend fun getPromotedProductIOS(): ProductIOS? @@ -5737,7 +5751,10 @@ public interface SubscriptionResolver { */ suspend fun developerProvidedBillingAndroid(): DeveloperProvidedBillingDetailsAndroid /** - * Fires when the App Store surfaces a promoted product (iOS only) + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. */ suspend fun promotedProductIOS(): String /** @@ -5899,11 +5916,14 @@ public data class MutationHandlers( val openRedeemOfferCodeAndroid: MutationOpenRedeemOfferCodeAndroidHandler? = null, /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ val presentCodeRedemptionSheetIOS: MutationPresentCodeRedemptionSheetIOSHandler? = null, @@ -6066,7 +6086,12 @@ public data class QueryHandlers( */ val getPendingTransactionsIOS: QueryGetPendingTransactionsIOSHandler? = null, /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ val getPromotedProductIOS: QueryGetPromotedProductIOSHandler? = null, @@ -6140,7 +6165,10 @@ public data class SubscriptionHandlers( */ val developerProvidedBillingAndroid: SubscriptionDeveloperProvidedBillingAndroidHandler? = null, /** - * Fires when the App Store surfaces a promoted product (iOS only) + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. */ val promotedProductIOS: SubscriptionPromotedProductIOSHandler? = null, /** diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index c1146ce06..6338ecb7e 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -2245,6 +2245,9 @@ public sealed record AdvancedCommerceInfoIOS /// The items purchased as part of this transaction [JsonPropertyName("items")] public required IReadOnlyList Items { get; init; } + /// Subscription period for this transaction + [JsonPropertyName("period")] + public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking [JsonPropertyName("requestReferenceId")] public string? RequestReferenceId { get; init; } @@ -4175,11 +4178,14 @@ public interface MutationResolver Task OpenRedeemOfferCodeAndroidAsync(); /// Show the App Store offer code redemption sheet. - /// On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - /// transaction produced by the redemption. Earlier iOS and Mac Catalyst - /// versions present the legacy sheet and return null; reconcile purchases - /// through the normal transaction listener or an explicit available-purchases - /// refresh. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// Other supported paths present the system sheet and return null: StoreKit 2's + /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + /// Catalyst 15. + /// Reconcile null results through the normal transaction listener or an explicit + /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Task PresentCodeRedemptionSheetIOSAsync(); @@ -4298,7 +4304,12 @@ public interface QueryResolver /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Task> GetPendingTransactionsIOSAsync(); - /// Read the App Store-promoted product, if any (iOS 11+). + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Task GetPromotedProductIOSAsync(); @@ -4353,7 +4364,10 @@ public interface SubscriptionResolver /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). Task DeveloperProvidedBillingAndroidAsync(); - /// Fires when the App Store surfaces a promoted product (iOS only) + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. Task PromotedProductIOSAsync(); /// Fires when a purchase fails or is cancelled diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index ff1896b4f..c8de204f6 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -46,6 +46,8 @@ export interface AdvancedCommerceInfoIOS { estimatedTax?: (string | null); /** The items purchased as part of this transaction */ items: AdvancedCommerceItemIOS[]; + /** Subscription period for this transaction */ + period?: (SubscriptionPeriodValueIOS | null); /** Request reference identifier for tracking */ requestReferenceId?: (string | null); /** Tax code for the transaction */ @@ -809,11 +811,14 @@ export interface Mutation { openRedeemOfferCodeAndroid: Promise; /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ presentCodeRedemptionSheetIOS?: Promise<(PurchaseIOS | null)>; @@ -1427,7 +1432,12 @@ export interface Query { */ getPendingTransactionsIOS: Promise; /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ getPromotedProductIOS?: Promise<(ProductIOS | null)>; @@ -1864,7 +1874,12 @@ export interface Subscription { * openiap-google 2.3.0 (requires Play Billing 9.1.0+). */ developerProvidedBillingAndroid: DeveloperProvidedBillingDetailsAndroid; - /** Fires when the App Store surfaces a promoted product (iOS only) */ + /** + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. + */ promotedProductIOS: string; /** Fires when a purchase fails or is cancelled */ purchaseError: PurchaseError; diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index ea89e9f93..06f21b963 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -557,6 +557,8 @@ public struct AdvancedCommerceInfoIOS: Codable { public var estimatedTax: String? = nil /// The items purchased as part of this transaction public var items: [AdvancedCommerceItemIOS] + /// Subscription period for this transaction + public var period: SubscriptionPeriodValueIOS? = nil /// Request reference identifier for tracking public var requestReferenceId: String? = nil /// Tax code for the transaction @@ -2623,11 +2625,14 @@ public protocol MutationResolver { /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android func openRedeemOfferCodeAndroid() async throws -> Bool /// Show the App Store offer code redemption sheet. - /// On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - /// transaction produced by the redemption. Earlier iOS and Mac Catalyst - /// versions present the legacy sheet and return null; reconcile purchases - /// through the normal transaction listener or an explicit available-purchases - /// refresh. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// Other supported paths present the system sheet and return null: StoreKit 2's + /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + /// Catalyst 15. + /// Reconcile null results through the normal transaction listener or an explicit + /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func presentCodeRedemptionSheetIOS() async throws -> PurchaseIOS? /// Present an external purchase link, StoreKit External (iOS 16+). @@ -2724,7 +2729,12 @@ public protocol QueryResolver { /// List unfinished StoreKit transactions in the queue. /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios func getPendingTransactionsIOS() async throws -> [PurchaseIOS] - /// Read the App Store-promoted product, if any (iOS 11+). + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios func getPromotedProductIOS() async throws -> ProductIOS? /// Get base64-encoded receipt data (legacy validation). @@ -2768,7 +2778,10 @@ public protocol SubscriptionResolver { /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). func developerProvidedBillingAndroid() async throws -> DeveloperProvidedBillingDetailsAndroid - /// Fires when the App Store surfaces a promoted product (iOS only) + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. func promotedProductIOS() async throws -> String /// Fires when a purchase fails or is cancelled func purchaseError() async throws -> PurchaseError 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 aeb053857..442769b44 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 @@ -1438,6 +1438,10 @@ public data class AdvancedCommerceInfoIOS( * The items purchased as part of this transaction */ val items: List, + /** + * Subscription period for this transaction + */ + val period: SubscriptionPeriodValueIOS? = null, /** * Request reference identifier for tracking */ @@ -1463,6 +1467,7 @@ public data class AdvancedCommerceInfoIOS( displayName = json["displayName"] as? String, estimatedTax = json["estimatedTax"] as? String, items = (json["items"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AdvancedCommerceItemIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AdvancedCommerceItemIOS") } ?: emptyList(), + period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) }, requestReferenceId = json["requestReferenceId"] as? String, taxCode = json["taxCode"] as? String, taxExclusivePrice = json["taxExclusivePrice"] as? String, @@ -1477,6 +1482,7 @@ public data class AdvancedCommerceInfoIOS( "displayName" to displayName, "estimatedTax" to estimatedTax, "items" to items.map { it.toJson() }, + "period" to period?.toJson(), "requestReferenceId" to requestReferenceId, "taxCode" to taxCode, "taxExclusivePrice" to taxExclusivePrice, @@ -5574,11 +5580,14 @@ public interface MutationResolver { suspend fun openRedeemOfferCodeAndroid(): Boolean /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ suspend fun presentCodeRedemptionSheetIOS(): PurchaseIOS? @@ -5721,7 +5730,12 @@ public interface QueryResolver { */ suspend fun getPendingTransactionsIOS(): List /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ suspend fun getPromotedProductIOS(): ProductIOS? @@ -5789,7 +5803,10 @@ public interface SubscriptionResolver { */ suspend fun developerProvidedBillingAndroid(): DeveloperProvidedBillingDetailsAndroid /** - * Fires when the App Store surfaces a promoted product (iOS only) + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. */ suspend fun promotedProductIOS(): String /** @@ -5951,11 +5968,14 @@ public data class MutationHandlers( val openRedeemOfferCodeAndroid: MutationOpenRedeemOfferCodeAndroidHandler? = null, /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ val presentCodeRedemptionSheetIOS: MutationPresentCodeRedemptionSheetIOSHandler? = null, @@ -6118,7 +6138,12 @@ public data class QueryHandlers( */ val getPendingTransactionsIOS: QueryGetPendingTransactionsIOSHandler? = null, /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ val getPromotedProductIOS: QueryGetPromotedProductIOSHandler? = null, @@ -6192,7 +6217,10 @@ public data class SubscriptionHandlers( */ val developerProvidedBillingAndroid: SubscriptionDeveloperProvidedBillingAndroidHandler? = null, /** - * Fires when the App Store surfaces a promoted product (iOS only) + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. */ val promotedProductIOS: SubscriptionPromotedProductIOSHandler? = null, /** diff --git a/packages/gql/src/api-ios.graphql b/packages/gql/src/api-ios.graphql index b75ae6ced..4da46a5be 100644 --- a/packages/gql/src/api-ios.graphql +++ b/packages/gql/src/api-ios.graphql @@ -2,7 +2,12 @@ extend type Query { """ - Read the App Store-promoted product, if any (iOS 11+). + Read the App Store-promoted product, if any (iOS 15+). + OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + externally redeemed win-back offer, OpenIAP preserves it for the next + matching requestPurchase unless the caller supplies an explicit win-back or + promotional offer. See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios """ # Future @@ -127,11 +132,14 @@ extend type Mutation { syncIOS: Boolean! """ Show the App Store offer code redemption sheet. - On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - transaction produced by the redemption. Earlier iOS and Mac Catalyst - versions present the legacy sheet and return null; reconcile purchases - through the normal transaction listener or an explicit available-purchases - refresh. + When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + visionOS 27+, returns the verified transaction produced by the redemption. + Other supported paths present the system sheet and return null: StoreKit 2's + scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + Catalyst 15. + Reconcile null results through the normal transaction listener or an explicit + available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios """ # Future diff --git a/packages/gql/src/event.graphql b/packages/gql/src/event.graphql index 3a6024a98..70622db34 100644 --- a/packages/gql/src/event.graphql +++ b/packages/gql/src/event.graphql @@ -13,7 +13,10 @@ extend type Subscription { """ purchaseError: PurchaseError! """ - Fires when the App Store surfaces a promoted product (iOS only) + Fires when the App Store surfaces a promoted product (iOS only). + A win-back offer attached to PurchaseIntent is preserved for the next + matching requestPurchase unless the caller supplies an explicit win-back or + promotional offer. """ promotedProductIOS: String! """ diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index c1146ce06..6338ecb7e 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -2245,6 +2245,9 @@ public sealed record AdvancedCommerceInfoIOS /// The items purchased as part of this transaction [JsonPropertyName("items")] public required IReadOnlyList Items { get; init; } + /// Subscription period for this transaction + [JsonPropertyName("period")] + public SubscriptionPeriodValueIOS? Period { get; init; } /// Request reference identifier for tracking [JsonPropertyName("requestReferenceId")] public string? RequestReferenceId { get; init; } @@ -4175,11 +4178,14 @@ public interface MutationResolver Task OpenRedeemOfferCodeAndroidAsync(); /// Show the App Store offer code redemption sheet. - /// On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - /// transaction produced by the redemption. Earlier iOS and Mac Catalyst - /// versions present the legacy sheet and return null; reconcile purchases - /// through the normal transaction listener or an explicit available-purchases - /// refresh. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// Other supported paths present the system sheet and return null: StoreKit 2's + /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + /// Catalyst 15. + /// Reconcile null results through the normal transaction listener or an explicit + /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Task PresentCodeRedemptionSheetIOSAsync(); @@ -4298,7 +4304,12 @@ public interface QueryResolver /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Task> GetPendingTransactionsIOSAsync(); - /// Read the App Store-promoted product, if any (iOS 11+). + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Task GetPromotedProductIOSAsync(); @@ -4353,7 +4364,10 @@ public interface SubscriptionResolver /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). Task DeveloperProvidedBillingAndroidAsync(); - /// Fires when the App Store surfaces a promoted product (iOS only) + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. Task PromotedProductIOSAsync(); /// Fires when a purchase fails or is cancelled diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index b16232319..490f79e33 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -1384,6 +1384,10 @@ public data class AdvancedCommerceInfoIOS( * The items purchased as part of this transaction */ val items: List, + /** + * Subscription period for this transaction + */ + val period: SubscriptionPeriodValueIOS? = null, /** * Request reference identifier for tracking */ @@ -1409,6 +1413,7 @@ public data class AdvancedCommerceInfoIOS( displayName = json["displayName"] as? String, estimatedTax = json["estimatedTax"] as? String, items = (json["items"] as? List<*>)?.mapNotNull { (it as? Map)?.let { AdvancedCommerceItemIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for AdvancedCommerceItemIOS") } ?: emptyList(), + period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) }, requestReferenceId = json["requestReferenceId"] as? String, taxCode = json["taxCode"] as? String, taxExclusivePrice = json["taxExclusivePrice"] as? String, @@ -1423,6 +1428,7 @@ public data class AdvancedCommerceInfoIOS( "displayName" to displayName, "estimatedTax" to estimatedTax, "items" to items.map { it.toJson() }, + "period" to period?.toJson(), "requestReferenceId" to requestReferenceId, "taxCode" to taxCode, "taxExclusivePrice" to taxExclusivePrice, @@ -5520,11 +5526,14 @@ public interface MutationResolver { suspend fun openRedeemOfferCodeAndroid(): Boolean /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ suspend fun presentCodeRedemptionSheetIOS(): PurchaseIOS? @@ -5667,7 +5676,12 @@ public interface QueryResolver { */ suspend fun getPendingTransactionsIOS(): List /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ suspend fun getPromotedProductIOS(): ProductIOS? @@ -5735,7 +5749,10 @@ public interface SubscriptionResolver { */ suspend fun developerProvidedBillingAndroid(): DeveloperProvidedBillingDetailsAndroid /** - * Fires when the App Store surfaces a promoted product (iOS only) + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. */ suspend fun promotedProductIOS(): String /** @@ -5897,11 +5914,14 @@ public data class MutationHandlers( val openRedeemOfferCodeAndroid: MutationOpenRedeemOfferCodeAndroidHandler? = null, /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ val presentCodeRedemptionSheetIOS: MutationPresentCodeRedemptionSheetIOSHandler? = null, @@ -6064,7 +6084,12 @@ public data class QueryHandlers( */ val getPendingTransactionsIOS: QueryGetPendingTransactionsIOSHandler? = null, /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ val getPromotedProductIOS: QueryGetPromotedProductIOSHandler? = null, @@ -6138,7 +6163,10 @@ public data class SubscriptionHandlers( */ val developerProvidedBillingAndroid: SubscriptionDeveloperProvidedBillingAndroidHandler? = null, /** - * Fires when the App Store surfaces a promoted product (iOS only) + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. */ val promotedProductIOS: SubscriptionPromotedProductIOSHandler? = null, /** diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index ea89e9f93..06f21b963 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -557,6 +557,8 @@ public struct AdvancedCommerceInfoIOS: Codable { public var estimatedTax: String? = nil /// The items purchased as part of this transaction public var items: [AdvancedCommerceItemIOS] + /// Subscription period for this transaction + public var period: SubscriptionPeriodValueIOS? = nil /// Request reference identifier for tracking public var requestReferenceId: String? = nil /// Tax code for the transaction @@ -2623,11 +2625,14 @@ public protocol MutationResolver { /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android func openRedeemOfferCodeAndroid() async throws -> Bool /// Show the App Store offer code redemption sheet. - /// On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - /// transaction produced by the redemption. Earlier iOS and Mac Catalyst - /// versions present the legacy sheet and return null; reconcile purchases - /// through the normal transaction listener or an explicit available-purchases - /// refresh. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// Other supported paths present the system sheet and return null: StoreKit 2's + /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + /// Catalyst 15. + /// Reconcile null results through the normal transaction listener or an explicit + /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func presentCodeRedemptionSheetIOS() async throws -> PurchaseIOS? /// Present an external purchase link, StoreKit External (iOS 16+). @@ -2724,7 +2729,12 @@ public protocol QueryResolver { /// List unfinished StoreKit transactions in the queue. /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios func getPendingTransactionsIOS() async throws -> [PurchaseIOS] - /// Read the App Store-promoted product, if any (iOS 11+). + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios func getPromotedProductIOS() async throws -> ProductIOS? /// Get base64-encoded receipt data (legacy validation). @@ -2768,7 +2778,10 @@ public protocol SubscriptionResolver { /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). func developerProvidedBillingAndroid() async throws -> DeveloperProvidedBillingDetailsAndroid - /// Fires when the App Store surfaces a promoted product (iOS only) + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. func promotedProductIOS() async throws -> String /// Fires when a purchase fails or is cancelled func purchaseError() async throws -> PurchaseError diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 5ae6eddd4..6db39b914 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -1223,6 +1223,7 @@ class AdvancedCommerceInfoIOS { this.displayName, this.estimatedTax, required this.items, + this.period, this.requestReferenceId, this.taxCode, this.taxExclusivePrice, @@ -1237,6 +1238,8 @@ class AdvancedCommerceInfoIOS { final String? estimatedTax; /// The items purchased as part of this transaction final List items; + /// Subscription period for this transaction + final SubscriptionPeriodValueIOS? period; /// Request reference identifier for tracking final String? requestReferenceId; /// Tax code for the transaction @@ -1252,6 +1255,7 @@ class AdvancedCommerceInfoIOS { displayName: json['displayName'] as String?, estimatedTax: json['estimatedTax'] as String?, items: (json['items'] as List).map((e) => AdvancedCommerceItemIOS.fromJson(e as Map)).toList(), + period: json['period'] != null ? SubscriptionPeriodValueIOS.fromJson(json['period'] as Map) : null, requestReferenceId: json['requestReferenceId'] as String?, taxCode: json['taxCode'] as String?, taxExclusivePrice: json['taxExclusivePrice'] as String?, @@ -1266,6 +1270,7 @@ class AdvancedCommerceInfoIOS { 'displayName': displayName, 'estimatedTax': estimatedTax, 'items': items.map((e) => e.toJson()).toList(), + 'period': period?.toJson(), 'requestReferenceId': requestReferenceId, 'taxCode': taxCode, 'taxExclusivePrice': taxExclusivePrice, @@ -5385,11 +5390,14 @@ abstract class MutationResolver { /// See: https://openiap.dev/docs/apis/android/open-redeem-offer-code-android Future openRedeemOfferCodeAndroid(); /// Show the App Store offer code redemption sheet. - /// On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - /// transaction produced by the redemption. Earlier iOS and Mac Catalyst - /// versions present the legacy sheet and return null; reconcile purchases - /// through the normal transaction listener or an explicit available-purchases - /// refresh. + /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + /// visionOS 27+, returns the verified transaction produced by the redemption. + /// Other supported paths present the system sheet and return null: StoreKit 2's + /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + /// Catalyst 15. + /// Reconcile null results through the normal transaction listener or an explicit + /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Future presentCodeRedemptionSheetIOS(); /// Present an external purchase link, StoreKit External (iOS 16+). @@ -5509,7 +5517,12 @@ abstract class QueryResolver { /// List unfinished StoreKit transactions in the queue. /// See: https://openiap.dev/docs/apis/ios/get-pending-transactions-ios Future> getPendingTransactionsIOS(); - /// Read the App Store-promoted product, if any (iOS 11+). + /// Read the App Store-promoted product, if any (iOS 15+). + /// OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + /// StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + /// externally redeemed win-back offer, OpenIAP preserves it for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. /// See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios Future getPromotedProductIOS(); /// Get base64-encoded receipt data (legacy validation). @@ -5553,7 +5566,10 @@ abstract class SubscriptionResolver { /// Billing Choice payload fields are available in OpenIAP Spec 2.1.0 / /// openiap-google 2.3.0 (requires Play Billing 9.1.0+). Future developerProvidedBillingAndroid(); - /// Fires when the App Store surfaces a promoted product (iOS only) + /// Fires when the App Store surfaces a promoted product (iOS only). + /// A win-back offer attached to PurchaseIntent is preserved for the next + /// matching requestPurchase unless the caller supplies an explicit win-back or + /// promotional offer. Future promotedProductIOS(); /// Fires when a purchase fails or is cancelled Future purchaseError(); diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 376a5e40d..163cad13a 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -439,6 +439,8 @@ class ActiveSubscription: class AdvancedCommerceInfoIOS: ## The items purchased as part of this transaction var items: Array[AdvancedCommerceItemIOS] = [] + ## Subscription period for this transaction + var period: SubscriptionPeriodValueIOS ## Request reference identifier for tracking var request_reference_id: Variant = null ## Tax code for the transaction @@ -465,6 +467,11 @@ class AdvancedCommerceInfoIOS: elif item is AdvancedCommerceItemIOS: arr.append(item) obj.items = arr + if data.has("period") and data["period"] != null: + if data["period"] is Dictionary: + obj.period = SubscriptionPeriodValueIOS.from_dict(data["period"]) + else: + obj.period = data["period"] if data.has("requestReferenceId") and data["requestReferenceId"] != null: obj.request_reference_id = data["requestReferenceId"] if data.has("taxCode") and data["taxCode"] != null: @@ -493,6 +500,10 @@ class AdvancedCommerceInfoIOS: dict["items"] = arr else: dict["items"] = null + if period != null and period.has_method("to_dict"): + dict["period"] = period.to_dict() + else: + dict["period"] = period if request_reference_id != null: dict["requestReferenceId"] = request_reference_id if tax_code != null: @@ -5348,7 +5359,7 @@ class Query: const return_type = "String" const is_array = false - ## Read the App Store-promoted product, if any (iOS 11+). See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios + ## Read the App Store-promoted product, if any (iOS 15+). OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an externally redeemed win-back offer, OpenIAP preserves it for the next matching requestPurchase unless the caller supplies an explicit win-back or promotional offer. See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios class getPromotedProductIOSField: const name = "getPromotedProductIOS" const snake_name = "get_promoted_product_ios" @@ -5785,7 +5796,7 @@ class Mutation: const return_type = "Boolean" const is_array = false - ## Show the App Store offer code redemption sheet. On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified transaction produced by the redemption. Earlier iOS and Mac Catalyst versions present the legacy sheet and return null; reconcile purchases through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + ## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios class presentCodeRedemptionSheetIOSField: const name = "presentCodeRedemptionSheetIOS" const snake_name = "present_code_redemption_sheet_ios" @@ -6073,7 +6084,7 @@ static func has_active_subscriptions_args(subscription_ids: Variant = null) -> D static func get_storefront_args() -> Dictionary: return {} -## Read the App Store-promoted product, if any (iOS 11+). See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios +## Read the App Store-promoted product, if any (iOS 15+). OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an externally redeemed win-back offer, OpenIAP preserves it for the next matching requestPurchase unless the caller supplies an explicit win-back or promotional offer. See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios static func get_promoted_product_ios_args() -> Dictionary: return {} @@ -6246,7 +6257,7 @@ static func begin_refund_request_ios_args(sku: String) -> Dictionary: static func sync_ios_args() -> Dictionary: return {} -## Show the App Store offer code redemption sheet. On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified transaction produced by the redemption. Earlier iOS and Mac Catalyst versions present the legacy sheet and return null; reconcile purchases through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios +## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios static func present_code_redemption_sheet_ios_args() -> Dictionary: return {} diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index ff1896b4f..c8de204f6 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -46,6 +46,8 @@ export interface AdvancedCommerceInfoIOS { estimatedTax?: (string | null); /** The items purchased as part of this transaction */ items: AdvancedCommerceItemIOS[]; + /** Subscription period for this transaction */ + period?: (SubscriptionPeriodValueIOS | null); /** Request reference identifier for tracking */ requestReferenceId?: (string | null); /** Tax code for the transaction */ @@ -809,11 +811,14 @@ export interface Mutation { openRedeemOfferCodeAndroid: Promise; /** * Show the App Store offer code redemption sheet. - * On iOS 27+, Mac Catalyst 27+, and visionOS 27+, returns the verified - * transaction produced by the redemption. Earlier iOS and Mac Catalyst - * versions present the legacy sheet and return null; reconcile purchases - * through the normal transaction listener or an explicit available-purchases - * refresh. + * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or + * visionOS 27+, returns the verified transaction produced by the redemption. + * Other supported paths present the system sheet and return null: StoreKit 2's + * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 + * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac + * Catalyst 15. + * Reconcile null results through the normal transaction listener or an explicit + * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ presentCodeRedemptionSheetIOS?: Promise<(PurchaseIOS | null)>; @@ -1427,7 +1432,12 @@ export interface Query { */ getPendingTransactionsIOS: Promise; /** - * Read the App Store-promoted product, if any (iOS 11+). + * Read the App Store-promoted product, if any (iOS 15+). + * OpenIAP consumes PurchaseIntent.intents on iOS 16.4+ and uses the + * StoreKit 1 observer only on iOS 15–16.3. When PurchaseIntent carries an + * externally redeemed win-back offer, OpenIAP preserves it for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. * See: https://openiap.dev/docs/apis/ios/get-promoted-product-ios */ getPromotedProductIOS?: Promise<(ProductIOS | null)>; @@ -1864,7 +1874,12 @@ export interface Subscription { * openiap-google 2.3.0 (requires Play Billing 9.1.0+). */ developerProvidedBillingAndroid: DeveloperProvidedBillingDetailsAndroid; - /** Fires when the App Store surfaces a promoted product (iOS only) */ + /** + * Fires when the App Store surfaces a promoted product (iOS only). + * A win-back offer attached to PurchaseIntent is preserved for the next + * matching requestPurchase unless the caller supplies an explicit win-back or + * promotional offer. + */ promotedProductIOS: string; /** Fires when a purchase fails or is cancelled */ purchaseError: PurchaseError; diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index 8e90ddf26..186ea30d8 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -728,6 +728,10 @@ type AdvancedCommerceInfoIOS { """ items: [AdvancedCommerceItemIOS!]! """ + Subscription period for this transaction + """ + period: SubscriptionPeriodValueIOS + """ Request reference identifier for tracking """ requestReferenceId: String From ddd0eb5955474ce47b8838ed3b5539f773681ddf Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:18:04 +0900 Subject: [PATCH 2/8] fix(apple): modernize StoreKit integrations Preserve PurchaseIntent win-back offers, select current entitlements deterministically, modernize offer-code redemption, and keep AppTransaction compatibility across supported toolchains. --- .../Screens/OfferCodeScreen.swift | 2 +- .../Screens/uis/TestingNotesCard.swift | 3 +- packages/apple/Sources/Helpers/IapState.swift | 15 ++ .../Sources/Helpers/StoreKitTypesBridge.swift | 74 ++++++--- packages/apple/Sources/OpenIapModule.swift | 149 +++++++++++++++--- packages/apple/Tests/OpenIapTests.swift | 68 ++++++++ 6 files changed, 268 insertions(+), 43 deletions(-) diff --git a/packages/apple/Example/OpenIapExample/Screens/OfferCodeScreen.swift b/packages/apple/Example/OpenIapExample/Screens/OfferCodeScreen.swift index 2cd3c4a81..09de0c2b4 100644 --- a/packages/apple/Example/OpenIapExample/Screens/OfferCodeScreen.swift +++ b/packages/apple/Example/OpenIapExample/Screens/OfferCodeScreen.swift @@ -132,7 +132,7 @@ struct OfferCodeScreen: View { if let purchase { print("✅ [OfferCode] Verified redemption: \(purchase.productId) (\(purchase.id))") } else { - print("✅ [OfferCode] Legacy redemption sheet presented; refresh purchases after completion") + print("✅ [OfferCode] System redemption sheet presented; refresh purchases after completion") } } catch { await MainActor.run { diff --git a/packages/apple/Example/OpenIapExample/Screens/uis/TestingNotesCard.swift b/packages/apple/Example/OpenIapExample/Screens/uis/TestingNotesCard.swift index 6cd6b341b..3c0104974 100644 --- a/packages/apple/Example/OpenIapExample/Screens/uis/TestingNotesCard.swift +++ b/packages/apple/Example/OpenIapExample/Screens/uis/TestingNotesCard.swift @@ -14,7 +14,7 @@ struct TestingNotesCard: View { VStack(alignment: .leading, spacing: 12) { TestingNote( icon: "checkmark.circle", - text: "Requires iOS 14.0+ for offer code redemption", + text: "Requires iOS 15.0+ for offer code redemption", color: AppColors.success ) @@ -47,4 +47,3 @@ struct TestingNotesCard: View { .padding(.horizontal) } } - diff --git a/packages/apple/Sources/Helpers/IapState.swift b/packages/apple/Sources/Helpers/IapState.swift index 49bb78abe..9a1be0ec0 100644 --- a/packages/apple/Sources/Helpers/IapState.swift +++ b/packages/apple/Sources/Helpers/IapState.swift @@ -249,3 +249,18 @@ actor IapState { listenerRegistry.hasSubscriptionBillingIssueListeners() } } + +/// Keeps the StoreKit offer attached to a promoted purchase intent until the +/// app starts the matching purchase. The generic form makes the one-shot, +/// product-scoped behavior testable without constructing StoreKit offers. +actor PromotedPurchaseIntentOfferStore { + private var offersByProductId: [String: Offer] = [:] + + func record(_ offer: Offer?, for productId: String) { + offersByProductId[productId] = offer + } + + func take(for productId: String) -> Offer? { + offersByProductId.removeValue(forKey: productId) + } +} diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift index 1cc74e60d..fa0bf16c9 100644 --- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift +++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift @@ -380,7 +380,11 @@ enum StoreKitTypesBridge { } } - static func purchaseOptions(from props: some IosPropsProtocol, product: StoreKit.Product? = nil) throws -> Set { + static func purchaseOptions( + from props: some IosPropsProtocol, + product: StoreKit.Product? = nil, + purchaseIntentOffer: StoreKit.Product.SubscriptionOffer? = nil + ) throws -> Set { var options: Set = [] if let quantity = props.quantity, quantity > 1 { options.insert(.quantity(quantity)) @@ -493,6 +497,13 @@ enum StoreKitTypesBridge { message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." ) } + } else if let purchaseIntentOffer, + props.withOffer == nil, + subscriptionProps.promotionalOfferJWS == nil { + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + options.insert(.winBackOffer(purchaseIntentOffer)) + OpenIapLog.debug("✅ Added win-back offer from PurchaseIntent") + } } // JWS Promotional Offer (iOS 15+, WWDC 2025) @@ -754,6 +765,35 @@ enum StoreKitTypesBridge { return nil } } + + static func standardizedDiscountOfferType( + from type: StoreKit.Product.SubscriptionOffer.OfferType + ) -> DiscountOfferType? { + #if compiler(>=6.1) + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *), + type == .winBack { + return nil + } + #endif + if type == .introductory { return .introductory } + if type == .promotional { return .promotional } + return nil + } + + static func purchaseOfferTypeString(from type: StoreKit.Transaction.OfferType) -> String { + #if compiler(>=6.1) + if type == .winBack { + return SubscriptionOfferTypeIOS.winBack.rawValue + } + #endif + if type == .introductory { + return SubscriptionOfferTypeIOS.introductory.rawValue + } + if type == .promotional { + return SubscriptionOfferTypeIOS.promotional.rawValue + } + return String(describing: type) + } } @available(iOS 15.0, macOS 14.0, tvOS 16.0, watchOS 8.0, *) @@ -797,8 +837,10 @@ private extension StoreKitTypesBridge { #if compiler(>=6.3) @available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) static func makeSubscriptionPricingTerm(from terms: StoreKit.Product.SubscriptionInfo.PricingTerms) -> SubscriptionPricingTermsIOS { - let offers = terms.subscriptionOffers.map { offer in - makeStandardizedSubscriptionOffer(from: offer, type: discountOfferType(from: offer)) + let offers = terms.subscriptionOffers.compactMap { offer in + standardizedDiscountOfferType(from: offer.type).map { type in + makeStandardizedSubscriptionOffer(from: offer, type: type) + } } return SubscriptionPricingTermsIOS( billingDisplayPrice: terms.billingDisplayPrice, @@ -907,15 +949,6 @@ private extension StoreKitTypesBridge { ) } - static func discountOfferType(from offer: StoreKit.Product.SubscriptionOffer) -> DiscountOfferType { - #if compiler(>=6.1) - if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { - return offer.type == .introductory ? .introductory : .promotional - } - #endif - return .promotional - } - static func introductoryPriceAmount(from offer: StoreKit.Product.SubscriptionOffer?) -> String? { guard let price = offer?.price else { return nil } return String(NSDecimalNumber(decimal: price).doubleValue) @@ -1050,16 +1083,7 @@ private extension StoreKitTypesBridge { paymentModeString = PaymentModeIOS.empty.rawValue } - // Map offer type to SubscriptionOfferTypeIOS enum - let typeString: String - switch offer.type { - case .introductory: - typeString = SubscriptionOfferTypeIOS.introductory.rawValue - case .promotional: - typeString = SubscriptionOfferTypeIOS.promotional.rawValue - default: - typeString = String(describing: offer.type) - } + let typeString = purchaseOfferTypeString(from: offer.type) return PurchaseOfferIOS( id: offer.id ?? "", @@ -1172,6 +1196,12 @@ private extension StoreKitTypesBridge { displayName: info.displayName, estimatedTax: decimalString(info.estimatedTax), items: items, + period: info.period.map { + SubscriptionPeriodValueIOS( + unit: $0.unit.subscriptionPeriodIOS, + value: $0.value + ) + }, requestReferenceId: info.requestReferenceID, taxCode: info.taxCode, taxExclusivePrice: decimalString(info.taxExclusivePrice), diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 4c3ec087a..e6c915ea8 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -6,6 +6,18 @@ private struct IndexedProductEntry: @unchecked Sendable { let index: Int let entry: OpenIAP.ProductOrSubscription } + +struct EntitlementSelectionKey: Comparable { + let purchaseDate: Date + let transactionId: UInt64 + + static func < (lhs: Self, rhs: Self) -> Bool { + if lhs.purchaseDate != rhs.purchaseDate { + return lhs.purchaseDate < rhs.purchaseDate + } + return lhs.transactionId < rhs.transactionId + } +} // UIKit: Required for UIApplication, UIWindowScene on iOS/tvOS/visionOS #if canImport(UIKit) import UIKit @@ -114,6 +126,9 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { private let promotedPurchaseObserverLock = NSLock() private var didRegisterPromotedPurchaseObserver = false private var isPromotedPurchaseObserverTransitionInFlight = false + private var promotedPurchaseIntentTask: Task? + private let promotedPurchaseIntentOffers = + PromotedPurchaseIntentOfferStore() #endif private enum SubscriptionPreflightOutcome { @@ -123,10 +138,14 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { private override init() { super.init() + startPromotedPurchaseIntentListenerIfAvailable() registerPromotedPurchaseObserverIfNeeded() } deinit { + #if os(iOS) + promotedPurchaseIntentTask?.cancel() + #endif unregisterPromotedPurchaseObserverIfNeeded() cancelConnectionTasksForDeinit() } @@ -363,7 +382,16 @@ 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, product: product) + #if os(iOS) + let purchaseIntentOffer = await promotedPurchaseIntentOffers.take(for: sku) + #else + let purchaseIntentOffer: StoreKit.Product.SubscriptionOffer? = nil + #endif + let options = try StoreKitTypesBridge.purchaseOptions( + from: iosProps, + product: product, + purchaseIntentOffer: purchaseIntentOffer + ) if StoreKitTypesBridge.isAutoRenewingSubscriptionProductType(product.type) { await preflightInactiveUnfinishedSubscriptions(productId: sku) @@ -1149,15 +1177,48 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { public func currentEntitlementIOS(sku: String) async throws -> PurchaseIOS? { try await ensureConnection() let product = try await storeProduct(for: sku) - guard let result = await product.currentEntitlement else { return nil } - do { - let transaction = try checkVerified(result) - return await StoreKitTypesBridge.purchaseIOS(from: transaction, jwsRepresentation: result.jwsRepresentation) - } catch { - let error = makePurchaseError(code: .transactionValidationFailed, message: error.localizedDescription) - emitPurchaseError(error) - throw error + + let entitlements: Transaction.Transactions + #if compiler(>=6.1) + if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) { + entitlements = product.currentEntitlements + } else { + entitlements = Transaction.currentEntitlements } + #else + entitlements = Transaction.currentEntitlements + #endif + + var latest: ( + key: EntitlementSelectionKey, + transaction: StoreKit.Transaction, + jwsRepresentation: String + )? + for await result in entitlements { + guard result.unsafePayloadValue.productID == sku else { continue } + do { + let transaction = try checkVerified(result) + let key = EntitlementSelectionKey( + purchaseDate: transaction.purchaseDate, + transactionId: transaction.id + ) + if latest.map({ $0.key < key }) ?? true { + latest = (key, transaction, result.jwsRepresentation) + } + } catch { + let error = makePurchaseError( + code: .transactionValidationFailed, + message: error.localizedDescription + ) + emitPurchaseError(error) + throw error + } + } + guard let latest else { return nil } + return await StoreKitTypesBridge.purchaseIOS( + from: latest.transaction, + jwsRepresentation: latest.jwsRepresentation + ) } /// Get the latest verified transaction for a product. @@ -1272,10 +1333,10 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } /// Present a sheet for redeeming offer codes. - /// - Note: iOS 27+, Mac Catalyst 27+, and visionOS 27+ return the verified - /// transaction produced by the redemption. Earlier iOS versions present - /// the legacy sheet and return nil, so callers must reconcile through the - /// transaction listener or an explicit available-purchases refresh. + /// - Note: Builds made with Xcode 27+ return the verified transaction on + /// iOS 27+, Mac Catalyst 27+, and visionOS 27+. Other supported paths + /// return nil after presenting the system sheet, so callers must reconcile + /// through the transaction listener or an available-purchases refresh. /// - SeeAlso: https://developer.apple.com/documentation/storekit/appstore/presentoffercoderedeemsheet(from:options:) /// /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios @@ -1284,7 +1345,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { #if compiler(>=6.4) #if os(iOS) || os(visionOS) - if #available(iOS 27.0, visionOS 27.0, *) { + if #available(iOS 27.0, macCatalyst 27.0, visionOS 27.0, *) { guard let viewController = await activeViewController() else { throw makePurchaseError( code: .purchaseError, @@ -1309,8 +1370,26 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { #endif #endif - // The legacy sheet cannot return the redeemed transaction directly. - // It is only available on iOS and Mac Catalyst. + // StoreKit 2's scene-based sheet is available before the result-returning + // Apple 27 API, but cannot return the redeemed transaction directly. + #if os(iOS) || os(visionOS) + if #available(iOS 16.0, macCatalyst 16.0, visionOS 1.0, *) { + guard let scene = await activeWindowScene() else { + throw makePurchaseError( + code: .purchaseError, + message: "Cannot find an active window scene for offer-code redemption" + ) + } + do { + try await AppStore.presentOfferCodeRedeemSheet(in: scene) + return nil + } catch { + throw PurchaseError.wrap(error, fallback: .purchaseError) + } + } + #endif + + // iOS 15 requires the original StoreKit sheet. #if os(iOS) await MainActor.run { SKPaymentQueue.default().presentCodeRedemptionSheet() @@ -1700,8 +1779,37 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { resources.unfinishedTransactionTask?.cancel() } + private func startPromotedPurchaseIntentListenerIfAvailable() { + #if os(iOS) + if #available(iOS 16.4, macCatalyst 16.4, *) { + guard promotedPurchaseIntentTask == nil else { return } + promotedPurchaseIntentTask = Task { [weak self] in + for await intent in PurchaseIntent.intents { + guard !Task.isCancelled, let self else { return } + let offer: StoreKit.Product.SubscriptionOffer? + if #available(iOS 18.0, macCatalyst 18.0, *) { + offer = intent.offer + } else { + offer = nil + } + await self.promotedPurchaseIntentOffers.record( + offer, + for: intent.product.id + ) + if let productManager = self.connection.currentProductManager() { + await productManager.addProduct(intent.product) + } + self.emitPromotedProduct(intent.product.id) + } + } + } + #endif + } + private func registerPromotedPurchaseObserverIfNeeded() { #if os(iOS) + if #available(iOS 16.4, macCatalyst 16.4, *) { return } + promotedPurchaseObserverLock.lock() let shouldRegister = !didRegisterPromotedPurchaseObserver && !isPromotedPurchaseObserverTransitionInFlight @@ -1731,6 +1839,8 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { private func unregisterPromotedPurchaseObserverIfNeeded() { #if os(iOS) + if #available(iOS 16.4, macCatalyst 16.4, *) { return } + promotedPurchaseObserverLock.lock() let shouldUnregister = didRegisterPromotedPurchaseObserver && !isPromotedPurchaseObserverTransitionInFlight @@ -2412,11 +2522,14 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { var revocationDateValue: Double? = nil var storeTypeValue: String? = nil - // Swift 6.1 compiler+ (Xcode 16.4+): AppTransaction.appTransactionID and originalPlatform available + // Swift 6.1 compiler+ (Xcode 16.4+): appTransactionID is back-deployed + // to the AppTransaction baseline; originalPlatform itself starts at 18.4. #if compiler(>=6.1) + appTransactionId = transaction.appTransactionID if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) { - appTransactionId = String(transaction.appTransactionID) originalPlatformValue = transaction.originalPlatform.rawValue + } else { + originalPlatformValue = transaction.originalPlatformStringRepresentation } #endif // compiler(>=6.1) diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift index 0b35736dd..c6b4bab29 100644 --- a/packages/apple/Tests/OpenIapTests.swift +++ b/packages/apple/Tests/OpenIapTests.swift @@ -45,6 +45,42 @@ final class OpenIapTests: XCTestCase { } } + func testEntitlementSelectionUsesLatestDateThenTransactionId() { + let earlier = EntitlementSelectionKey( + purchaseDate: Date(timeIntervalSince1970: 1), + transactionId: 100 + ) + let later = EntitlementSelectionKey( + purchaseDate: Date(timeIntervalSince1970: 2), + transactionId: 1 + ) + let sameDateHigherId = EntitlementSelectionKey( + purchaseDate: later.purchaseDate, + transactionId: 2 + ) + + XCTAssertLessThan(earlier, later) + XCTAssertLessThan(later, sameDateHigherId) + } + + func testPromotedPurchaseIntentOfferIsProductScopedAndOneShot() async { + let store = PromotedPurchaseIntentOfferStore() + await store.record("win-back", for: "subscription") + + let unrelated = await store.take(for: "other") + let matching = await store.take(for: "subscription") + let consumed = await store.take(for: "subscription") + + XCTAssertNil(unrelated) + XCTAssertEqual(matching, "win-back") + XCTAssertNil(consumed) + + await store.record("stale", for: "subscription") + await store.record(nil, for: "subscription") + let cleared = await store.take(for: "subscription") + XCTAssertNil(cleared) + } + func testConnectedGenerationIsInvalidatedWhenEndingBegins() async throws { let lifecycle = OpenIapConnectionLifecycle() let initWork = try XCTUnwrap(lifecycle.makeInitTask { _ in true }) @@ -841,6 +877,38 @@ final class OpenIapTests: XCTestCase { XCTAssertTrue(jsonString.contains("promo_code_abc")) } + func testAdvancedCommerceInfoPeriodSerialization() throws { + let info = AdvancedCommerceInfoIOS( + items: [], + period: SubscriptionPeriodValueIOS(unit: .month, value: 1) + ) + + let data = try JSONEncoder().encode(info) + let decoded = try JSONDecoder().decode(AdvancedCommerceInfoIOS.self, from: data) + + XCTAssertEqual(decoded.period?.unit, .month) + XCTAssertEqual(decoded.period?.value, 1) + } + + #if compiler(>=6.1) + @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *) + func testStoreKitOfferTypeMappingsPreserveWinBack() { + XCTAssertEqual( + StoreKitTypesBridge.standardizedDiscountOfferType(from: .introductory), + .introductory + ) + XCTAssertEqual( + StoreKitTypesBridge.standardizedDiscountOfferType(from: .promotional), + .promotional + ) + XCTAssertNil(StoreKitTypesBridge.standardizedDiscountOfferType(from: .winBack)) + XCTAssertEqual( + StoreKitTypesBridge.purchaseOfferTypeString(from: .winBack), + SubscriptionOfferTypeIOS.winBack.rawValue + ) + } + #endif + // MARK: - Subscription-Only Props Tests func testRequestSubscriptionIosPropsWithWinBackOffer() throws { From 14ee73ca57f9ae6665e59299009d4b0b4e3de20d Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:18:24 +0900 Subject: [PATCH 3/8] fix(google): harden billing integration Guard suspended-subscription queries by Play capability, require a foreground Horizon activity, enforce single-product requests, and isolate composite build outputs. --- packages/google/build.gradle.kts | 12 +++++ packages/google/openiap/build.gradle.kts | 23 +++++++-- .../java/dev/hyo/openiap/OpenIapModule.kt | 47 ++++++++++++------- .../java/dev/hyo/openiap/helpers/Helpers.kt | 17 ++++--- .../hyo/openiap/HorizonPurchaseSafetyTest.kt | 23 +++++++++ .../dev/hyo/openiap/QueryPurchasesRaceTest.kt | 45 +++++++++++++++++- 6 files changed, 137 insertions(+), 30 deletions(-) diff --git a/packages/google/build.gradle.kts b/packages/google/build.gradle.kts index 380a79acb..fd2266b39 100644 --- a/packages/google/build.gradle.kts +++ b/packages/google/build.gradle.kts @@ -22,6 +22,18 @@ val androidVersion = System.getenv("ORG_GRADLE_PROJECT_openIapVersion") ?: run { extra["OPENIAP_VERSION"] = androidVersion +// Composite consumers must not reuse packages/google/build with standalone or +// sibling consumer builds. AGP and Kotlin keep variant intermediates in that +// directory, and different composite roots can otherwise invalidate each +// other's outputs while Gradle still considers downstream tasks up to date. +gradle.parent?.rootProject?.let { consumerRoot -> + val isolatedBuildRoot = consumerRoot.layout.buildDirectory + .get() + .asFile + .resolve("included-openiap-google") + layout.buildDirectory.set(isolatedBuildRoot) +} + // Configure Maven Central publishing at the root. // Credentials are sourced from env or gradle.properties. // Maven Central publishing is configured per-module via Vanniktech plugin. diff --git a/packages/google/openiap/build.gradle.kts b/packages/google/openiap/build.gradle.kts index 463d43b8e..5fd1fe40d 100644 --- a/packages/google/openiap/build.gradle.kts +++ b/packages/google/openiap/build.gradle.kts @@ -44,10 +44,25 @@ if (!usesBuiltInKotlin) { } pluginManager.apply("org.jetbrains.kotlin.plugin.compose") -// Consumer examples include this module in their own Gradle builds. Applying -// the publication plugin there unnecessarily couples them to the standalone -// Google package's AGP version (Vanniktech 0.37 requires AGP 8.13+). -val isStandaloneGoogleBuild = rootProject.projectDir.canonicalFile == projectDir.parentFile.canonicalFile +// Consumer examples include this module in their own Gradle builds, while KMP +// and MAUI consume the Google root as a composite build. Keep every consumer's +// intermediates under its own root build directory so sequential or parallel +// builds cannot corrupt packages/google/openiap/build. Applying the publication +// plugin there would also couple consumers to the standalone publishing setup. +val isStandaloneGoogleBuild = + gradle.parent == null && + rootProject.projectDir.canonicalFile == projectDir.parentFile.canonicalFile +if (!isStandaloneGoogleBuild) { + layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google")) + tasks.configureEach { + // The same source project is embedded by hosts that intentionally use + // different AGP/Kotlin versions. Gradle's shared build cache can restore + // a host-incompatible classes jar even though the isolated build path is + // correct. Keep normal up-to-date checks, but never exchange cached task + // outputs between embedded consumers. + outputs.doNotCacheIf("embedded OpenIAP Google host toolchains differ") { true } + } +} if (isStandaloneGoogleBuild) { pluginManager.apply("com.vanniktech.maven.publish") } diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt index de62e36d1..f4dda7299 100644 --- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt +++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt @@ -180,6 +180,14 @@ internal fun horizonUnsupportedPurchaseError( else -> null } +internal fun horizonPurchaseSkuCountError(skus: Collection): OpenIapError? = when { + skus.isEmpty() -> OpenIapError.EmptySkuList + skus.size != 1 -> OpenIapError.DeveloperError( + "Meta Horizon Billing purchases one SKU at a time" + ) + else -> null +} + internal fun matchesRequestedProductIds( primaryProductId: String, productIds: Collection, @@ -491,12 +499,25 @@ class OpenIapModule( } ?: false } - val contextForInit = currentActivityRef?.get() ?: fallbackActivity ?: context - OpenIapLog.debug("Building BillingClient with ${contextForInit.javaClass.simpleName}...", TAG) + val activityForInit = currentActivityRef?.get() ?: fallbackActivity + if (activityForInit == null) { + val error = OpenIapError.MissingCurrentActivity + synchronized(connectionLifecycleLock) { + if ( + connectionAttempt === attempt && + connectionGeneration == attempt.generation + ) { + connectionAttempt = null + } + } + attempt.completion.completeExceptionally(error) + return@withContext attempt.completion.await() + } + OpenIapLog.debug("Building BillingClient with ${activityForInit.javaClass.simpleName}...", TAG) lateinit var client: BillingClient client = runCatching { buildBillingClient( - contextForInit, + activityForInit, PurchasesUpdatedListener { result, purchases -> onPurchasesUpdated(client, attempt.generation, result, purchases) }, @@ -899,9 +920,8 @@ class OpenIapModule( return@withContext emptyList() } - if (androidArgs.skus.isEmpty()) { - val err = OpenIapError.EmptySkuList - emitPurchaseError(err) + horizonPurchaseSkuCountError(androidArgs.skus)?.let { error -> + emitPurchaseError(error) return@withContext emptyList() } @@ -1744,27 +1764,22 @@ class OpenIapModule( * Build BillingClient with the provided context. * * CRITICAL: Horizon SDK requires Activity to properly initialize OVRPlatform with returnComponent. - * If Context (non-Activity) is provided, Horizon SDK will run in limited mode and may cause - * NullPointerException during purchase flow. + * initConnection rejects a missing Activity before reaching this builder. * - * @param contextForBilling Activity (preferred) or Application Context (fallback) + * @param activity Activity required by the Horizon Billing Compatibility SDK */ private fun buildBillingClient( - contextForBilling: Context, + activity: Activity, listener: PurchasesUpdatedListener, ): BillingClient { - if (contextForBilling is Activity) { - OpenIapLog.debug("Building BillingClient with Activity", TAG) - } else { - OpenIapLog.warn("Building BillingClient with Context (not Activity) - Horizon SDK will run in limited mode", TAG) - } + OpenIapLog.debug("Building BillingClient with Activity", TAG) val pendingPurchasesParams = com.meta.horizon.billingclient.api.PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build() val builder = BillingClient - .newBuilder(contextForBilling) + .newBuilder(activity) .setListener(listener) .enablePendingPurchases(pendingPurchasesParams) 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 6a7f306a7..9d7c081e3 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 @@ -45,15 +45,14 @@ internal suspend fun queryPurchases( // 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) - } + if ( + productType == BillingClient.ProductType.SUBS && + includeSuspended && + billingClient.isFeatureSupported( + BillingClient.FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS + ).responseCode == BillingClient.BillingResponseCode.OK + ) { + paramsBuilder.includeSuspendedSubscriptions(true) } val params = paramsBuilder.build() diff --git a/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/HorizonPurchaseSafetyTest.kt b/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/HorizonPurchaseSafetyTest.kt index 24db58735..807424e97 100644 --- a/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/HorizonPurchaseSafetyTest.kt +++ b/packages/google/openiap/src/testHorizon/java/dev/hyo/openiap/HorizonPurchaseSafetyTest.kt @@ -189,6 +189,29 @@ class HorizonPurchaseSafetyTest { ) } + @Test + fun `Horizon purchase requires exactly one sku`() { + assertTrue(horizonPurchaseSkuCountError(emptyList()) is OpenIapError.EmptySkuList) + assertNull(horizonPurchaseSkuCountError(listOf("coins"))) + + val multipleSkuError = horizonPurchaseSkuCountError(listOf("coins", "gems")) + assertTrue(multipleSkuError is OpenIapError.DeveloperError) + assertEquals( + "Meta Horizon Billing purchases one SKU at a time", + multipleSkuError?.debugMessage, + ) + } + + @Test + fun `Horizon initConnection requires a current Activity`() = runTest { + val context = ApplicationProvider.getApplicationContext() + val module = OpenIapModule(context) + + val thrown = runCatching { module.initConnection(null) }.exceptionOrNull() + + assertTrue(thrown is OpenIapError.MissingCurrentActivity) + } + @Test fun `subscription management deep link fails fast when unsupported`() = runTest { val context = ApplicationProvider.getApplicationContext() diff --git a/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt b/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt index c30ed7f39..034797eca 100644 --- a/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt +++ b/packages/google/openiap/src/testPlay/java/dev/hyo/openiap/QueryPurchasesRaceTest.kt @@ -51,6 +51,7 @@ import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.thread import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -248,6 +249,37 @@ class QueryPurchasesRaceTest { assertEquals("owned purchase query failed", error?.debugMessage) } + @Test + fun `queryPurchases includes suspended subscriptions when requested`() = runTest { + val client = DuplicateBillingClient() + + queryPurchases( + client, + operations(client), + BillingClient.ProductType.SUBS, + includeSuspended = true, + ) + + assertTrue(client.lastQueryPurchasesParams!!.getIncludeSuspendedSubscriptions()) + } + + @Test + fun `queryPurchases falls back when suspended subscriptions are unsupported`() = runTest { + val client = DuplicateBillingClient( + suspendedSubscriptionsFeatureResponseCode = + BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED, + ) + + queryPurchases( + client, + operations(client), + BillingClient.ProductType.SUBS, + includeSuspended = true, + ) + + assertFalse(client.lastQueryPurchasesParams!!.getIncludeSuspendedSubscriptions()) + } + @Test fun `restorePurchases rejects partial success`() = runTest { val client = DuplicateBillingClient( @@ -317,9 +349,13 @@ class QueryPurchasesRaceTest { private val purchases: List = emptyList(), private val throwsOnQueryPurchases: Boolean = false, purchaseResponseCodes: List = emptyList(), + private val suspendedSubscriptionsFeatureResponseCode: Int = + BillingClient.BillingResponseCode.OK, ) : BillingClient() { val callbackFailures = Collections.synchronizedList(mutableListOf()) val productDetailsQueryCount = AtomicInteger(0) + var lastQueryPurchasesParams: QueryPurchasesParams? = null + private set private val purchaseResponseCodes = java.util.concurrent.ConcurrentLinkedQueue(purchaseResponseCodes) @@ -327,6 +363,7 @@ class QueryPurchasesRaceTest { params: QueryPurchasesParams, listener: PurchasesResponseListener ) { + lastQueryPurchasesParams = params if (throwsOnQueryPurchases) { throw IllegalStateException("query failed") } @@ -418,7 +455,13 @@ class QueryPurchasesRaceTest { override fun getConnectionState(): Int = ConnectionState.CONNECTED override fun isFeatureSupported(feature: String): BillingResult = - unsupported() + if (feature == FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS) { + BillingResult.newBuilder() + .setResponseCode(suspendedSubscriptionsFeatureResponseCode) + .build() + } else { + unsupported() + } override fun launchBillingFlow( activity: Activity, From 3e94ee5313d9d879dc083ca68e8aa1bd897f5272 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:19:28 +0900 Subject: [PATCH 4/8] fix: align framework store integrations Propagate current StoreKit and Billing behavior across React Native, Expo, Flutter, Godot, KMP, and MAUI examples, bridges, tests, and build isolation. --- .../java/expo/modules/iap/ExpoIapModule.kt | 2 +- .../example/__tests__/offer-code.test.tsx | 4 +- libraries/expo-iap/example/app/offer-code.tsx | 2 +- .../src/__tests__/withLocalOpenIAP.test.ts | 7 ++++ .../expo-iap/plugin/src/withLocalOpenIAP.ts | 12 +++++- libraries/expo-iap/src/modules/ios.ts | 6 ++- .../lib/src/screens/offer_code_screen.dart | 2 +- .../test/ios_methods_test.dart | 2 +- .../Example/tests/test_types_only.gd | 14 +++++++ .../godot-iap/addons/godot-iap/godot_iap.gd | 4 +- .../dev/hyo/martie/screens/OfferCodeScreen.kt | 2 +- .../hyochan/kmpiap/InAppPurchaseAndroid.kt | 37 ++++++++++------- .../kmpiap/BillingQueryLifecycleTest.kt | 41 ++++++++++++++++++- .../kmpiap/ProductPayloadNormalizerTestIOS.kt | 7 ++++ .../Pages/OfferCodePage.xaml.cs | 2 +- .../OpenIap.Maui.Tests/RecordJsonTests.cs | 6 +++ .../java/com/margelo/nitro/iap/HybridRnIap.kt | 2 +- .../example/screens/OfferCode.tsx | 2 +- .../react-native-iap/ios/RnIapHelper.swift | 20 +++++++++ libraries/react-native-iap/src/index.ts | 6 ++- .../react-native-iap/src/specs/RnIap.nitro.ts | 6 ++- 21 files changed, 151 insertions(+), 35 deletions(-) diff --git a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt index 9e11f929a..9e9efd7db 100644 --- a/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +++ b/libraries/expo-iap/android/src/main/java/expo/modules/iap/ExpoIapModule.kt @@ -130,7 +130,7 @@ class ExpoIapModule : Module() { ExpoIapLog.debug("Activity available: ${it.javaClass.name}") openIap.setActivity(it) }.onFailure { - ExpoIapLog.warning("Activity not available during initConnection - OpenIAP will use Context") + ExpoIapLog.warning("Activity not available during initConnection") } // If already connected, short-circuit diff --git a/libraries/expo-iap/example/__tests__/offer-code.test.tsx b/libraries/expo-iap/example/__tests__/offer-code.test.tsx index 83f1b39c1..03023d164 100644 --- a/libraries/expo-iap/example/__tests__/offer-code.test.tsx +++ b/libraries/expo-iap/example/__tests__/offer-code.test.tsx @@ -131,7 +131,7 @@ describe('OfferCode Component', () => { }); }); - it('should explain the legacy iOS redemption result', async () => { + it('should explain a nil iOS redemption result', async () => { Object.defineProperty(Platform, 'OS', { get: jest.fn(() => 'ios'), configurable: true, @@ -146,7 +146,7 @@ describe('OfferCode Component', () => { await waitFor(() => { expect(Alert.alert).toHaveBeenCalledWith( 'Redemption Sheet Presented', - 'This iOS version uses the legacy sheet. Refresh available purchases after completing redemption.', + 'The system sheet did not return a transaction directly. Refresh available purchases after completing redemption.', ); }); }); diff --git a/libraries/expo-iap/example/app/offer-code.tsx b/libraries/expo-iap/example/app/offer-code.tsx index 6d09d983d..b69937ae6 100644 --- a/libraries/expo-iap/example/app/offer-code.tsx +++ b/libraries/expo-iap/example/app/offer-code.tsx @@ -90,7 +90,7 @@ export default function OfferCodeScreen() { } else { Alert.alert( 'Redemption Sheet Presented', - 'This iOS version uses the legacy sheet. Refresh available purchases after completing redemption.', + 'The system sheet did not return a transaction directly. Refresh available purchases after completing redemption.', ); } } else { diff --git a/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts b/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts index 3afe4c04e..04a3d61fc 100644 --- a/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts +++ b/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts @@ -22,6 +22,9 @@ describe('ensureLocalOpenIapFlavorStrategy', () => { expect(result).toContain('subprojects { subproject ->'); expect(result).toContain('subproject.plugins.withId("com.android.library")'); expect(result).toContain('missingDimensionStrategy "platform", "play"'); + expect(result).toContain( + 'layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google"))', + ); }); it('emits Kotlin DSL for Kotlin project build files', () => { @@ -38,6 +41,10 @@ describe('ensureLocalOpenIapFlavorStrategy', () => { expect(result).toContain( 'missingDimensionStrategy("platform", "horizon")', ); + expect(result).toContain('project(":openiap-google")'); + expect(result).toContain( + 'layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google"))', + ); expect(result).not.toContain( 'missingDimensionStrategy "platform", "horizon"', ); diff --git a/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts b/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts index 4920869b1..d03737bea 100644 --- a/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts +++ b/libraries/expo-iap/plugin/src/withLocalOpenIAP.ts @@ -127,7 +127,11 @@ export const ensureLocalOpenIapFlavorStrategy = ( const strategyBlock = language === 'kotlin' - ? `subprojects { + ? `project(":openiap-google") { + layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google")) +} + +subprojects { plugins.withId("com.android.library") { extensions.configure("android") { defaultConfig { @@ -136,7 +140,11 @@ export const ensureLocalOpenIapFlavorStrategy = ( } } }` - : `subprojects { subproject -> + : `project(":openiap-google") { + layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google")) +} + +subprojects { subproject -> subproject.plugins.withId("com.android.library") { subproject.android { defaultConfig { diff --git a/libraries/expo-iap/src/modules/ios.ts b/libraries/expo-iap/src/modules/ios.ts index b57228e4c..e3e3c7823 100644 --- a/libraries/expo-iap/src/modules/ios.ts +++ b/libraries/expo-iap/src/modules/ios.ts @@ -282,8 +282,10 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async ( * * Note: This only works on real devices, not simulators. * - * @returns The verified redeemed purchase on iOS 27+, or null after the - * legacy sheet is presented on earlier iOS versions. + * @returns The verified redeemed purchase when built with Xcode 27+ and + * running on Apple 27+, or null when the system sheet cannot return the + * transaction directly (StoreKit 2 on iOS/Catalyst 16–26 and visionOS 1–26; + * StoreKit 1 on iOS/Catalyst 15). * @throws Error if called on non-iOS platform or tvOS * * @platform iOS diff --git a/libraries/flutter_inapp_purchase/example/lib/src/screens/offer_code_screen.dart b/libraries/flutter_inapp_purchase/example/lib/src/screens/offer_code_screen.dart index 04f607caf..4be6dfe64 100644 --- a/libraries/flutter_inapp_purchase/example/lib/src/screens/offer_code_screen.dart +++ b/libraries/flutter_inapp_purchase/example/lib/src/screens/offer_code_screen.dart @@ -74,7 +74,7 @@ class _OfferCodeScreenState extends State { setState(() { _statusMessage = purchase != null ? 'Verified redemption: ${purchase.productId} (${purchase.id}).' - : 'Legacy redemption sheet presented. Refresh purchases after completing redemption.'; + : 'The system sheet did not return a transaction directly. Refresh purchases after completing redemption.'; _isSuccess = true; }); } catch (e) { diff --git a/libraries/flutter_inapp_purchase/test/ios_methods_test.dart b/libraries/flutter_inapp_purchase/test/ios_methods_test.dart index 797d1461d..a127c24b7 100644 --- a/libraries/flutter_inapp_purchase/test/ios_methods_test.dart +++ b/libraries/flutter_inapp_purchase/test/ios_methods_test.dart @@ -210,7 +210,7 @@ void main() { }, ); - test('presentCodeRedemptionSheetIOS preserves legacy null', () async { + test('presentCodeRedemptionSheetIOS preserves a null result', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { if (methodCall.method == 'presentCodeRedemptionSheetIOS') return null; diff --git a/libraries/godot-iap/Example/tests/test_types_only.gd b/libraries/godot-iap/Example/tests/test_types_only.gd index f8c0f7b3f..2b468b380 100644 --- a/libraries/godot-iap/Example/tests/test_types_only.gd +++ b/libraries/godot-iap/Example/tests/test_types_only.gd @@ -739,6 +739,10 @@ func _test_purchase_ios_json_round_trip() -> void: offer.payment_mode = "payAsYouGo" purchase.offer_ios = offer var advanced_info = Types.AdvancedCommerceInfoIOS.new() + var advanced_period = Types.SubscriptionPeriodValueIOS.new() + advanced_period.unit = Types.SubscriptionPeriodIOS.MONTH + advanced_period.value = 1 + advanced_info.period = advanced_period advanced_info.request_reference_id = "request-reference" advanced_info.display_name = "Premium bundle" var advanced_item = Types.AdvancedCommerceItemIOS.new() @@ -763,6 +767,16 @@ func _test_purchase_ios_json_round_trip() -> void: "request-reference", "advancedCommerceInfoIOS should survive the wire round trip" ) + _assert_equal( + parsed.advanced_commerce_info_ios.period.unit, + Types.SubscriptionPeriodIOS.MONTH, + "advancedCommerceInfoIOS period unit should survive the wire round trip" + ) + _assert_equal( + parsed.advanced_commerce_info_ios.period.value, + 1, + "advancedCommerceInfoIOS period value should survive the wire round trip" + ) _assert_equal( parsed.advanced_commerce_info_ios.items[0].details.json_representation, "{\"sku\":\"ios.sku\"}", diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index 40bd0e263..eb10ea1d4 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -1341,8 +1341,8 @@ func get_all_transactions_ios() -> Array: return purchases ## Present the code redemption sheet (iOS only). -## @return Types.PurchaseIOS on iOS 27+ after verified redemption, or null -## after the legacy sheet is presented on earlier iOS versions. +## @return Types.PurchaseIOS when built with Xcode 27+ and running on Apple 27+, +## or null when the system sheet cannot return the transaction directly. ## ## See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func present_code_redemption_sheet_ios() -> Variant: diff --git a/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/OfferCodeScreen.kt b/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/OfferCodeScreen.kt index 1a55482ca..a8b6a5786 100644 --- a/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/OfferCodeScreen.kt +++ b/libraries/kmp-iap/example/composeApp/src/commonMain/kotlin/dev/hyo/martie/screens/OfferCodeScreen.kt @@ -136,7 +136,7 @@ fun OfferCodeScreen(navController: NavController) { result = if (purchase != null) { "Verified redemption: ${purchase.productId} (${purchase.id})" } else { - "Legacy redemption sheet presented; refresh purchases after completion" + "The system sheet did not return a transaction directly; refresh purchases after completion" } } catch (e: Exception) { result = "Failed to present sheet: ${e.message}" diff --git a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt index db271ecf2..23ef18e8f 100644 --- a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt +++ b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt @@ -226,6 +226,24 @@ internal fun subscriptionUpdateSourceCount( ): Int = listOf(purchaseToken, originalExternalTransactionId) .count { !it.isNullOrBlank() } +internal fun availablePurchasesQueryParams( + client: BillingClient, + productType: String, + includeSuspendedSubscriptions: Boolean, +): QueryPurchasesParams { + val builder = QueryPurchasesParams.newBuilder().setProductType(productType) + if ( + productType == BillingClient.ProductType.SUBS && + includeSuspendedSubscriptions && + client.isFeatureSupported( + BillingClient.FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS, + ).responseCode == BillingClient.BillingResponseCode.OK + ) { + builder.includeSuspendedSubscriptions(true) + } + return builder.build() +} + private fun Any.invokeOptionalStringGetter(methodName: String): String? = runCatching { javaClass.getMethod(methodName).invoke(this) as? String } .getOrNull() @@ -1769,23 +1787,14 @@ internal class InAppPurchaseAndroid( expectedGeneration = expectedGeneration, captureOwner = captureOwner, ) { client, complete -> - val paramsBuilder = QueryPurchasesParams.newBuilder().setProductType(type) - // 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 (type == BillingClient.ProductType.SUBS && includeSuspendedSubs) { - 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() + val params = availablePurchasesQueryParams( + client, + type, + includeSuspendedSubs, + ) client.queryPurchasesAsync(params) { result, purchases -> if (result.responseCode == BillingClient.BillingResponseCode.OK) { complete(Result.success(purchases.map { it.toPurchase() })) diff --git a/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt b/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt index 2e6f17236..61d38cb24 100644 --- a/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt +++ b/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt @@ -62,6 +62,32 @@ import kotlin.test.assertSame import kotlin.test.assertTrue class BillingQueryLifecycleTest { + @Test + fun `available purchases requests suspended subscriptions only when supported`() { + val supportedClient = LifecycleBillingClient( + suspendedSubscriptionsSupported = true, + ) + val subscriptionParams = availablePurchasesQueryParams( + supportedClient, + BillingClient.ProductType.SUBS, + includeSuspendedSubscriptions = true, + ) + val inAppParams = availablePurchasesQueryParams( + supportedClient, + BillingClient.ProductType.INAPP, + includeSuspendedSubscriptions = true, + ) + val unsupportedParams = availablePurchasesQueryParams( + LifecycleBillingClient(suspendedSubscriptionsSupported = false), + BillingClient.ProductType.SUBS, + includeSuspendedSubscriptions = true, + ) + + assertTrue(subscriptionParams.getIncludeSuspendedSubscriptions()) + assertFalse(inAppParams.getIncludeSuspendedSubscriptions()) + assertFalse(unsupportedParams.getIncludeSuspendedSubscriptions()) + } + @Test fun `end fails fetchProducts when its native callback never arrives`() = runTest { @@ -446,6 +472,7 @@ class BillingQueryLifecycleTest { private class LifecycleBillingClient( private val completeFirstProductQuery: Boolean = false, + private val suspendedSubscriptionsSupported: Boolean = false, ) : BillingClient() { @Volatile var ready = true @@ -496,7 +523,19 @@ private class LifecycleBillingClient( override fun endConnection() = Unit - override fun isFeatureSupported(feature: String): BillingResult = unsupported() + override fun isFeatureSupported(feature: String): BillingResult = + if (feature == FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS) { + BillingResult.newBuilder() + .setResponseCode( + if (suspendedSubscriptionsSupported) { + BillingResponseCode.OK + } else { + BillingResponseCode.FEATURE_NOT_SUPPORTED + }, + ).build() + } else { + unsupported() + } override fun launchBillingFlow( activity: Activity, diff --git a/libraries/kmp-iap/library/src/iosTest/kotlin/io/github/hyochan/kmpiap/ProductPayloadNormalizerTestIOS.kt b/libraries/kmp-iap/library/src/iosTest/kotlin/io/github/hyochan/kmpiap/ProductPayloadNormalizerTestIOS.kt index 160cccf3d..66213dd6a 100644 --- a/libraries/kmp-iap/library/src/iosTest/kotlin/io/github/hyochan/kmpiap/ProductPayloadNormalizerTestIOS.kt +++ b/libraries/kmp-iap/library/src/iosTest/kotlin/io/github/hyochan/kmpiap/ProductPayloadNormalizerTestIOS.kt @@ -3,6 +3,7 @@ package io.github.hyochan.kmpiap import io.github.hyochan.kmpiap.openiap.ErrorCode import io.github.hyochan.kmpiap.openiap.ProductSubscriptionIOS import io.github.hyochan.kmpiap.openiap.SubscriptionBillingPlanTypeIOS +import io.github.hyochan.kmpiap.openiap.SubscriptionPeriodIOS import platform.Foundation.NSNull import kotlin.test.Test import kotlin.test.assertEquals @@ -158,6 +159,10 @@ class ProductPayloadNormalizerTestIOS { "transactionDate" to 1_700_000_000_000.0, "advancedCommerceInfoIOS" to mapOf( "items" to emptyList(), + "period" to mapOf( + "unit" to "month", + "value" to 1, + ), "requestReferenceId" to "request-reference", ), "billingPlanTypeIOS" to "monthly", @@ -183,6 +188,8 @@ class ProductPayloadNormalizerTestIOS { val purchase = assertNotNull(decodePurchasePayloadIOS(payload)) assertEquals("request-reference", purchase.advancedCommerceInfoIOS?.requestReferenceId) + assertEquals(SubscriptionPeriodIOS.Month, purchase.advancedCommerceInfoIOS?.period?.unit) + assertEquals(1, purchase.advancedCommerceInfoIOS?.period?.value) assertEquals(SubscriptionBillingPlanTypeIOS.Monthly, purchase.billingPlanTypeIOS) assertEquals(12, purchase.commitmentInfoIOS?.totalBillingPeriods) assertEquals("monthly-plan", purchase.currentPlanId) diff --git a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs index 197817de2..c7cefe933 100644 --- a/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs +++ b/libraries/maui-iap/example/OpenIap.Maui.Example/Pages/OfferCodePage.xaml.cs @@ -63,7 +63,7 @@ private async void OnPresentClicked(object sender, EventArgs e) ResultPanel.IsVisible = true; ResultLabel.Text = purchase is not null ? $"Verified redemption: {purchase.ProductId} ({purchase.TransactionId})" - : "The legacy redemption sheet was presented. Refresh available purchases after completing redemption."; + : "The system sheet did not return a transaction directly. Refresh available purchases after completing redemption."; } catch (Exception ex) { diff --git a/libraries/maui-iap/tests/OpenIap.Maui.Tests/RecordJsonTests.cs b/libraries/maui-iap/tests/OpenIap.Maui.Tests/RecordJsonTests.cs index d908aab8f..45e8da893 100644 --- a/libraries/maui-iap/tests/OpenIap.Maui.Tests/RecordJsonTests.cs +++ b/libraries/maui-iap/tests/OpenIap.Maui.Tests/RecordJsonTests.cs @@ -201,6 +201,10 @@ public void ProductAndroid_DefaultsPlatformAndTypeWhenOmitted() "revocationDate": 1720000000111 } ], + "period": { + "unit": "month", + "value": 1 + }, "requestReferenceId": "request-reference", "taxCode": "digital-goods", "taxExclusivePrice": "9.00", @@ -365,6 +369,8 @@ public void PurchaseIOS_FullCanonicalPayloadRoundTripsEveryGeneratedField() Assert.Equal("1999999999", ios.PreviousOriginalTransactionIdIOS); Assert.Equal(3, ios.CommitmentInfoIOS?.BillingPeriodNumber); Assert.Equal("request-reference", ios.AdvancedCommerceInfoIOS?.RequestReferenceId); + Assert.Equal(SubscriptionPeriodIOS.Month, ios.AdvancedCommerceInfoIOS?.Period?.Unit); + Assert.Equal(1, ios.AdvancedCommerceInfoIOS?.Period?.Value); Assert.Equal( """{"sku":"premium.bundle"}""", ios.AdvancedCommerceInfoIOS?.Items[0].Details?.JsonRepresentation); diff --git a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt index 9a97c20a3..8534703d2 100644 --- a/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +++ b/libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt @@ -197,7 +197,7 @@ class HybridRnIap : HybridRnIapSpec() { } } .onFailure { - RnIapLog.warn("Activity not available during initConnection - OpenIAP will use Context") + RnIapLog.warn("Activity not available during initConnection") } } } catch (err: CancellationException) { diff --git a/libraries/react-native-iap/example/screens/OfferCode.tsx b/libraries/react-native-iap/example/screens/OfferCode.tsx index 4b2818def..224c685b0 100644 --- a/libraries/react-native-iap/example/screens/OfferCode.tsx +++ b/libraries/react-native-iap/example/screens/OfferCode.tsx @@ -90,7 +90,7 @@ export default function OfferCodeScreen() { } else { Alert.alert( 'Redemption Sheet Presented', - 'This iOS version uses the legacy sheet. Refresh available purchases after completing redemption.', + 'The system sheet did not return a transaction directly. Refresh available purchases after completing redemption.', ); } } else { diff --git a/libraries/react-native-iap/ios/RnIapHelper.swift b/libraries/react-native-iap/ios/RnIapHelper.swift index 85662fbdc..9b95dcfe6 100644 --- a/libraries/react-native-iap/ios/RnIapHelper.swift +++ b/libraries/react-native-iap/ios/RnIapHelper.swift @@ -129,6 +129,13 @@ enum RnIapHelper { return .second(value) } + static func wrapSubscriptionPeriodValue( + _ value: SubscriptionPeriodValueIOS? + ) -> Variant_NullType_SubscriptionPeriodValueIOS? { + guard let value = value else { return nil } + return .second(value) + } + static func wrapTransactionCommitmentInfo( _ value: TransactionCommitmentInfoIOS? ) -> Variant_NullType_TransactionCommitmentInfoIOS? { @@ -185,6 +192,9 @@ enum RnIapHelper { displayName: wrapString(dictionary["displayName"] as? String), estimatedTax: wrapString(dictionary["estimatedTax"] as? String), items: items, + period: wrapSubscriptionPeriodValue( + convertSubscriptionPeriodValue(dictionary["period"]) + ), requestReferenceId: wrapString(dictionary["requestReferenceId"] as? String), taxCode: wrapString(dictionary["taxCode"] as? String), taxExclusivePrice: wrapString(dictionary["taxExclusivePrice"] as? String), @@ -192,6 +202,16 @@ enum RnIapHelper { ) } + static func convertSubscriptionPeriodValue(_ value: Any?) -> SubscriptionPeriodValueIOS? { + guard let dictionary = value as? [String: Any], + let unitValue = dictionary["unit"] as? String, + let unit = SubscriptionPeriodIOS(fromString: unitValue), + let periodValue = doubleValue(dictionary["value"]) else { + return nil + } + return SubscriptionPeriodValueIOS(unit: unit, value: periodValue) + } + static func convertAdvancedCommerceItem(_ dictionary: [String: Any]) -> AdvancedCommerceItemIOS { let details: Variant_NullType_AdvancedCommerceItemDetailsIOS? if let detailsDictionary = dictionary["details"] as? [String: Any] { diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index 9164f5e61..b8f91fea1 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -2258,8 +2258,10 @@ export const syncIOS: MutationField<'syncIOS'> = async () => { /** * Present the code redemption sheet for offer codes (iOS only) - * @returns The verified redeemed purchase on iOS 27+, or null after the - * legacy sheet is presented on earlier iOS versions. + * @returns The verified redeemed purchase when built with Xcode 27+ and + * running on Apple 27+, or null when the system sheet cannot return the + * transaction directly (StoreKit 2 on iOS/Catalyst 16–26 and visionOS 1–26; + * StoreKit 1 on iOS/Catalyst 15). * @platform iOS * * @see {@link https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios} diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts index 1fcde60db..e9966b440 100644 --- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts +++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts @@ -836,8 +836,10 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> { /** * Present the code redemption sheet for offer codes (iOS only) - * @returns The verified redeemed purchase on iOS 27+, or null after the - * legacy sheet is presented on earlier iOS versions. + * @returns The verified redeemed purchase when built with Xcode 27+ and + * running on Apple 27+, or null when the system sheet cannot return the + * transaction directly (StoreKit 2 on iOS/Catalyst 16–26 and visionOS 1–26; + * StoreKit 1 on iOS/Catalyst 15). * @platform iOS */ presentCodeRedemptionSheetIOS(): Promise; From 8e6ca8443165619a9a2eb47d463e65aaee7bf4b0 Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:21:09 +0900 Subject: [PATCH 5/8] docs: align current store api guidance Document StoreKit toolchain and platform fallbacks, Horizon activity requirements, current entitlement semantics, and nullable redemption results across searchable, compiled, and knowledge references. --- knowledge/_claude-context/context.md | 36 ++++++++++++------- knowledge/external/google-billing-api.md | 13 ++++--- knowledge/external/horizon-api.md | 4 +++ knowledge/external/storekit2-api.md | 17 +++++---- packages/docs/public/llms-full.txt | 36 ++++++++++++------- packages/docs/public/llms.txt | 4 +-- packages/docs/src/lib/searchData.ts | 6 ++-- packages/docs/src/pages/docs/apis/index.tsx | 5 +-- .../src/pages/docs/apis/init-connection.tsx | 10 ++++-- .../docs/apis/ios/current-entitlement-ios.tsx | 11 ++++-- .../docs/apis/ios/get-app-transaction-ios.tsx | 4 +-- .../apis/ios/get-promoted-product-ios.tsx | 14 +++++--- .../ios/present-code-redemption-sheet-ios.tsx | 24 +++++++------ .../ios/promoted-product-listener-ios.tsx | 11 ++++-- .../docs/features/offer-code-redemption.tsx | 24 +++++++------ .../src/pages/docs/foundation/one-pager.tsx | 2 +- packages/docs/src/pages/docs/setup/godot.tsx | 8 ++--- .../docs/types/ios/app-transaction-ios.tsx | 12 +++++-- .../docs/src/pages/docs/types/purchase.tsx | 6 ++++ .../src/pages/docs/updates/deprecations.tsx | 2 +- scripts/agent/compile-context.ts | 2 +- 21 files changed, 164 insertions(+), 87 deletions(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index 09f935b09..edf46489a 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-08-08T10:05:12.433Z +> Last updated: 2026-08-09T12:55:09.925Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -3420,11 +3420,16 @@ if (purchase.isSuspended) { ### Query Suspended Subscriptions (8.1+) ```kotlin -// Include suspended subscriptions in query results -val params = QueryPurchasesParams.newBuilder() +// Include suspended subscriptions when the connected Play Store supports it. +val paramsBuilder = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) - .setIncludeSuspended(true) // New in 8.1 - .build() +if (billingClient.isFeatureSupported( + BillingClient.FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS + ).responseCode == BillingClient.BillingResponseCode.OK +) { + paramsBuilder.includeSuspendedSubscriptions(true) // New in 8.1 +} +val params = paramsBuilder.build() billingClient.queryPurchasesAsync(params) { billingResult, purchases -> purchases.forEach { purchase -> @@ -3847,6 +3852,10 @@ import com.meta.horizon.billingclient.api.* ### Important Notes +- The Billing Compatibility SDK initializes Horizon platform state from an + Android `Activity`. OpenIAP therefore requires a current foreground Activity + for Horizon `initConnection`; it returns `MissingCurrentActivity` instead of + falling back to an application context. - Horizon Billing Compatibility 2.x reads the app id from Android manifest meta-data key `com.meta.horizon.platform.HORIZON_APP_ID`. The older `com.meta.horizon.platform.ovr.OCULUS_APP_ID` key is deprecated; OpenIAP also @@ -4111,16 +4120,19 @@ let result = try await AppStore.presentOfferCodeRedeemSheet( SwiftUI exposes the same result through `offerCodeRedemption(options:isPresented:onCompletion:)`. These APIs require the -Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose only the -legacy redemption sheet API. +Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose the StoreKit 2 +scene-based `AppStore.presentOfferCodeRedeemSheet(in:)` API, which presents the +sheet but does not return the redeemed transaction. OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`. Xcode 27 builds call the new API, require a verified result, and return the -mapped transaction. Xcode 26 builds retain the legacy sheet; iOS and Mac -Catalyst 14–26 therefore return `nil` after presentation and rely on the +mapped transaction. Xcode 26 builds use the StoreKit 2 scene API on iOS and Mac +Catalyst 16+ and visionOS 1+, and return `nil` after presentation; iOS and Mac +Catalyst 15 retain the StoreKit 1 fallback. Both nil-returning paths rely on the transaction listener or explicit purchase reconciliation. Xcode 27 beta 4 -declares `RedeemOption`, but its public symbol graph exposes no constructible -option values, so OpenIAP currently passes an empty set. +declares `RedeemOption`, +but its public symbol graph exposes no constructible option values, so OpenIAP +currently passes an empty set. ### Subscription Bundles and Suites (Xcode 27 beta) @@ -4403,7 +4415,7 @@ let result = try await product.purchase(confirmIn: window) > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. -## AppTransaction Updates (iOS 18.4+) +## AppTransaction Updates (Xcode 16.4+; back-deployed) ```swift let appTransaction = try await AppTransaction.shared diff --git a/knowledge/external/google-billing-api.md b/knowledge/external/google-billing-api.md index 9d5c51f08..38c44a04f 100644 --- a/knowledge/external/google-billing-api.md +++ b/knowledge/external/google-billing-api.md @@ -361,11 +361,16 @@ if (purchase.isSuspended) { ### Query Suspended Subscriptions (8.1+) ```kotlin -// Include suspended subscriptions in query results -val params = QueryPurchasesParams.newBuilder() +// Include suspended subscriptions when the connected Play Store supports it. +val paramsBuilder = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) - .setIncludeSuspended(true) // New in 8.1 - .build() +if (billingClient.isFeatureSupported( + BillingClient.FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS + ).responseCode == BillingClient.BillingResponseCode.OK +) { + paramsBuilder.includeSuspendedSubscriptions(true) // New in 8.1 +} +val params = paramsBuilder.build() billingClient.queryPurchasesAsync(params) { billingResult, purchases -> purchases.forEach { purchase -> diff --git a/knowledge/external/horizon-api.md b/knowledge/external/horizon-api.md index 07e902f6a..87625fd65 100644 --- a/knowledge/external/horizon-api.md +++ b/knowledge/external/horizon-api.md @@ -100,6 +100,10 @@ import com.meta.horizon.billingclient.api.* ### Important Notes +- The Billing Compatibility SDK initializes Horizon platform state from an + Android `Activity`. OpenIAP therefore requires a current foreground Activity + for Horizon `initConnection`; it returns `MissingCurrentActivity` instead of + falling back to an application context. - Horizon Billing Compatibility 2.x reads the app id from Android manifest meta-data key `com.meta.horizon.platform.HORIZON_APP_ID`. The older `com.meta.horizon.platform.ovr.OCULUS_APP_ID` key is deprecated; OpenIAP also diff --git a/knowledge/external/storekit2-api.md b/knowledge/external/storekit2-api.md index 84f419e96..0b4257eb0 100644 --- a/knowledge/external/storekit2-api.md +++ b/knowledge/external/storekit2-api.md @@ -77,16 +77,19 @@ let result = try await AppStore.presentOfferCodeRedeemSheet( SwiftUI exposes the same result through `offerCodeRedemption(options:isPresented:onCompletion:)`. These APIs require the -Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose only the -legacy redemption sheet API. +Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose the StoreKit 2 +scene-based `AppStore.presentOfferCodeRedeemSheet(in:)` API, which presents the +sheet but does not return the redeemed transaction. OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`. Xcode 27 builds call the new API, require a verified result, and return the -mapped transaction. Xcode 26 builds retain the legacy sheet; iOS and Mac -Catalyst 14–26 therefore return `nil` after presentation and rely on the +mapped transaction. Xcode 26 builds use the StoreKit 2 scene API on iOS and Mac +Catalyst 16+ and visionOS 1+, and return `nil` after presentation; iOS and Mac +Catalyst 15 retain the StoreKit 1 fallback. Both nil-returning paths rely on the transaction listener or explicit purchase reconciliation. Xcode 27 beta 4 -declares `RedeemOption`, but its public symbol graph exposes no constructible -option values, so OpenIAP currently passes an empty set. +declares `RedeemOption`, +but its public symbol graph exposes no constructible option values, so OpenIAP +currently passes an empty set. ### Subscription Bundles and Suites (Xcode 27 beta) @@ -369,7 +372,7 @@ let result = try await product.purchase(confirmIn: window) > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. -## AppTransaction Updates (iOS 18.4+) +## AppTransaction Updates (Xcode 16.4+; back-deployed) ```swift let appTransaction = try await AppTransaction.shared diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index 0bd22616d..e1a61f342 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-08-08T17:25:19.792Z +> Generated: 2026-08-09T12:55:09.936Z ## Table of Contents 1. Installation @@ -758,11 +758,16 @@ if (purchase.isSuspended) { ### Query Suspended Subscriptions (8.1+) ```kotlin -// Include suspended subscriptions in query results -val params = QueryPurchasesParams.newBuilder() +// Include suspended subscriptions when the connected Play Store supports it. +val paramsBuilder = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) - .setIncludeSuspended(true) // New in 8.1 - .build() +if (billingClient.isFeatureSupported( + BillingClient.FeatureType.INCLUDE_SUSPENDED_SUBSCRIPTIONS + ).responseCode == BillingClient.BillingResponseCode.OK +) { + paramsBuilder.includeSuspendedSubscriptions(true) // New in 8.1 +} +val params = paramsBuilder.build() billingClient.queryPurchasesAsync(params) { billingResult, purchases -> purchases.forEach { purchase -> @@ -1183,6 +1188,10 @@ import com.meta.horizon.billingclient.api.* ### Important Notes +- The Billing Compatibility SDK initializes Horizon platform state from an + Android `Activity`. OpenIAP therefore requires a current foreground Activity + for Horizon `initConnection`; it returns `MissingCurrentActivity` instead of + falling back to an application context. - Horizon Billing Compatibility 2.x reads the app id from Android manifest meta-data key `com.meta.horizon.platform.HORIZON_APP_ID`. The older `com.meta.horizon.platform.ovr.OCULUS_APP_ID` key is deprecated; OpenIAP also @@ -1445,16 +1454,19 @@ let result = try await AppStore.presentOfferCodeRedeemSheet( SwiftUI exposes the same result through `offerCodeRedemption(options:isPresented:onCompletion:)`. These APIs require the -Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose only the -legacy redemption sheet API. +Xcode 27 beta SDK and are currently beta. Xcode 26.x SDKs expose the StoreKit 2 +scene-based `AppStore.presentOfferCodeRedeemSheet(in:)` API, which presents the +sheet but does not return the redeemed transaction. OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`. Xcode 27 builds call the new API, require a verified result, and return the -mapped transaction. Xcode 26 builds retain the legacy sheet; iOS and Mac -Catalyst 14–26 therefore return `nil` after presentation and rely on the +mapped transaction. Xcode 26 builds use the StoreKit 2 scene API on iOS and Mac +Catalyst 16+ and visionOS 1+, and return `nil` after presentation; iOS and Mac +Catalyst 15 retain the StoreKit 1 fallback. Both nil-returning paths rely on the transaction listener or explicit purchase reconciliation. Xcode 27 beta 4 -declares `RedeemOption`, but its public symbol graph exposes no constructible -option values, so OpenIAP currently passes an empty set. +declares `RedeemOption`, +but its public symbol graph exposes no constructible option values, so OpenIAP +currently passes an empty set. ### Subscription Bundles and Suites (Xcode 27 beta) @@ -1737,7 +1749,7 @@ let result = try await product.purchase(confirmIn: window) > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. -## AppTransaction Updates (iOS 18.4+) +## AppTransaction Updates (Xcode 16.4+; back-deployed) ```swift let appTransaction = try await AppTransaction.shared diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 664eb6815..7abe561e6 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-08-08T17:25:19.792Z +> Generated: 2026-08-09T12:55:09.936Z ## Installation @@ -240,7 +240,7 @@ interface PurchaseError { ### iOS - syncIOS() - Sync with App Store -- presentCodeRedemptionSheetIOS() - Show offer code UI and return a verified PurchaseIOS on Apple 27+ (null after the legacy sheet) +- presentCodeRedemptionSheetIOS() - Show offer code UI; Xcode 27+ builds return a verified PurchaseIOS on Apple 27+, while other supported system-sheet paths return null - showManageSubscriptionsIOS() - Open subscription management - beginRefundRequestIOS() - Start refund flow diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts index 39ce3b674..8e3144d4d 100644 --- a/packages/docs/src/lib/searchData.ts +++ b/packages/docs/src/lib/searchData.ts @@ -161,7 +161,7 @@ export const apiData: ApiItem[] = [ id: 'get-promoted-product-ios', title: 'getPromotedProductIOS', category: 'iOS Specific', - description: 'Get the currently promoted product (iOS 11+)', + description: 'Get the currently promoted product (iOS 15+)', parameters: '', returns: 'ProductIOS', path: '/docs/apis/ios/get-promoted-product-ios', @@ -261,9 +261,9 @@ export const apiData: ApiItem[] = [ title: 'presentCodeRedemptionSheetIOS', category: 'iOS Specific', description: - 'Present the App Store code redemption sheet and return its verified transaction on Apple 27+', + 'Present the App Store code redemption sheet; Xcode 27+ builds return a verified transaction on Apple 27+, while other supported paths return null', parameters: '', - returns: 'PurchaseIOS', + returns: 'PurchaseIOS | null', path: '/docs/apis/ios/present-code-redemption-sheet-ios', }, { diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index c0603f9d7..f5b17073d 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -492,8 +492,9 @@ function APIsIndex() { - Show the App Store offer code redemption sheet and return its - verified transaction on Apple 27+. See{' '} + Show the App Store offer code redemption sheet. Xcode 27+ builds + return its verified transaction on Apple 27+; other supported + system-sheet paths return null. See{' '} Offer Code Redemption diff --git a/packages/docs/src/pages/docs/apis/init-connection.tsx b/packages/docs/src/pages/docs/apis/init-connection.tsx index ca1f9e242..c64c568ed 100644 --- a/packages/docs/src/pages/docs/apis/init-connection.tsx +++ b/packages/docs/src/pages/docs/apis/init-connection.tsx @@ -23,8 +23,9 @@ function InitConnection() {

iOS: Verifies AppStore.canMakePayments, - registers the SKPaymentQueue observer for promoted IAPs, - and starts a Transaction.updates listener that drives the + receives promoted IAPs through PurchaseIntent.intents on + iOS 16.4+ (and SKPaymentQueue only on iOS 15–16.3), and + starts a Transaction.updates listener that drives the purchase event stream. Safe to call repeatedly.{' '} . Android: Starts BillingClient and waits for onBillingSetupFinished. Required before any other Play - Billing call.{' '} + Billing call. Meta Horizon additionally requires a current foreground{' '} + Activity; initialization fails with{' '} + MissingCurrentActivity instead of falling back to an + application context.{' '}

- Wraps Transaction.currentEntitlement(for:) — single-product - convenience over currentEntitlements. See the{' '} + Uses Product.currentEntitlements where available and + otherwise filters Transaction.currentEntitlements by SKU. + This avoids StoreKit's deprecated singular{' '} + Product.currentEntitlement property. Generic Advanced + Commerce SKUs can yield multiple current transactions, so OpenIAP + returns the latest verified entitlement by purchase date and transaction + ID. See the{' '} diff --git a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx index 701a858e6..f7bfd59fc 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx @@ -117,8 +117,8 @@ var appTx = await ((QueryResolver)OpenIapClient.Instance).GetAppTransactionIOSAs originalPurchaseDate, environment,{' '} deviceVerification, deviceVerificationNonce,{' '} signedDate, appId, appVersionId,{' '} - preorderDate, plus iOS 18.4+ additions like{' '} - appTransactionId and originalPlatform). + preorderDate, the back-deployed{' '} + appTransactionId, and originalPlatform).

); diff --git a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx index f5ff3fbc0..d44160d88 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx @@ -12,7 +12,7 @@ function GetPromotedProductIOS() {
@@ -20,11 +20,15 @@ function GetPromotedProductIOS() { iOS{' '} getPromotedProductIOS -

Get the currently promoted product from App Store (iOS 11+).

+

Get the currently promoted product from App Store (iOS 15+).

- Reads the product surfaced via App Store promoted IAP campaigns ( - SKPaymentTransactionObserver.shouldAddStorePayment). iOS - 11+. See the{' '} + Reads the product surfaced via App Store promoted IAP campaigns. OpenIAP + listens to PurchaseIntent.intents on iOS 16.4+ and uses{' '} + SKPaymentTransactionObserver.shouldAddStorePayment only on + iOS 15–16.3. When a purchase intent contains an externally redeemed + win-back offer, OpenIAP preserves that exact offer for the next matching{' '} + requestPurchase call unless the caller supplies an explicit + win-back or promotional offer. See the{' '}

- On iOS 27, Mac Catalyst 27, and visionOS 27 or later, this calls{' '} + In Xcode 27+ builds on iOS 27, Mac Catalyst 27, and visionOS 27 or + later, this calls{' '} AppStore.presentOfferCodeRedeemSheet(from:options:) and - returns the verified transaction produced by redemption. On iOS 14–26 - and Mac Catalyst 14–26, it presents the legacy{' '} - SKPaymentQueue sheet and returns null; use the - purchase listener or refresh available purchases after the sheet closes. - See the{' '} + returns the verified transaction produced by redemption. On iOS and Mac + Catalyst 16–26 and visionOS 1–26, it uses the StoreKit 2 scene-based + sheet and returns null. Builds made with an older SDK use + that same path on Apple 27 runtimes. iOS and Mac Catalyst 15 use the + StoreKit 1 fallback and also return null. For any + nil-returning path, use the purchase listener or refresh available + purchases after the sheet closes. See the{' '}

PurchaseIOS | null — the verified redeemed transaction on - Apple 27+ runtimes. A null result means the legacy sheet - was presented successfully but cannot return its transaction directly; - it does not mean the feature is unsupported or that redemption failed. + Apple 27+ runtimes from Xcode 27+ builds. A null result + means the system sheet was presented successfully but that API path + cannot return its transaction directly; it does not mean the feature is + unsupported or that redemption failed.

Example

@@ -87,7 +91,7 @@ function PresentCodeRedemptionSheetIOS() { if let purchase { print("Verified redemption:", purchase.productId) } else { - // iOS 14–26: reconcile through the listener or refresh purchases. + // Nil result: reconcile through the listener or refresh purchases. }`} ), kotlin: ( diff --git a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx index e04645c4e..6026183d2 100644 --- a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -57,7 +57,11 @@ IObservable promotedProducts = OpenIapClient.Instance.PromotedProductIOS ), }} -

Registers a listener for App Store promoted product events.

+

+ Registers a listener for App Store promoted product events. OpenIAP uses{' '} + PurchaseIntent.intents on iOS 16.4+ and the StoreKit 1 + observer only on iOS 15–16.3, so both mechanisms never run together. +

{{ @@ -243,7 +247,10 @@ using var subscription = iap.PromotedProductIOS.Subscribe(async productId => requestPurchase() {' '} - flow after the app receives or restores the promoted product. + flow after the app receives or restores the promoted product. If the + purchase intent contains an externally redeemed win-back offer, OpenIAP + automatically carries it into the next matching purchase unless that + request supplies an explicit win-back or promotional offer.
); diff --git a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx index 0a15ff7b2..68d7ce147 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -26,9 +26,9 @@ function OfferCodeRedemption() { Current API boundary:{' '} presentCodeRedemptionSheetIOS returns the verified{' '} PurchaseIOS produced by Apple's new StoreKit API on - iOS 27, Mac Catalyst 27, and visionOS 27 or later. On iOS and Mac - Catalyst 14–26 it presents the legacy system sheet and returns{' '} - null, so observe the redeemed purchase through{' '} + iOS 27, Mac Catalyst 27, and visionOS 27 or later. Earlier supported + runtimes present Apple's system sheet and return null, + so observe the redeemed purchase through{' '} purchaseUpdatedListener {' '} @@ -91,10 +91,13 @@ function OfferCodeRedemption() {

Initialize the store connection before presenting the sheet, and register a purchase listener before the user redeems a - code. Apple 27+ returns the verified redeemed purchase - directly. Earlier iOS versions return null after - presenting the legacy sheet, and the redeemed transaction - arrives through the listener or a subsequent refresh. + code. Builds made with Xcode 27+ return the verified redeemed + purchase directly on Apple 27+ runtimes. Other supported paths + return null after presenting the system sheet, + and the redeemed transaction arrives through the listener or a + subsequent refresh. The implementation uses StoreKit 2 on iOS + and Mac Catalyst 16+ and visionOS 1+, and keeps the StoreKit 1 + fallback only for iOS and Mac Catalyst 15.

{{ @@ -266,9 +269,10 @@ func _exit_tree() -> void:
  • The pre-built Godot GDExtension must also have been compiled - with Xcode 27; an Xcode 26-built framework uses the legacy{' '} - null result even on Apple 27. Build from source - or confirm the release artifact toolchain. + with Xcode 27; an Xcode 26-built framework uses the + scene-based StoreKit 2 API and returns null{' '} + even on Apple 27. Build from source or confirm the release + artifact toolchain.
  • Use a physical device for an actual App Store sandbox or diff --git a/packages/docs/src/pages/docs/foundation/one-pager.tsx b/packages/docs/src/pages/docs/foundation/one-pager.tsx index 634541ffb..d80191cd5 100644 --- a/packages/docs/src/pages/docs/foundation/one-pager.tsx +++ b/packages/docs/src/pages/docs/foundation/one-pager.tsx @@ -370,7 +370,7 @@ function OnePager() {
  • Store APIs Supported: Apple StoreKit 2, Google Play - Billing 9.1.0, Meta Horizon 1.1 + Billing 9.1.0, Meta Horizon Billing Compatibility 2.0.0
  • Sponsor: Meta (founding sponsor) diff --git a/packages/docs/src/pages/docs/setup/godot.tsx b/packages/docs/src/pages/docs/setup/godot.tsx index 5e4e6cf32..04b3e0bf2 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -123,10 +123,10 @@ make android offer code redemption {' '} only returns a verified result when the framework was built with Xcode - 27 or later — a framework built with Xcode 26 falls back to the legacy - redemption sheet and returns null, even on devices - running the latest OS. The published godot-iap 3.0.0 framework is - built with Xcode 27. If you build from source and use offer codes, + 27 or later — a framework built with Xcode 26 uses the scene-based + StoreKit 2 redemption sheet and returns null, even on + devices running the latest OS. The published godot-iap 3.0.0 framework + is built with Xcode 27. If you build from source and use offer codes, build with Xcode 27 or later.

    diff --git a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx index 7d059f989..810f3e6a1 100644 --- a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -136,15 +136,21 @@ function AppTransactionIos() { appTransactionId - App transaction ID (iOS 18.4+) + + Stable app transaction ID. StoreKit back-deploys this property + across the supported AppTransaction runtime range when built + with Xcode 16.4+. + originalPlatform - Original platform (iOS 18.4+), including the Xcode 27 SDK's - back-deployed managed value + Original platform. OpenIAP uses StoreKit's compatibility + string on the AppTransaction baseline and the typed value on iOS + 18.4+, including the Xcode 27 SDK's back-deployed{' '} + managed value. diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index 67996367e..a1ce57665 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -537,6 +537,12 @@ function Purchase() { Items purchased in this transaction + + + period + + Subscription period unit and value (optional) + requestReferenceId diff --git a/packages/docs/src/pages/docs/updates/deprecations.tsx b/packages/docs/src/pages/docs/updates/deprecations.tsx index facb13610..27f153630 100644 --- a/packages/docs/src/pages/docs/updates/deprecations.tsx +++ b/packages/docs/src/pages/docs/updates/deprecations.tsx @@ -79,7 +79,7 @@ const migrationGroups = [ ['willExpireSoon', 'daysUntilExpirationIOS'], [ 'presentCodeRedemptionSheetIOS Boolean result', - 'nullable PurchaseIOS result: verified on Apple 27+; null after the legacy sheet on iOS or Mac Catalyst 14–26', + 'nullable PurchaseIOS result: verified on Apple 27+ with Xcode 27+; null after the system sheet on iOS/Catalyst 15–26, visionOS 1–26, or Apple 27 from an older build', ], ['receipt-failed', 'purchase-verification-failed'], ['receipt-finished', 'purchase-verification-finished'], diff --git a/scripts/agent/compile-context.ts b/scripts/agent/compile-context.ts index b3cf790ec..313c89926 100644 --- a/scripts/agent/compile-context.ts +++ b/scripts/agent/compile-context.ts @@ -813,7 +813,7 @@ interface PurchaseError { ### iOS - syncIOS() - Sync with App Store -- presentCodeRedemptionSheetIOS() - Show offer code UI and return a verified PurchaseIOS on Apple 27+ (null after the legacy sheet) +- presentCodeRedemptionSheetIOS() - Show offer code UI; Xcode 27+ builds return a verified PurchaseIOS on Apple 27+, while other supported system-sheet paths return null - showManageSubscriptionsIOS() - Open subscription management - beginRefundRequestIOS() - Start refund flow From 5a96596d9b8e00a5e0e161eda3216ea175b927cc Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:21:37 +0900 Subject: [PATCH 6/8] ci: harden cross-platform verification Build the Expo config plugin before prebuild so generated native projects exercise local Google build-directory isolation during verification. --- .claude/commands/verify-all.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.claude/commands/verify-all.md b/.claude/commands/verify-all.md index 084d75d78..2b7178b94 100644 --- a/.claude/commands/verify-all.md +++ b/.claude/commands/verify-all.md @@ -65,7 +65,7 @@ set -euo pipefail COMPILER_INDEX_STORE_ENABLE=NO) # Expo bridge tests and clean native prebuilds/consumer builds -(cd libraries/expo-iap && bun run lint:tsc && bun run test) +(cd libraries/expo-iap && bun run build:plugin && bun run lint:tsc && bun run test) (cd libraries/expo-iap/example && bun run test -- --runInBand) (cd libraries/expo-iap/example && \ npx expo prebuild --platform android --clean) @@ -92,7 +92,13 @@ set -euo pipefail ([ -f env ] || cp env.example env) && \ flutter build apk --debug) (cd libraries/flutter_inapp_purchase/example/ios && \ - pod install --repo-update && \ + pod install --repo-update) +# CocoaPods regenerates FlutterGeneratedPluginSwiftPackage at Flutter's iOS 13 +# default. Run Flutter's configuration step afterward so it raises that +# aggregate package to the app/plugin iOS 15 deployment target. +(cd libraries/flutter_inapp_purchase/example && \ + flutter build ios --config-only --simulator) +(cd libraries/flutter_inapp_purchase/example/ios && \ xcodebuild build \ -workspace Runner.xcworkspace \ -scheme Runner \ From f4744a184625f9293a61218b666e4c89be0234fb Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 22:33:55 +0900 Subject: [PATCH 7/8] fix(apple): preserve Xcode 16 compatibility Gate Product.currentEntitlements on the Xcode 26 Swift compiler so Xcode 16.4 builds retain the Transaction.currentEntitlements fallback. --- packages/apple/Sources/OpenIapModule.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index e6c915ea8..9ca4945cd 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -1179,7 +1179,10 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { let product = try await storeProduct(for: sku) let entitlements: Transaction.Transactions - #if compiler(>=6.1) + // Product.currentEntitlements ships in the Xcode 26 SDK and is + // back-deployed by StoreKit. Xcode 16.4 also uses Swift 6.1, so the + // compiler guard must stay at Swift 6.2+ to keep that SDK buildable. + #if compiler(>=6.2) if #available(iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, visionOS 2.4, *) { entitlements = product.currentEntitlements } else { From ffe57d9feecf8ee30c94a6faa744d3734d14d17d Mon Sep 17 00:00:00 2001 From: Hyo Date: Sun, 9 Aug 2026 23:08:59 +0900 Subject: [PATCH 8/8] fix: address store API review findings Validate native bridge inputs, apply platform naming conventions, document Catalyst and toolchain behavior accurately, and regenerate synchronized contracts and context. --- knowledge/_claude-context/context.md | 87 +++++++++++-------- knowledge/external/storekit2-api.md | 85 ++++++++++-------- .../src/__tests__/withLocalOpenIAP.test.ts | 17 ++-- libraries/expo-iap/src/modules/ios.ts | 16 ++-- libraries/expo-iap/src/types.ts | 11 +-- .../flutter_inapp_purchase/lib/types.dart | 11 +-- .../godot-iap/addons/godot-iap/godot_iap.gd | 7 +- libraries/godot-iap/addons/godot-iap/types.gd | 4 +- .../hyochan/kmpiap/InAppPurchaseAndroid.kt | 4 +- .../kmpiap/BillingQueryLifecycleTest.kt | 6 +- .../io/github/hyochan/kmpiap/openiap/Types.kt | 22 ++--- libraries/maui-iap/src/OpenIap.Maui/Types.cs | 11 +-- .../react-native-iap/ios/RnIapHelper.swift | 12 +-- libraries/react-native-iap/src/index.ts | 6 +- .../react-native-iap/src/specs/RnIap.nitro.ts | 24 ++--- libraries/react-native-iap/src/types.ts | 11 +-- .../Sources/Helpers/StoreKitTypesBridge.swift | 10 +-- packages/apple/Sources/Models/Types.swift | 11 +-- packages/apple/Sources/OpenIapModule.swift | 13 +-- packages/apple/Tests/OpenIapTests.swift | 12 +-- .../OpenIapTests/AppAccountTokenTests.swift | 26 +++--- packages/docs/public/llms-full.txt | 87 +++++++++++-------- packages/docs/public/llms.txt | 2 +- .../docs/apis/ios/get-app-transaction-ios.tsx | 7 +- .../ios/present-code-redemption-sheet-ios.tsx | 38 +++++--- .../docs/features/offer-code-redemption.tsx | 33 ++++--- packages/docs/src/pages/docs/setup/godot.tsx | 12 +-- .../docs/types/ios/app-transaction-ios.tsx | 10 ++- .../src/pages/docs/updates/deprecations.tsx | 2 +- .../src/main/java/dev/hyo/openiap/Types.kt | 22 ++--- packages/gql/src/api-ios.graphql | 11 +-- packages/gql/src/generated/Types.cs | 11 +-- packages/gql/src/generated/Types.kt | 22 ++--- packages/gql/src/generated/Types.swift | 11 +-- packages/gql/src/generated/types.dart | 11 +-- packages/gql/src/generated/types.gd | 4 +- packages/gql/src/generated/types.ts | 11 +-- 37 files changed, 393 insertions(+), 307 deletions(-) diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md index edf46489a..4e31db829 100644 --- a/knowledge/_claude-context/context.md +++ b/knowledge/_claude-context/context.md @@ -1,7 +1,7 @@ # OpenIAP Project Context > **Auto-generated for Claude Code** -> Last updated: 2026-08-09T12:55:09.925Z +> Last updated: 2026-08-09T13:54:08.141Z > > Usage: `claude --context knowledge/_claude-context/context.md` @@ -4047,34 +4047,34 @@ This document provides external API reference for Apple's StoreKit 2 framework. ## Recent StoreKit Features -| Feature | iOS Version | Description | -| -------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- | -| Win-back offers | iOS 18.0 | Re-engage churned subscribers | -| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | -| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key | -| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) | -| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | -| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | -| `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) | -| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | -| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | -| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | -| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | -| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | -| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | -| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | -| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions | -| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata | -| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data | -| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp | -| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation | -| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform | -| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload | -| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API | -| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning | -| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers | -| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 | +| Feature | iOS Version | Description | +| -------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Win-back offers | iOS 18.0 | Re-engage churned subscribers | +| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | +| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key | +| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) | +| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | +| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | +| `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) | +| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | +| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | +| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | +| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | +| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | +| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | +| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | +| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions | +| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata | +| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data | +| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp | +| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation | +| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform | +| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload | +| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API | +| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning | +| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers | +| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 | ### StoreKit Message presentation @@ -4126,9 +4126,11 @@ sheet but does not return the redeemed transaction. OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`. Xcode 27 builds call the new API, require a verified result, and return the -mapped transaction. Xcode 26 builds use the StoreKit 2 scene API on iOS and Mac -Catalyst 16+ and visionOS 1+, and return `nil` after presentation; iOS and Mac -Catalyst 15 retain the StoreKit 1 fallback. Both nil-returning paths rely on the +mapped transaction on Apple 27+ runtimes. Older result paths use the StoreKit 2 +scene API on iOS 16+ and visionOS 1+ and return `nil` after presentation; iOS 15 +retains the StoreKit 1 fallback. In Mac Catalyst apps, the scene API throws +`StoreKitError.unknown`, while the Catalyst 15 StoreKit 1 call has no effect and +returns `nil`. Nil results from an actually presented sheet rely on the transaction listener or explicit purchase reconciliation. Xcode 27 beta 4 declares `RedeemOption`, but its public symbol graph exposes no constructible option values, so OpenIAP @@ -4415,17 +4417,28 @@ let result = try await product.purchase(confirmIn: window) > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. -## AppTransaction Updates (Xcode 16.4+; back-deployed) +## AppTransaction Identity Updates (Xcode 16.4+; back-deployed) ```swift let appTransaction = try await AppTransaction.shared -// New in iOS 18.4 (back-deployed to iOS 15) +// Introduced in iOS 18.4 (back-deployed to the AppTransaction baseline) let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account -let originalPlatform = appTransaction.originalPlatform // Original purchase platform +let originalPlatform = appTransaction.originalPlatform // Typed value on iOS 18.4+ +``` + +OpenIAP uses `originalPlatformStringRepresentation` on older runtimes. The typed +`originalPlatform` property starts at iOS 18.4, macOS 15.4, tvOS 18.4, watchOS +11.4, and visionOS 2.4. + +## AppTransaction Acquisition Updates (Xcode 27 SDK) + +```swift +let appTransaction = try await AppTransaction.shared // Public in the Xcode 27 SDK and back-deployed to these existing runtimes let revocationDate = appTransaction.revocationDate // App-acquisition revocation +// Runtime-gated to Apple 27+ let storeType = appTransaction.storeType // Acquisition store channel ``` @@ -4471,6 +4484,10 @@ if let advancedInfo = product.advancedCommerceInfo { } ``` +For Advanced Commerce transactions, OpenIAP maps +`AdvancedCommerceInfoIOS.period` as an optional `SubscriptionPeriodValueIOS` +containing the subscription period `unit` and integer `value`. + ## Monthly Subscriptions With 12-Month Commitment (iOS 26.4+) This billing plan lets customers pay monthly while committing to an annual diff --git a/knowledge/external/storekit2-api.md b/knowledge/external/storekit2-api.md index 0b4257eb0..37356a370 100644 --- a/knowledge/external/storekit2-api.md +++ b/knowledge/external/storekit2-api.md @@ -4,34 +4,34 @@ This document provides external API reference for Apple's StoreKit 2 framework. ## Recent StoreKit Features -| Feature | iOS Version | Description | -| -------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- | -| Win-back offers | iOS 18.0 | Re-engage churned subscribers | -| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | -| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key | -| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) | -| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | -| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | -| `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) | -| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | -| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | -| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | -| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | -| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | -| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | -| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | -| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions | -| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata | -| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data | -| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp | -| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation | -| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform | -| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload | -| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API | -| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning | -| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers | -| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 | +| Feature | iOS Version | Description | +| -------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Win-back offers | iOS 18.0 | Re-engage churned subscribers | +| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | +| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key | +| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) | +| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | +| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | +| `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) | +| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | +| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | +| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | +| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | +| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | +| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | +| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | +| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions | +| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata | +| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data | +| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp | +| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation | +| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform | +| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload | +| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API | +| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning | +| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers | +| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 | ### StoreKit Message presentation @@ -83,9 +83,11 @@ sheet but does not return the redeemed transaction. OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`. Xcode 27 builds call the new API, require a verified result, and return the -mapped transaction. Xcode 26 builds use the StoreKit 2 scene API on iOS and Mac -Catalyst 16+ and visionOS 1+, and return `nil` after presentation; iOS and Mac -Catalyst 15 retain the StoreKit 1 fallback. Both nil-returning paths rely on the +mapped transaction on Apple 27+ runtimes. Older result paths use the StoreKit 2 +scene API on iOS 16+ and visionOS 1+ and return `nil` after presentation; iOS 15 +retains the StoreKit 1 fallback. In Mac Catalyst apps, the scene API throws +`StoreKitError.unknown`, while the Catalyst 15 StoreKit 1 call has no effect and +returns `nil`. Nil results from an actually presented sheet rely on the transaction listener or explicit purchase reconciliation. Xcode 27 beta 4 declares `RedeemOption`, but its public symbol graph exposes no constructible option values, so OpenIAP @@ -372,17 +374,28 @@ let result = try await product.purchase(confirmIn: window) > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. -## AppTransaction Updates (Xcode 16.4+; back-deployed) +## AppTransaction Identity Updates (Xcode 16.4+; back-deployed) ```swift let appTransaction = try await AppTransaction.shared -// New in iOS 18.4 (back-deployed to iOS 15) +// Introduced in iOS 18.4 (back-deployed to the AppTransaction baseline) let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account -let originalPlatform = appTransaction.originalPlatform // Original purchase platform +let originalPlatform = appTransaction.originalPlatform // Typed value on iOS 18.4+ +``` + +OpenIAP uses `originalPlatformStringRepresentation` on older runtimes. The typed +`originalPlatform` property starts at iOS 18.4, macOS 15.4, tvOS 18.4, watchOS +11.4, and visionOS 2.4. + +## AppTransaction Acquisition Updates (Xcode 27 SDK) + +```swift +let appTransaction = try await AppTransaction.shared // Public in the Xcode 27 SDK and back-deployed to these existing runtimes let revocationDate = appTransaction.revocationDate // App-acquisition revocation +// Runtime-gated to Apple 27+ let storeType = appTransaction.storeType // Acquisition store channel ``` @@ -428,6 +441,10 @@ if let advancedInfo = product.advancedCommerceInfo { } ``` +For Advanced Commerce transactions, OpenIAP maps +`AdvancedCommerceInfoIOS.period` as an optional `SubscriptionPeriodValueIOS` +containing the subscription period `unit` and integer `value`. + ## Monthly Subscriptions With 12-Month Commitment (iOS 26.4+) This billing plan lets customers pay monthly while committing to an annual diff --git a/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts b/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts index 04a3d61fc..398e3684e 100644 --- a/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts +++ b/libraries/expo-iap/plugin/src/__tests__/withLocalOpenIAP.test.ts @@ -20,10 +20,12 @@ describe('ensureLocalOpenIapFlavorStrategy', () => { ); expect(result).toContain('subprojects { subproject ->'); - expect(result).toContain('subproject.plugins.withId("com.android.library")'); - expect(result).toContain('missingDimensionStrategy "platform", "play"'); expect(result).toContain( - 'layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google"))', + 'subproject.plugins.withId("com.android.library")', + ); + expect(result).toContain('missingDimensionStrategy "platform", "play"'); + expect(result).toMatch( + /project\(":openiap-google"\)\s*\{\s*layout\.buildDirectory\.set\(rootProject\.layout\.buildDirectory\.dir\("openiap-google"\)\)\s*\}/, ); }); @@ -38,12 +40,9 @@ describe('ensureLocalOpenIapFlavorStrategy', () => { expect(result).toContain( 'extensions.configure("android")', ); - expect(result).toContain( - 'missingDimensionStrategy("platform", "horizon")', - ); - expect(result).toContain('project(":openiap-google")'); - expect(result).toContain( - 'layout.buildDirectory.set(rootProject.layout.buildDirectory.dir("openiap-google"))', + expect(result).toContain('missingDimensionStrategy("platform", "horizon")'); + expect(result).toMatch( + /project\(":openiap-google"\)\s*\{\s*layout\.buildDirectory\.set\(rootProject\.layout\.buildDirectory\.dir\("openiap-google"\)\)\s*\}/, ); expect(result).not.toContain( 'missingDimensionStrategy "platform", "horizon"', diff --git a/libraries/expo-iap/src/modules/ios.ts b/libraries/expo-iap/src/modules/ios.ts index e3e3c7823..fa5147485 100644 --- a/libraries/expo-iap/src/modules/ios.ts +++ b/libraries/expo-iap/src/modules/ios.ts @@ -283,9 +283,9 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async ( * Note: This only works on real devices, not simulators. * * @returns The verified redeemed purchase when built with Xcode 27+ and - * running on Apple 27+, or null when the system sheet cannot return the - * transaction directly (StoreKit 2 on iOS/Catalyst 16–26 and visionOS 1–26; - * StoreKit 1 on iOS/Catalyst 15). + * running on Apple 27+. Earlier iOS/visionOS system sheets return null; + * Catalyst 16–26 surfaces StoreKitError.unknown, and Catalyst 15 is a no-op + * that returns null. * @throws Error if called on non-iOS platform or tvOS * * @platform iOS @@ -486,9 +486,8 @@ export const getExternalPurchaseCustomLinkTokenIOS: QueryField< "getExternalPurchaseCustomLinkTokenIOS requires a tokenType ('acquisition' or 'services')", ); } - const result = await ExpoIapModule.getExternalPurchaseCustomLinkTokenIOS( - tokenType, - ); + const result = + await ExpoIapModule.getExternalPurchaseCustomLinkTokenIOS(tokenType); return result as ExternalPurchaseCustomLinkTokenResultIOS; }; @@ -513,9 +512,8 @@ export const showExternalPurchaseCustomLinkNoticeIOS: MutationField< "showExternalPurchaseCustomLinkNoticeIOS requires a noticeType ('browser')", ); } - const result = await ExpoIapModule.showExternalPurchaseCustomLinkNoticeIOS( - noticeType, - ); + const result = + await ExpoIapModule.showExternalPurchaseCustomLinkNoticeIOS(noticeType); return result as ExternalPurchaseCustomLinkNoticeResultIOS; }; diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts index c8de204f6..056e0c007 100644 --- a/libraries/expo-iap/src/types.ts +++ b/libraries/expo-iap/src/types.ts @@ -813,11 +813,12 @@ export interface Mutation { * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart index 6db39b914..50f99a113 100644 --- a/libraries/flutter_inapp_purchase/lib/types.dart +++ b/libraries/flutter_inapp_purchase/lib/types.dart @@ -5392,11 +5392,12 @@ abstract class MutationResolver { /// Show the App Store offer code redemption sheet. /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or /// visionOS 27+, returns the verified transaction produced by the redemption. - /// Other supported paths present the system sheet and return null: StoreKit 2's - /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - /// Catalyst 15. - /// Reconcile null results through the normal transaction listener or an explicit + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Future presentCodeRedemptionSheetIOS(); diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index eb10ea1d4..73f25afd6 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -1341,8 +1341,11 @@ func get_all_transactions_ios() -> Array: return purchases ## Present the code redemption sheet (iOS only). -## @return Types.PurchaseIOS when built with Xcode 27+ and running on Apple 27+, -## or null when the system sheet cannot return the transaction directly. +## @return Types.PurchaseIOS for a verified Apple 27+ redemption from an Xcode +## 27+ build. Returns null on unsupported platforms, native request failures, +## missing or invalid purchaseJson, and system-sheet paths that cannot return +## the transaction directly. Mac Catalyst 15 also returns null without showing +## a sheet because StoreKit 1 has no effect there. ## ## See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func present_code_redemption_sheet_ios() -> Variant: diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd index 163cad13a..f59061cfd 100644 --- a/libraries/godot-iap/addons/godot-iap/types.gd +++ b/libraries/godot-iap/addons/godot-iap/types.gd @@ -5796,7 +5796,7 @@ class Mutation: const return_type = "Boolean" const is_array = false - ## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + ## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 call has no effect and returns null. Reconcile null results from a presented sheet through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios class presentCodeRedemptionSheetIOSField: const name = "presentCodeRedemptionSheetIOS" const snake_name = "present_code_redemption_sheet_ios" @@ -6257,7 +6257,7 @@ static func begin_refund_request_ios_args(sku: String) -> Dictionary: static func sync_ios_args() -> Dictionary: return {} -## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios +## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 call has no effect and returns null. Reconcile null results from a presented sheet through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios static func present_code_redemption_sheet_ios_args() -> Dictionary: return {} diff --git a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt index 23ef18e8f..eab62c702 100644 --- a/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt +++ b/libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt @@ -226,7 +226,7 @@ internal fun subscriptionUpdateSourceCount( ): Int = listOf(purchaseToken, originalExternalTransactionId) .count { !it.isNullOrBlank() } -internal fun availablePurchasesQueryParams( +internal fun availablePurchasesQueryParamsAndroid( client: BillingClient, productType: String, includeSuspendedSubscriptions: Boolean, @@ -1790,7 +1790,7 @@ internal class InAppPurchaseAndroid( // 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. - val params = availablePurchasesQueryParams( + val params = availablePurchasesQueryParamsAndroid( client, type, includeSuspendedSubs, diff --git a/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt b/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt index 61d38cb24..f8181db5c 100644 --- a/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt +++ b/libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/BillingQueryLifecycleTest.kt @@ -67,17 +67,17 @@ class BillingQueryLifecycleTest { val supportedClient = LifecycleBillingClient( suspendedSubscriptionsSupported = true, ) - val subscriptionParams = availablePurchasesQueryParams( + val subscriptionParams = availablePurchasesQueryParamsAndroid( supportedClient, BillingClient.ProductType.SUBS, includeSuspendedSubscriptions = true, ) - val inAppParams = availablePurchasesQueryParams( + val inAppParams = availablePurchasesQueryParamsAndroid( supportedClient, BillingClient.ProductType.INAPP, includeSuspendedSubscriptions = true, ) - val unsupportedParams = availablePurchasesQueryParams( + val unsupportedParams = availablePurchasesQueryParamsAndroid( LifecycleBillingClient(suspendedSubscriptionsSupported = false), BillingClient.ProductType.SUBS, includeSuspendedSubscriptions = true, diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt index 44de509b4..43f3c9131 100644 --- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt +++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt @@ -5530,11 +5530,12 @@ public interface MutationResolver { * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ @@ -5918,11 +5919,12 @@ public data class MutationHandlers( * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs index 6338ecb7e..258d3b23c 100644 --- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs +++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs @@ -4180,11 +4180,12 @@ public interface MutationResolver /// Show the App Store offer code redemption sheet. /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or /// visionOS 27+, returns the verified transaction produced by the redemption. - /// Other supported paths present the system sheet and return null: StoreKit 2's - /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - /// Catalyst 15. - /// Reconcile null results through the normal transaction listener or an explicit + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Task PresentCodeRedemptionSheetIOSAsync(); diff --git a/libraries/react-native-iap/ios/RnIapHelper.swift b/libraries/react-native-iap/ios/RnIapHelper.swift index 9b95dcfe6..f44cc7283 100644 --- a/libraries/react-native-iap/ios/RnIapHelper.swift +++ b/libraries/react-native-iap/ios/RnIapHelper.swift @@ -129,7 +129,7 @@ enum RnIapHelper { return .second(value) } - static func wrapSubscriptionPeriodValue( + static func wrapSubscriptionPeriodValueIOS( _ value: SubscriptionPeriodValueIOS? ) -> Variant_NullType_SubscriptionPeriodValueIOS? { guard let value = value else { return nil } @@ -192,8 +192,8 @@ enum RnIapHelper { displayName: wrapString(dictionary["displayName"] as? String), estimatedTax: wrapString(dictionary["estimatedTax"] as? String), items: items, - period: wrapSubscriptionPeriodValue( - convertSubscriptionPeriodValue(dictionary["period"]) + period: wrapSubscriptionPeriodValueIOS( + convertSubscriptionPeriodValueIOS(dictionary["period"]) ), requestReferenceId: wrapString(dictionary["requestReferenceId"] as? String), taxCode: wrapString(dictionary["taxCode"] as? String), @@ -202,11 +202,13 @@ enum RnIapHelper { ) } - static func convertSubscriptionPeriodValue(_ value: Any?) -> SubscriptionPeriodValueIOS? { + static func convertSubscriptionPeriodValueIOS(_ value: Any?) -> SubscriptionPeriodValueIOS? { guard let dictionary = value as? [String: Any], let unitValue = dictionary["unit"] as? String, let unit = SubscriptionPeriodIOS(fromString: unitValue), - let periodValue = doubleValue(dictionary["value"]) else { + let periodValue = doubleValue(dictionary["value"]), + periodValue.isFinite, + Int32(exactly: periodValue) != nil else { return nil } return SubscriptionPeriodValueIOS(unit: unit, value: periodValue) diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts index b8f91fea1..8d992c99e 100644 --- a/libraries/react-native-iap/src/index.ts +++ b/libraries/react-native-iap/src/index.ts @@ -2259,9 +2259,9 @@ export const syncIOS: MutationField<'syncIOS'> = async () => { /** * Present the code redemption sheet for offer codes (iOS only) * @returns The verified redeemed purchase when built with Xcode 27+ and - * running on Apple 27+, or null when the system sheet cannot return the - * transaction directly (StoreKit 2 on iOS/Catalyst 16–26 and visionOS 1–26; - * StoreKit 1 on iOS/Catalyst 15). + * running on Apple 27+. Earlier iOS/visionOS system sheets return null; + * Catalyst 16–26 surfaces StoreKitError.unknown, and Catalyst 15 is a no-op + * that returns null. * @platform iOS * * @see {@link https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios} diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts index e9966b440..f73a85fea 100644 --- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts +++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts @@ -128,22 +128,16 @@ export type BillingChoiceImageLayoutAndroid = | 'rectangular-two-by-two'; export type BillingChoiceScreenTypeAndroid = - | 'unspecified' - | 'developer-rendered' - | 'google-rendered'; + 'unspecified' | 'developer-rendered' | 'google-rendered'; export type DeveloperBillingTypeAndroid = - | 'developer-billing-type-unspecified' - | 'in-app' - | 'external-link'; + 'developer-billing-type-unspecified' | 'in-app' | 'external-link'; export type InAppMessageCategoryAndroid = - | 'unknown-in-app-message-category-id' - | 'transactional'; + 'unknown-in-app-message-category-id' | 'transactional'; export type InAppMessageResponseCodeAndroid = - | 'no-action-needed' - | 'subscription-status-updated'; + 'no-action-needed' | 'subscription-status-updated'; // Developer Billing Launch Mode (Android 8.3.0+) // Defined locally for Nitro codegen @@ -162,9 +156,7 @@ export type ExternalLinkLaunchModeAndroid = // External Link Type (Android 8.2.0+) // Defined locally for Nitro codegen export type ExternalLinkTypeAndroid = - | 'unspecified' - | 'link-to-digital-content-offer' - | 'link-to-app-download'; + 'unspecified' | 'link-to-digital-content-offer' | 'link-to-app-download'; // ╔══════════════════════════════════════════════════════════════════════════╗ // ║ PARAMS ║ @@ -837,9 +829,9 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> { /** * Present the code redemption sheet for offer codes (iOS only) * @returns The verified redeemed purchase when built with Xcode 27+ and - * running on Apple 27+, or null when the system sheet cannot return the - * transaction directly (StoreKit 2 on iOS/Catalyst 16–26 and visionOS 1–26; - * StoreKit 1 on iOS/Catalyst 15). + * running on Apple 27+. Earlier iOS/visionOS system sheets return null; + * Catalyst 16–26 surfaces StoreKitError.unknown, and Catalyst 15 is a no-op + * that returns null. * @platform iOS */ presentCodeRedemptionSheetIOS(): Promise; diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts index c8de204f6..056e0c007 100644 --- a/libraries/react-native-iap/src/types.ts +++ b/libraries/react-native-iap/src/types.ts @@ -813,11 +813,12 @@ export interface Mutation { * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift index fa0bf16c9..ab242fc34 100644 --- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift +++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift @@ -380,7 +380,7 @@ enum StoreKitTypesBridge { } } - static func purchaseOptions( + static func purchaseOptionsIOS( from props: some IosPropsProtocol, product: StoreKit.Product? = nil, purchaseIntentOffer: StoreKit.Product.SubscriptionOffer? = nil @@ -766,7 +766,7 @@ enum StoreKitTypesBridge { } } - static func standardizedDiscountOfferType( + static func standardizedDiscountOfferTypeIOS( from type: StoreKit.Product.SubscriptionOffer.OfferType ) -> DiscountOfferType? { #if compiler(>=6.1) @@ -780,7 +780,7 @@ enum StoreKitTypesBridge { return nil } - static func purchaseOfferTypeString(from type: StoreKit.Transaction.OfferType) -> String { + static func purchaseOfferTypeStringIOS(from type: StoreKit.Transaction.OfferType) -> String { #if compiler(>=6.1) if type == .winBack { return SubscriptionOfferTypeIOS.winBack.rawValue @@ -838,7 +838,7 @@ private extension StoreKitTypesBridge { @available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) static func makeSubscriptionPricingTerm(from terms: StoreKit.Product.SubscriptionInfo.PricingTerms) -> SubscriptionPricingTermsIOS { let offers = terms.subscriptionOffers.compactMap { offer in - standardizedDiscountOfferType(from: offer.type).map { type in + standardizedDiscountOfferTypeIOS(from: offer.type).map { type in makeStandardizedSubscriptionOffer(from: offer, type: type) } } @@ -1083,7 +1083,7 @@ private extension StoreKitTypesBridge { paymentModeString = PaymentModeIOS.empty.rawValue } - let typeString = purchaseOfferTypeString(from: offer.type) + let typeString = purchaseOfferTypeStringIOS(from: offer.type) return PurchaseOfferIOS( id: offer.id ?? "", diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 06f21b963..ce8355084 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -2627,11 +2627,12 @@ public protocol MutationResolver { /// Show the App Store offer code redemption sheet. /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or /// visionOS 27+, returns the verified transaction produced by the redemption. - /// Other supported paths present the system sheet and return null: StoreKit 2's - /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - /// Catalyst 15. - /// Reconcile null results through the normal transaction listener or an explicit + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func presentCodeRedemptionSheetIOS() async throws -> PurchaseIOS? diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 9ca4945cd..36ce3d215 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -138,7 +138,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { private override init() { super.init() - startPromotedPurchaseIntentListenerIfAvailable() + startPromotedPurchaseIntentListenerIfAvailableIOS() registerPromotedPurchaseObserverIfNeeded() } @@ -387,7 +387,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { #else let purchaseIntentOffer: StoreKit.Product.SubscriptionOffer? = nil #endif - let options = try StoreKitTypesBridge.purchaseOptions( + let options = try StoreKitTypesBridge.purchaseOptionsIOS( from: iosProps, product: product, purchaseIntentOffer: purchaseIntentOffer @@ -1337,9 +1337,10 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { /// Present a sheet for redeeming offer codes. /// - Note: Builds made with Xcode 27+ return the verified transaction on - /// iOS 27+, Mac Catalyst 27+, and visionOS 27+. Other supported paths - /// return nil after presenting the system sheet, so callers must reconcile - /// through the transaction listener or an available-purchases refresh. + /// iOS 27+, Mac Catalyst 27+, and visionOS 27+. Earlier iOS and visionOS + /// paths return nil after presenting the system sheet. Mac Catalyst 16–26 + /// throws StoreKitError.unknown, and Catalyst 15 returns nil after a + /// StoreKit 1 call that has no effect. /// - SeeAlso: https://developer.apple.com/documentation/storekit/appstore/presentoffercoderedeemsheet(from:options:) /// /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios @@ -1782,7 +1783,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { resources.unfinishedTransactionTask?.cancel() } - private func startPromotedPurchaseIntentListenerIfAvailable() { + private func startPromotedPurchaseIntentListenerIfAvailableIOS() { #if os(iOS) if #available(iOS 16.4, macCatalyst 16.4, *) { guard promotedPurchaseIntentTask == nil else { return } diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift index c6b4bab29..cb25bcf68 100644 --- a/packages/apple/Tests/OpenIapTests.swift +++ b/packages/apple/Tests/OpenIapTests.swift @@ -45,7 +45,7 @@ final class OpenIapTests: XCTestCase { } } - func testEntitlementSelectionUsesLatestDateThenTransactionId() { + func testEntitlementSelectionUsesLatestDateThenTransactionIdIOS() { let earlier = EntitlementSelectionKey( purchaseDate: Date(timeIntervalSince1970: 1), transactionId: 100 @@ -63,7 +63,7 @@ final class OpenIapTests: XCTestCase { XCTAssertLessThan(later, sameDateHigherId) } - func testPromotedPurchaseIntentOfferIsProductScopedAndOneShot() async { + func testPromotedPurchaseIntentOfferIsProductScopedAndOneShotIOS() async { let store = PromotedPurchaseIntentOfferStore() await store.record("win-back", for: "subscription") @@ -894,16 +894,16 @@ final class OpenIapTests: XCTestCase { @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *) func testStoreKitOfferTypeMappingsPreserveWinBack() { XCTAssertEqual( - StoreKitTypesBridge.standardizedDiscountOfferType(from: .introductory), + StoreKitTypesBridge.standardizedDiscountOfferTypeIOS(from: .introductory), .introductory ) XCTAssertEqual( - StoreKitTypesBridge.standardizedDiscountOfferType(from: .promotional), + StoreKitTypesBridge.standardizedDiscountOfferTypeIOS(from: .promotional), .promotional ) - XCTAssertNil(StoreKitTypesBridge.standardizedDiscountOfferType(from: .winBack)) + XCTAssertNil(StoreKitTypesBridge.standardizedDiscountOfferTypeIOS(from: .winBack)) XCTAssertEqual( - StoreKitTypesBridge.purchaseOfferTypeString(from: .winBack), + StoreKitTypesBridge.purchaseOfferTypeStringIOS(from: .winBack), SubscriptionOfferTypeIOS.winBack.rawValue ) } diff --git a/packages/apple/Tests/OpenIapTests/AppAccountTokenTests.swift b/packages/apple/Tests/OpenIapTests/AppAccountTokenTests.swift index 832f85651..3246335ee 100644 --- a/packages/apple/Tests/OpenIapTests/AppAccountTokenTests.swift +++ b/packages/apple/Tests/OpenIapTests/AppAccountTokenTests.swift @@ -17,7 +17,7 @@ final class AppAccountTokenTests: XCTestCase { ) // Should not throw for valid UUID - XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptions(from: props)) + XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) } func testAppAccountToken_ValidUUID_Uppercase_DoesNotThrow() throws { @@ -31,7 +31,7 @@ final class AppAccountTokenTests: XCTestCase { ) // Should not throw for valid uppercase UUID - XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptions(from: props)) + XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) } func testAppAccountToken_GeneratedUUID_DoesNotThrow() throws { @@ -45,7 +45,7 @@ final class AppAccountTokenTests: XCTestCase { ) // Should not throw for UUID generated from Foundation - XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptions(from: props)) + XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) } func testAppAccountToken_Nil_DoesNotThrow() throws { @@ -59,7 +59,7 @@ final class AppAccountTokenTests: XCTestCase { ) // Should not throw when appAccountToken is nil - XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptions(from: props)) + XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) } // MARK: - Invalid UUID Format Tests @@ -75,7 +75,7 @@ final class AppAccountTokenTests: XCTestCase { ) // Should throw developerError for non-UUID format - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -97,7 +97,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -117,7 +117,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -136,7 +136,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -156,7 +156,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -176,7 +176,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -197,7 +197,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptions(from: props)) + XCTAssertNoThrow(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) } func testAppAccountToken_SubscriptionProps_InvalidUUID_ThrowsDeveloperError() throws { @@ -210,7 +210,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError, got \(type(of: error))") return @@ -233,7 +233,7 @@ final class AppAccountTokenTests: XCTestCase { withOffer: nil ) - XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptions(from: props)) { error in + XCTAssertThrowsError(try StoreKitTypesBridge.purchaseOptionsIOS(from: props)) { error in guard let purchaseError = error as? PurchaseError else { XCTFail("Expected PurchaseError") return diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt index e1a61f342..33f84f581 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-08-09T12:55:09.936Z +> Generated: 2026-08-09T13:54:08.179Z ## Table of Contents 1. Installation @@ -1381,34 +1381,34 @@ This document provides external API reference for Apple's StoreKit 2 framework. ## Recent StoreKit Features -| Feature | iOS Version | Description | -| -------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- | -| Win-back offers | iOS 18.0 | Re-engage churned subscribers | -| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | -| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key | -| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) | -| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | -| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | -| `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) | -| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | -| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | -| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | -| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | -| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | -| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | -| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | -| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions | -| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata | -| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data | -| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp | -| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation | -| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform | -| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload | -| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API | -| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning | -| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers | -| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 | +| Feature | iOS Version | Description | +| -------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Win-back offers | iOS 18.0 | Re-engage churned subscribers | +| `Product.SubscriptionInfo.RenewalInfo.eligibleWinBackOfferIDs` | iOS 18.0 | Query win-back offer eligibility before purchase | +| Consumable transaction history | iOS 18.0 | Opt-in via `SKIncludeConsumableInAppPurchaseHistory` Info.plist key | +| StoreKit `Message.billingIssue` | iOS / Mac Catalyst 16.4, visionOS 1.0 | Listener for subscription billing issues (`Message` is unavailable on macOS, tvOS, and watchOS) | +| UI context for purchases | iOS 18.2 | Required for proper payment sheet display | +| External purchase notice | iOS 17.4 | `ExternalPurchase.presentNoticeSheet()` | +| `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) | +| `Transaction.offerPeriod` | iOS 18.4 | Offer period information on Transaction | +| `Transaction.advancedCommerceInfo` | iOS 18.4 | Advanced Commerce API data on Transaction | +| `Transaction.appTransactionID` | iOS 18.4 | Per-Apple-Account identifier on Transaction | +| Expanded offer codes | iOS 18.4 | Offer codes for consumables/non-consumables | +| JWS promotional offers | WWDC 2025 | New `promotionalOffer` purchase option with JWS format | +| `introductoryOfferEligibility` | WWDC 2025 | Set eligibility via purchase option | +| `SubscriptionStatus` by Transaction ID | WWDC 2025 | `status(for: transactionID:)` | +| Monthly subscriptions with a 12-month commitment | iOS 26.4+ runtime / Xcode 26.5 SDK | Monthly billing option for annual auto-renewable subscriptions | +| Subscription Bundles and Suites | Apple 27 / Xcode 27 beta SDK | Read-only product, bundled-subscription, transaction, and renewal metadata | +| Bundle ownership and revocation metadata | Xcode 27 beta SDK | Back-deployed assigned ownership, bundle-upgrade reason, assignment revocation, and unbundling data | +| `AppTransaction.storeType`, `revocationDate` | Xcode 27 beta SDK | App-acquisition channel and back-deployed revocation timestamp | +| `AppTransaction.all` | Apple 27 / Xcode 27 beta SDK | Async sequence of app-acquisition records; not exported as an OpenIAP 3 operation | +| `AppStore.Platform.managed` | Xcode 27 beta SDK | Back-deployed managed-distribution acquisition platform | +| Advanced Commerce item partners | Apple 27 / Xcode 27 beta SDK | Partner identifiers and names in each item-details JSON payload | +| Group purchases and volume purchasing | Announced at WWDC 2026 | Group Purchases are planned for later in 2026; Xcode 27 beta 4 has no public StoreKit group API | +| Retention Messaging | WWDC 2026 | Cancellation-flow messaging and offers, including real-time server decisioning | +| Retention offer type | WWDC 2026 | Signed transaction / renewal info can report offer type `5` for retention offers | +| Offer codes for all IAP types | 2026 | Offer codes expand beyond auto-renewable subscriptions; IAP promo-code creation ends March 26, 2026 | ### StoreKit Message presentation @@ -1460,9 +1460,11 @@ sheet but does not return the redeemed transaction. OpenIAP 3 changes `presentCodeRedemptionSheetIOS` to return `PurchaseIOS?`. Xcode 27 builds call the new API, require a verified result, and return the -mapped transaction. Xcode 26 builds use the StoreKit 2 scene API on iOS and Mac -Catalyst 16+ and visionOS 1+, and return `nil` after presentation; iOS and Mac -Catalyst 15 retain the StoreKit 1 fallback. Both nil-returning paths rely on the +mapped transaction on Apple 27+ runtimes. Older result paths use the StoreKit 2 +scene API on iOS 16+ and visionOS 1+ and return `nil` after presentation; iOS 15 +retains the StoreKit 1 fallback. In Mac Catalyst apps, the scene API throws +`StoreKitError.unknown`, while the Catalyst 15 StoreKit 1 call has no effect and +returns `nil`. Nil results from an actually presented sheet rely on the transaction listener or explicit purchase reconciliation. Xcode 27 beta 4 declares `RedeemOption`, but its public symbol graph exposes no constructible option values, so OpenIAP @@ -1749,17 +1751,28 @@ let result = try await product.purchase(confirmIn: window) > **OpenIAP Note**: UI context is handled automatically in OpenIAP using the active window scene. -## AppTransaction Updates (Xcode 16.4+; back-deployed) +## AppTransaction Identity Updates (Xcode 16.4+; back-deployed) ```swift let appTransaction = try await AppTransaction.shared -// New in iOS 18.4 (back-deployed to iOS 15) +// Introduced in iOS 18.4 (back-deployed to the AppTransaction baseline) let appTransactionID = appTransaction.appTransactionID // Globally unique per Apple Account -let originalPlatform = appTransaction.originalPlatform // Original purchase platform +let originalPlatform = appTransaction.originalPlatform // Typed value on iOS 18.4+ +``` + +OpenIAP uses `originalPlatformStringRepresentation` on older runtimes. The typed +`originalPlatform` property starts at iOS 18.4, macOS 15.4, tvOS 18.4, watchOS +11.4, and visionOS 2.4. + +## AppTransaction Acquisition Updates (Xcode 27 SDK) + +```swift +let appTransaction = try await AppTransaction.shared // Public in the Xcode 27 SDK and back-deployed to these existing runtimes let revocationDate = appTransaction.revocationDate // App-acquisition revocation +// Runtime-gated to Apple 27+ let storeType = appTransaction.storeType // Acquisition store channel ``` @@ -1805,6 +1818,10 @@ if let advancedInfo = product.advancedCommerceInfo { } ``` +For Advanced Commerce transactions, OpenIAP maps +`AdvancedCommerceInfoIOS.period` as an optional `SubscriptionPeriodValueIOS` +containing the subscription period `unit` and integer `value`. + ## Monthly Subscriptions With 12-Month Commitment (iOS 26.4+) This billing plan lets customers pay monthly while committing to an annual diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt index 7abe561e6..4af609b9b 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-08-09T12:55:09.936Z +> Generated: 2026-08-09T13:54:08.179Z ## Installation diff --git a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx index f7bfd59fc..c3aac1fd4 100644 --- a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx @@ -117,8 +117,11 @@ var appTx = await ((QueryResolver)OpenIapClient.Instance).GetAppTransactionIOSAs originalPurchaseDate, environment,{' '} deviceVerification, deviceVerificationNonce,{' '} signedDate, appId, appVersionId,{' '} - preorderDate, the back-deployed{' '} - appTransactionId, and originalPlatform). + preorderDate, appTransactionId (introduced on + iOS 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, and visionOS 2.4 and + back-deployed by Xcode 16.4+), and originalPlatform (typed + at those runtime floors, with a compatibility string on the + AppTransaction baseline).

    ); diff --git a/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx index 2af3366c4..e5db6f8ba 100644 --- a/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx @@ -31,13 +31,16 @@ function PresentCodeRedemptionSheetIOS() { In Xcode 27+ builds on iOS 27, Mac Catalyst 27, and visionOS 27 or later, this calls{' '} AppStore.presentOfferCodeRedeemSheet(from:options:) and - returns the verified transaction produced by redemption. On iOS and Mac - Catalyst 16–26 and visionOS 1–26, it uses the StoreKit 2 scene-based - sheet and returns null. Builds made with an older SDK use - that same path on Apple 27 runtimes. iOS and Mac Catalyst 15 use the - StoreKit 1 fallback and also return null. For any - nil-returning path, use the purchase listener or refresh available - purchases after the sheet closes. See the{' '} + returns the verified transaction produced by redemption. On iOS 16–26 + and visionOS 1–26, it uses the StoreKit 2 scene-based sheet and returns{' '} + null; an older SDK uses that same path on those platforms + at Apple 27. iOS 15 uses the StoreKit 1 fallback and also returns{' '} + null. In Mac Catalyst apps, the scene-based API throws{' '} + StoreKitError.unknown (surfaced by OpenIAP as a purchase + error), while the Catalyst 15 StoreKit 1 call has no effect and returns{' '} + null. Reconcile null results only from an actually + presented sheet through the purchase listener or an available-purchases + refresh. See the{' '} PurchaseIOS | null — the verified redeemed transaction on Apple 27+ runtimes from Xcode 27+ builds. A null result - means the system sheet was presented successfully but that API path - cannot return its transaction directly; it does not mean the feature is - unsupported or that redemption failed. + normally means the iOS or visionOS system sheet was presented but that + API path cannot return its transaction directly. Mac Catalyst 15 is the + exception: StoreKit 1 has no effect there and OpenIAP returns{' '} + null. Mac Catalyst 16–26 rejects the scene API with an + error instead of returning null.

    Example

    @@ -91,7 +96,8 @@ function PresentCodeRedemptionSheetIOS() { if let purchase { print("Verified redemption:", purchase.productId) } else { - // Nil result: reconcile through the listener or refresh purchases. + // iOS/visionOS: reconcile through the listener or refresh purchases. + // Mac Catalyst 15: StoreKit 1 has no effect, so no sheet was shown. }`} ), kotlin: ( @@ -104,8 +110,14 @@ if (purchase != null) println("Verified redemption: " + purchase.productId)`} ), dart: ( diff --git a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx index 68d7ce147..1dbbd9b1f 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -26,13 +26,15 @@ function OfferCodeRedemption() { Current API boundary:{' '} presentCodeRedemptionSheetIOS returns the verified{' '} PurchaseIOS produced by Apple's new StoreKit API on - iOS 27, Mac Catalyst 27, and visionOS 27 or later. Earlier supported - runtimes present Apple's system sheet and return null, - so observe the redeemed purchase through{' '} + iOS 27, Mac Catalyst 27, and visionOS 27 or later. Earlier iOS and + visionOS runtimes present Apple's system sheet and return{' '} + null, so observe the redeemed purchase through{' '} purchaseUpdatedListener {' '} - or an explicit available-purchases refresh. + or an explicit available-purchases refresh. Mac Catalyst 16–26 instead + throws StoreKitError.unknown, and the Catalyst 15 StoreKit + 1 call has no effect.

    @@ -92,12 +94,14 @@ function OfferCodeRedemption() { Initialize the store connection before presenting the sheet, and register a purchase listener before the user redeems a code. Builds made with Xcode 27+ return the verified redeemed - purchase directly on Apple 27+ runtimes. Other supported paths - return null after presenting the system sheet, - and the redeemed transaction arrives through the listener or a - subsequent refresh. The implementation uses StoreKit 2 on iOS - and Mac Catalyst 16+ and visionOS 1+, and keeps the StoreKit 1 - fallback only for iOS and Mac Catalyst 15. + purchase directly on Apple 27+ runtimes. Earlier iOS and + visionOS paths return null after presenting the + system sheet, and the redeemed transaction arrives through the + listener or a subsequent refresh. Mac Catalyst 16–26 rejects + the StoreKit 2 scene API with{' '} + StoreKitError.unknown; its StoreKit 1 call on + Catalyst 15 has no effect. iOS 15 keeps the functional + StoreKit 1 fallback.

    {{ @@ -269,10 +273,11 @@ func _exit_tree() -> void:
  • The pre-built Godot GDExtension must also have been compiled - with Xcode 27; an Xcode 26-built framework uses the - scene-based StoreKit 2 API and returns null{' '} - even on Apple 27. Build from source or confirm the release - artifact toolchain. + with Xcode 27; an Xcode 26-built framework uses the older + API path even on Apple 27. On iOS and visionOS it returns{' '} + null; on Mac Catalyst it follows the + platform-specific error or no-op behavior above. Build from + source or confirm the release artifact toolchain.
  • Use a physical device for an actual App Store sandbox or diff --git a/packages/docs/src/pages/docs/setup/godot.tsx b/packages/docs/src/pages/docs/setup/godot.tsx index 04b3e0bf2..11bab9e82 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -123,11 +123,13 @@ make android offer code redemption {' '} only returns a verified result when the framework was built with Xcode - 27 or later — a framework built with Xcode 26 uses the scene-based - StoreKit 2 redemption sheet and returns null, even on - devices running the latest OS. The published godot-iap 3.0.0 framework - is built with Xcode 27. If you build from source and use offer codes, - build with Xcode 27 or later. + 27 or later and runs on iOS 27, Mac Catalyst 27, or visionOS + 27 or later. Older runtimes still use the non-result system-sheet path + even when the framework was built with Xcode 27. A framework built + with Xcode 26 also uses that older path, including on Apple 27 + devices. The published godot-iap 3.0.0 framework is built with Xcode + 27. If you build from source and use offer codes, build with Xcode 27 + or later; the runtime requirement remains separate.

    diff --git a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx index 810f3e6a1..7c6e75051 100644 --- a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -137,8 +137,9 @@ function AppTransactionIos() { appTransactionId - Stable app transaction ID. StoreKit back-deploys this property - across the supported AppTransaction runtime range when built + Stable app transaction ID. Introduced on iOS 18.4, macOS 15.4, + tvOS 18.4, watchOS 11.4, and visionOS 2.4; StoreKit back-deploys + it across the supported AppTransaction runtime range when built with Xcode 16.4+. @@ -149,8 +150,9 @@ function AppTransactionIos() { Original platform. OpenIAP uses StoreKit's compatibility string on the AppTransaction baseline and the typed value on iOS - 18.4+, including the Xcode 27 SDK's back-deployed{' '} - managed value. + 18.4, macOS 15.4, tvOS 18.4, watchOS 11.4, and visionOS 2.4 or + later. Xcode 16.4+ provides the compatibility path; the Xcode 27 + SDK also adds the back-deployed managed value. diff --git a/packages/docs/src/pages/docs/updates/deprecations.tsx b/packages/docs/src/pages/docs/updates/deprecations.tsx index 27f153630..bb9a0a4cf 100644 --- a/packages/docs/src/pages/docs/updates/deprecations.tsx +++ b/packages/docs/src/pages/docs/updates/deprecations.tsx @@ -79,7 +79,7 @@ const migrationGroups = [ ['willExpireSoon', 'daysUntilExpirationIOS'], [ 'presentCodeRedemptionSheetIOS Boolean result', - 'nullable PurchaseIOS result: verified on Apple 27+ with Xcode 27+; null after the system sheet on iOS/Catalyst 15–26, visionOS 1–26, or Apple 27 from an older build', + 'nullable PurchaseIOS result: verified on Apple 27+ with Xcode 27+; null after the system sheet on iOS 15–26 and visionOS 1–26; Catalyst 16–26 throws StoreKitError.unknown and Catalyst 15 has no effect', ], ['receipt-failed', 'purchase-verification-failed'], ['receipt-finished', 'purchase-verification-finished'], 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 442769b44..6b1f9b81c 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 @@ -5582,11 +5582,12 @@ public interface MutationResolver { * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ @@ -5970,11 +5971,12 @@ public data class MutationHandlers( * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ diff --git a/packages/gql/src/api-ios.graphql b/packages/gql/src/api-ios.graphql index 4da46a5be..3823c5b0b 100644 --- a/packages/gql/src/api-ios.graphql +++ b/packages/gql/src/api-ios.graphql @@ -134,11 +134,12 @@ extend type Mutation { Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. - Other supported paths present the system sheet and return null: StoreKit 2's - scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - Catalyst 15. - Reconcile null results through the normal transaction listener or an explicit + StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + call has no effect and returns null. Reconcile null results from a presented + sheet through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios """ diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs index 6338ecb7e..258d3b23c 100644 --- a/packages/gql/src/generated/Types.cs +++ b/packages/gql/src/generated/Types.cs @@ -4180,11 +4180,12 @@ public interface MutationResolver /// Show the App Store offer code redemption sheet. /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or /// visionOS 27+, returns the verified transaction produced by the redemption. - /// Other supported paths present the system sheet and return null: StoreKit 2's - /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - /// Catalyst 15. - /// Reconcile null results through the normal transaction listener or an explicit + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Task PresentCodeRedemptionSheetIOSAsync(); diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 490f79e33..e614239c9 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -5528,11 +5528,12 @@ public interface MutationResolver { * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ @@ -5916,11 +5917,12 @@ public data class MutationHandlers( * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */ diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index 06f21b963..ce8355084 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -2627,11 +2627,12 @@ public protocol MutationResolver { /// Show the App Store offer code redemption sheet. /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or /// visionOS 27+, returns the verified transaction produced by the redemption. - /// Other supported paths present the system sheet and return null: StoreKit 2's - /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - /// Catalyst 15. - /// Reconcile null results through the normal transaction listener or an explicit + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios func presentCodeRedemptionSheetIOS() async throws -> PurchaseIOS? diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 6db39b914..50f99a113 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -5392,11 +5392,12 @@ abstract class MutationResolver { /// Show the App Store offer code redemption sheet. /// When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or /// visionOS 27+, returns the verified transaction produced by the redemption. - /// Other supported paths present the system sheet and return null: StoreKit 2's - /// scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - /// runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - /// Catalyst 15. - /// Reconcile null results through the normal transaction listener or an explicit + /// StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + /// visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + /// iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + /// scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + /// call has no effect and returns null. Reconcile null results from a presented + /// sheet through the normal transaction listener or an explicit /// available-purchases refresh. /// See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios Future presentCodeRedemptionSheetIOS(); diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index 163cad13a..f59061cfd 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -5796,7 +5796,7 @@ class Mutation: const return_type = "Boolean" const is_array = false - ## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios + ## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 call has no effect and returns null. Reconcile null results from a presented sheet through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios class presentCodeRedemptionSheetIOSField: const name = "presentCodeRedemptionSheetIOS" const snake_name = "present_code_redemption_sheet_ios" @@ -6257,7 +6257,7 @@ static func begin_refund_request_ios_args(sku: String) -> Dictionary: static func sync_ios_args() -> Dictionary: return {} -## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. Other supported paths present the system sheet and return null: StoreKit 2's scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac Catalyst 15. Reconcile null results through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios +## Show the App Store offer code redemption sheet. When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or visionOS 27+, returns the verified transaction produced by the redemption. StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 call has no effect and returns null. Reconcile null results from a presented sheet through the normal transaction listener or an explicit available-purchases refresh. See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios static func present_code_redemption_sheet_ios_args() -> Dictionary: return {} diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index c8de204f6..056e0c007 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -813,11 +813,12 @@ export interface Mutation { * Show the App Store offer code redemption sheet. * When built with Xcode 27+ and running on iOS 27+, Mac Catalyst 27+, or * visionOS 27+, returns the verified transaction produced by the redemption. - * Other supported paths present the system sheet and return null: StoreKit 2's - * scene-based sheet on iOS and Mac Catalyst 16–26, visionOS 1–26, or Apple 27 - * runtimes from an older build; and the StoreKit 1 fallback on iOS or Mac - * Catalyst 15. - * Reconcile null results through the normal transaction listener or an explicit + * StoreKit 2's scene-based sheet returns null after presentation on iOS 16–26, + * visionOS 1–26, and those platforms on Apple 27 when built with an older SDK. + * iOS 15 uses the StoreKit 1 sheet and also returns null. On Mac Catalyst, the + * scene-based API throws StoreKitError.unknown, while the Catalyst 15 StoreKit 1 + * call has no effect and returns null. Reconcile null results from a presented + * sheet through the normal transaction listener or an explicit * available-purchases refresh. * See: https://openiap.dev/docs/apis/ios/present-code-redemption-sheet-ios */