From c717c92b6b14011d5851fb602a673083ce420a43 Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:15:38 +0900
Subject: [PATCH 1/8] feat(gql): add ios billing plan types
---
packages/gql/src/type-ios.graphql | 80 +++++++++++++++++++++++++++++++
1 file changed, 80 insertions(+)
diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql
index 55992a265..750e933ef 100644
--- a/packages/gql/src/type-ios.graphql
+++ b/packages/gql/src/type-ios.graphql
@@ -37,6 +37,21 @@ enum SubscriptionOfferTypeIOS {
WinBack
}
+enum SubscriptionBillingPlanTypeIOS {
+ """
+ Unknown or unsupported billing plan type.
+ """
+ Unknown
+ """
+ Monthly billing with a 12-month commitment.
+ """
+ Monthly
+ """
+ Up-front billing for the full subscription period.
+ """
+ UpFront
+}
+
# iOS subscription period (unit + value)
type SubscriptionPeriodValueIOS {
unit: SubscriptionPeriodIOS!
@@ -61,11 +76,27 @@ type SubscriptionOfferIOS
type SubscriptionInfoIOS {
introductoryOffer: SubscriptionOfferIOS
+ pricingTerms: [SubscriptionPricingTermsIOS!]
promotionalOffers: [SubscriptionOfferIOS!]
subscriptionGroupId: String!
subscriptionPeriod: SubscriptionPeriodValueIOS!
}
+type SubscriptionCommitmentInfoIOS {
+ displayPrice: String!
+ period: SubscriptionPeriodValueIOS!
+ price: Float!
+}
+
+type SubscriptionPricingTermsIOS {
+ billingDisplayPrice: String!
+ billingPeriod: SubscriptionPeriodValueIOS!
+ billingPlanType: SubscriptionBillingPlanTypeIOS!
+ billingPrice: Float!
+ commitmentInfo: SubscriptionCommitmentInfoIOS!
+ subscriptionOffers: [SubscriptionOffer!]
+}
+
# iOS product
type ProductIOS implements ProductCommon {
# Common fields
@@ -95,6 +126,12 @@ type ProductIOS implements ProductCommon {
"""
subscriptionOffers: [SubscriptionOffer!]
+ """
+ iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ monthly subscriptions with a 12-month commitment.
+ """
+ pricingTermsIOS: [SubscriptionPricingTermsIOS!]
+
# Deprecated platform-specific field
"""
@deprecated Use subscriptionOffers instead for cross-platform compatibility.
@@ -131,6 +168,12 @@ type ProductSubscriptionIOS implements ProductCommon {
"""
subscriptionOffers: [SubscriptionOffer!]
+ """
+ iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ monthly subscriptions with a 12-month commitment.
+ """
+ pricingTermsIOS: [SubscriptionPricingTermsIOS!]
+
"""
App Store subscription group identifier for intro-offer eligibility checks.
"""
@@ -218,6 +261,14 @@ type PurchaseIOS implements PurchaseCommon {
countryCodeIOS: String
renewalInfoIOS: RenewalInfoIOS
"""
+ iOS 26.4+ billing plan selected for this transaction.
+ """
+ billingPlanTypeIOS: SubscriptionBillingPlanTypeIOS
+ """
+ iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ """
+ commitmentInfoIOS: TransactionCommitmentInfoIOS
+ """
Advanced Commerce API metadata (iOS 18.4+).
Present only for transactions that use the Advanced Commerce API.
Contains item details, tax information, and refund data for generic SKU purchases.
@@ -288,6 +339,11 @@ input RequestSubscriptionIosProps {
"""
promotionalOfferJWS: PromotionalOfferJWSInputIOS
"""
+ Billing plan to use when purchasing an annual subscription that offers
+ monthly billing with a 12-month commitment (iOS 26.4+).
+ """
+ billingPlanType: SubscriptionBillingPlanTypeIOS
+ """
Override introductory offer eligibility (iOS 15+, WWDC 2025).
Set to true to indicate the user is eligible for introductory offer,
or false to indicate they are not. When nil, the system determines eligibility.
@@ -445,6 +501,30 @@ type RenewalInfoIOS {
Possible values: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", "WIN_BACK", etc.
"""
renewalOfferType: String
+ """
+ iOS 26.4+ billing plan that will renew after the current period.
+ """
+ renewalBillingPlanType: SubscriptionBillingPlanTypeIOS
+ """
+ iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ 12-month commitment.
+ """
+ commitmentInfo: RenewalCommitmentInfoIOS
+}
+
+type TransactionCommitmentInfoIOS {
+ billingPeriodNumber: Int!
+ commitmentExpiresDate: Float!
+ commitmentPrice: Float!
+ totalBillingPeriods: Int!
+}
+
+type RenewalCommitmentInfoIOS {
+ commitmentAutoRenewProductId: String!
+ commitmentAutoRenewStatus: Boolean!
+ commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS!
+ commitmentRenewalDate: Float!
+ commitmentRenewalPrice: Float!
}
type SubscriptionStatusIOS {
From 0a46d2b888cbb759f1d38fe7de3dfaa4856138f4 Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:15:50 +0900
Subject: [PATCH 2/8] chore(gql): sync ios billing plan types
---
libraries/expo-iap/src/types.ts | 59 ++++
.../flutter_inapp_purchase/lib/types.dart | 210 ++++++++++++
libraries/godot-iap/addons/godot-iap/types.gd | 298 ++++++++++++++++++
.../io/github/hyochan/kmpiap/openiap/Types.kt | 196 ++++++++++++
libraries/maui-iap/src/OpenIap.Maui/Types.cs | 135 ++++++++
libraries/react-native-iap/src/types.ts | 59 ++++
packages/apple/Sources/Models/Types.swift | 60 ++++
.../src/main/java/dev/hyo/openiap/Types.kt | 193 ++++++++++++
packages/gql/src/generated/Types.cs | 135 ++++++++
packages/gql/src/generated/Types.kt | 196 ++++++++++++
packages/gql/src/generated/Types.swift | 60 ++++
packages/gql/src/generated/types.dart | 210 ++++++++++++
packages/gql/src/generated/types.gd | 298 ++++++++++++++++++
packages/gql/src/generated/types.ts | 59 ++++
14 files changed, 2168 insertions(+)
diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts
index 518f5ffde..e62ef2b05 100644
--- a/libraries/expo-iap/src/types.ts
+++ b/libraries/expo-iap/src/types.ts
@@ -985,6 +985,11 @@ export interface ProductIOS extends ProductCommon {
jsonRepresentationIOS: string;
platform: 'ios';
price?: (number | null);
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ pricingTermsIOS?: (SubscriptionPricingTermsIOS[] | null);
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
* @deprecated Use subscriptionOffers instead
@@ -1108,6 +1113,11 @@ export interface ProductSubscriptionIOS extends ProductCommon {
jsonRepresentationIOS: string;
platform: 'ios';
price?: (number | null);
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ pricingTermsIOS?: (SubscriptionPricingTermsIOS[] | null);
/** App Store subscription group identifier for intro-offer eligibility checks. */
subscriptionGroupIdIOS?: (string | null);
/**
@@ -1234,6 +1244,10 @@ export interface PurchaseIOS extends PurchaseCommon {
advancedCommerceInfoIOS?: (AdvancedCommerceInfoIOS | null);
appAccountToken?: (string | null);
appBundleIdIOS?: (string | null);
+ /** iOS 26.4+ billing plan selected for this transaction. */
+ billingPlanTypeIOS?: (SubscriptionBillingPlanTypeIOS | null);
+ /** iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment. */
+ commitmentInfoIOS?: (TransactionCommitmentInfoIOS | null);
countryCodeIOS?: (string | null);
currencyCodeIOS?: (string | null);
currencySymbolIOS?: (string | null);
@@ -1454,12 +1468,25 @@ export interface RefundResultIOS {
status: string;
}
+export interface RenewalCommitmentInfoIOS {
+ commitmentAutoRenewProductId: string;
+ commitmentAutoRenewStatus: boolean;
+ commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS;
+ commitmentRenewalDate: number;
+ commitmentRenewalPrice: number;
+}
+
/**
* Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
* https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
*/
export interface RenewalInfoIOS {
autoRenewPreference?: (string | null);
+ /**
+ * iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ * 12-month commitment.
+ */
+ commitmentInfo?: (RenewalCommitmentInfoIOS | null);
/**
* When subscription expires due to cancellation/billing issue
* Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
@@ -1486,6 +1513,8 @@ export interface RenewalInfoIOS {
* Possible values: "AGREED", "PENDING", null (no price increase)
*/
priceIncreaseStatus?: (string | null);
+ /** iOS 26.4+ billing plan that will renew after the current period. */
+ renewalBillingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
* Expected renewal date (milliseconds since epoch)
* For active subscriptions, when the next renewal/charge will occur
@@ -1645,6 +1674,11 @@ export interface RequestSubscriptionIosProps {
advancedCommerceData?: (string | null);
andDangerouslyFinishTransactionAutomatically?: (boolean | null);
appAccountToken?: (string | null);
+ /**
+ * Billing plan to use when purchasing an annual subscription that offers
+ * monthly billing with a 12-month commitment (iOS 26.4+).
+ */
+ billingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -1777,8 +1811,17 @@ export interface Subscription {
export type SubscriptionPurchaseUpdatedArgs = (PurchaseUpdatedListenerOptions | null) | undefined;
+export type SubscriptionBillingPlanTypeIOS = 'unknown' | 'monthly' | 'up-front';
+
+export interface SubscriptionCommitmentInfoIOS {
+ displayPrice: string;
+ period: SubscriptionPeriodValueIOS;
+ price: number;
+}
+
export interface SubscriptionInfoIOS {
introductoryOffer?: (SubscriptionOfferIOS | null);
+ pricingTerms?: (SubscriptionPricingTermsIOS[] | null);
promotionalOffers?: (SubscriptionOfferIOS[] | null);
subscriptionGroupId: string;
subscriptionPeriod: SubscriptionPeriodValueIOS;
@@ -1900,6 +1943,15 @@ export interface SubscriptionPeriodValueIOS {
value: number;
}
+export interface SubscriptionPricingTermsIOS {
+ billingDisplayPrice: string;
+ billingPeriod: SubscriptionPeriodValueIOS;
+ billingPlanType: SubscriptionBillingPlanTypeIOS;
+ billingPrice: number;
+ commitmentInfo: SubscriptionCommitmentInfoIOS;
+ subscriptionOffers?: (SubscriptionOffer[] | null);
+}
+
/**
* Product-level subscription replacement parameters (Android)
* Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
@@ -1926,6 +1978,13 @@ export interface SubscriptionStatusIOS {
state: string;
}
+export interface TransactionCommitmentInfoIOS {
+ billingPeriodNumber: number;
+ commitmentExpiresDate: number;
+ commitmentPrice: number;
+ totalBillingPeriods: number;
+}
+
/**
* User Choice Billing event details (Android)
* Fired when a user selects alternative billing in the User Choice Billing dialog
diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart
index 9723c028e..bbe4f26b5 100644
--- a/libraries/flutter_inapp_purchase/lib/types.dart
+++ b/libraries/flutter_inapp_purchase/lib/types.dart
@@ -795,6 +795,33 @@ enum SubResponseCodeAndroid {
String toJson() => value;
}
+enum SubscriptionBillingPlanTypeIOS {
+ /// Unknown or unsupported billing plan type.
+ Unknown('unknown'),
+ /// Monthly billing with a 12-month commitment.
+ Monthly('monthly'),
+ /// Up-front billing for the full subscription period.
+ UpFront('up-front');
+
+ const SubscriptionBillingPlanTypeIOS(this.value);
+ final String value;
+
+ factory SubscriptionBillingPlanTypeIOS.fromJson(String value) {
+ final normalized = value.toLowerCase().replaceAll('_', '-');
+ switch (normalized) {
+ case 'unknown':
+ return SubscriptionBillingPlanTypeIOS.Unknown;
+ case 'monthly':
+ return SubscriptionBillingPlanTypeIOS.Monthly;
+ case 'up-front':
+ return SubscriptionBillingPlanTypeIOS.UpFront;
+ }
+ throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS value: $value');
+ }
+
+ String toJson() => value;
+}
+
enum SubscriptionOfferTypeIOS {
Introductory('introductory'),
Promotional('promotional'),
@@ -2500,6 +2527,7 @@ class ProductIOS extends Product implements ProductCommon {
required this.jsonRepresentationIOS,
this.platform = IapPlatform.IOS,
this.price,
+ this.pricingTermsIOS,
this.subscriptionInfoIOS,
this.subscriptionOffers,
required this.title,
@@ -2518,6 +2546,9 @@ class ProductIOS extends Product implements ProductCommon {
final String jsonRepresentationIOS;
final IapPlatform platform;
final double? price;
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ final List? pricingTermsIOS;
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
final SubscriptionInfoIOS? subscriptionInfoIOS;
/// Standardized subscription offers.
@@ -2542,6 +2573,7 @@ class ProductIOS extends Product implements ProductCommon {
jsonRepresentationIOS: json['jsonRepresentationIOS'] as String,
platform: IapPlatform.fromJson(json['platform'] as String),
price: (json['price'] as num?)?.toDouble(),
+ pricingTermsIOS: (json['pricingTermsIOS'] as List?) == null ? null : (json['pricingTermsIOS'] as List?)!.map((e) => SubscriptionPricingTermsIOS.fromJson(e as Map)).toList(),
subscriptionInfoIOS: json['subscriptionInfoIOS'] != null ? SubscriptionInfoIOS.fromJson(json['subscriptionInfoIOS'] as Map) : null,
subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
title: json['title'] as String,
@@ -2565,6 +2597,7 @@ class ProductIOS extends Product implements ProductCommon {
'jsonRepresentationIOS': jsonRepresentationIOS,
'platform': platform.toJson(),
'price': price,
+ 'pricingTermsIOS': pricingTermsIOS == null ? null : pricingTermsIOS!.map((e) => e.toJson()).toList(),
'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(),
'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
'title': title,
@@ -2737,6 +2770,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
required this.jsonRepresentationIOS,
this.platform = IapPlatform.IOS,
this.price,
+ this.pricingTermsIOS,
this.subscriptionGroupIdIOS,
this.subscriptionInfoIOS,
this.subscriptionOffers,
@@ -2765,6 +2799,9 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
final String jsonRepresentationIOS;
final IapPlatform platform;
final double? price;
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ final List? pricingTermsIOS;
/// App Store subscription group identifier for intro-offer eligibility checks.
final String? subscriptionGroupIdIOS;
/// @deprecated Use subscriptionOffers for offer metadata and subscriptionGroupIdIOS for the App Store subscription group identifier.
@@ -2798,6 +2835,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
jsonRepresentationIOS: json['jsonRepresentationIOS'] as String,
platform: IapPlatform.fromJson(json['platform'] as String),
price: (json['price'] as num?)?.toDouble(),
+ pricingTermsIOS: (json['pricingTermsIOS'] as List?) == null ? null : (json['pricingTermsIOS'] as List?)!.map((e) => SubscriptionPricingTermsIOS.fromJson(e as Map)).toList(),
subscriptionGroupIdIOS: json['subscriptionGroupIdIOS'] as String?,
subscriptionInfoIOS: json['subscriptionInfoIOS'] != null ? SubscriptionInfoIOS.fromJson(json['subscriptionInfoIOS'] as Map) : null,
subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
@@ -2830,6 +2868,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
'jsonRepresentationIOS': jsonRepresentationIOS,
'platform': platform.toJson(),
'price': price,
+ 'pricingTermsIOS': pricingTermsIOS == null ? null : pricingTermsIOS!.map((e) => e.toJson()).toList(),
'subscriptionGroupIdIOS': subscriptionGroupIdIOS,
'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(),
'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
@@ -3016,6 +3055,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
this.advancedCommerceInfoIOS,
this.appAccountToken,
this.appBundleIdIOS,
+ this.billingPlanTypeIOS,
+ this.commitmentInfoIOS,
this.countryCodeIOS,
this.currencyCodeIOS,
this.currencySymbolIOS,
@@ -3057,6 +3098,10 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
final AdvancedCommerceInfoIOS? advancedCommerceInfoIOS;
final String? appAccountToken;
final String? appBundleIdIOS;
+ /// iOS 26.4+ billing plan selected for this transaction.
+ final SubscriptionBillingPlanTypeIOS? billingPlanTypeIOS;
+ /// iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ final TransactionCommitmentInfoIOS? commitmentInfoIOS;
final String? countryCodeIOS;
final String? currencyCodeIOS;
final String? currencySymbolIOS;
@@ -3097,6 +3142,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
advancedCommerceInfoIOS: json['advancedCommerceInfoIOS'] != null ? AdvancedCommerceInfoIOS.fromJson(json['advancedCommerceInfoIOS'] as Map) : null,
appAccountToken: json['appAccountToken'] as String?,
appBundleIdIOS: json['appBundleIdIOS'] as String?,
+ billingPlanTypeIOS: json['billingPlanTypeIOS'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanTypeIOS'] as String) : null,
+ commitmentInfoIOS: json['commitmentInfoIOS'] != null ? TransactionCommitmentInfoIOS.fromJson(json['commitmentInfoIOS'] as Map) : null,
countryCodeIOS: json['countryCodeIOS'] as String?,
currencyCodeIOS: json['currencyCodeIOS'] as String?,
currencySymbolIOS: json['currencySymbolIOS'] as String?,
@@ -3140,6 +3187,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
'advancedCommerceInfoIOS': advancedCommerceInfoIOS?.toJson(),
'appAccountToken': appAccountToken,
'appBundleIdIOS': appBundleIdIOS,
+ 'billingPlanTypeIOS': billingPlanTypeIOS?.toJson(),
+ 'commitmentInfoIOS': commitmentInfoIOS?.toJson(),
'countryCodeIOS': countryCodeIOS,
'currencyCodeIOS': currencyCodeIOS,
'currencySymbolIOS': currencySymbolIOS,
@@ -3231,17 +3280,56 @@ class RefundResultIOS {
}
}
+class RenewalCommitmentInfoIOS {
+ const RenewalCommitmentInfoIOS({
+ required this.commitmentAutoRenewProductId,
+ required this.commitmentAutoRenewStatus,
+ required this.commitmentRenewalBillingPlanType,
+ required this.commitmentRenewalDate,
+ required this.commitmentRenewalPrice,
+ });
+
+ final String commitmentAutoRenewProductId;
+ final bool commitmentAutoRenewStatus;
+ final SubscriptionBillingPlanTypeIOS commitmentRenewalBillingPlanType;
+ final double commitmentRenewalDate;
+ final double commitmentRenewalPrice;
+
+ factory RenewalCommitmentInfoIOS.fromJson(Map json) {
+ return RenewalCommitmentInfoIOS(
+ commitmentAutoRenewProductId: json['commitmentAutoRenewProductId'] as String,
+ commitmentAutoRenewStatus: json['commitmentAutoRenewStatus'] as bool,
+ commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS.fromJson(json['commitmentRenewalBillingPlanType'] as String),
+ commitmentRenewalDate: (json['commitmentRenewalDate'] as num).toDouble(),
+ commitmentRenewalPrice: (json['commitmentRenewalPrice'] as num).toDouble(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'RenewalCommitmentInfoIOS',
+ 'commitmentAutoRenewProductId': commitmentAutoRenewProductId,
+ 'commitmentAutoRenewStatus': commitmentAutoRenewStatus,
+ 'commitmentRenewalBillingPlanType': commitmentRenewalBillingPlanType.toJson(),
+ 'commitmentRenewalDate': commitmentRenewalDate,
+ 'commitmentRenewalPrice': commitmentRenewalPrice,
+ };
+ }
+}
+
/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
class RenewalInfoIOS {
const RenewalInfoIOS({
this.autoRenewPreference,
+ this.commitmentInfo,
this.expirationReason,
this.gracePeriodExpirationDate,
this.isInBillingRetry,
this.jsonRepresentation,
this.pendingUpgradeProductId,
this.priceIncreaseStatus,
+ this.renewalBillingPlanType,
this.renewalDate,
this.renewalOfferId,
this.renewalOfferType,
@@ -3249,6 +3337,9 @@ class RenewalInfoIOS {
});
final String? autoRenewPreference;
+ /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ /// 12-month commitment.
+ final RenewalCommitmentInfoIOS? commitmentInfo;
/// When subscription expires due to cancellation/billing issue
/// Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
final String? expirationReason;
@@ -3265,6 +3356,8 @@ class RenewalInfoIOS {
/// User's response to subscription price increase
/// Possible values: "AGREED", "PENDING", null (no price increase)
final String? priceIncreaseStatus;
+ /// iOS 26.4+ billing plan that will renew after the current period.
+ final SubscriptionBillingPlanTypeIOS? renewalBillingPlanType;
/// Expected renewal date (milliseconds since epoch)
/// For active subscriptions, when the next renewal/charge will occur
final double? renewalDate;
@@ -3278,12 +3371,14 @@ class RenewalInfoIOS {
factory RenewalInfoIOS.fromJson(Map json) {
return RenewalInfoIOS(
autoRenewPreference: json['autoRenewPreference'] as String?,
+ commitmentInfo: json['commitmentInfo'] != null ? RenewalCommitmentInfoIOS.fromJson(json['commitmentInfo'] as Map) : null,
expirationReason: json['expirationReason'] as String?,
gracePeriodExpirationDate: (json['gracePeriodExpirationDate'] as num?)?.toDouble(),
isInBillingRetry: json['isInBillingRetry'] as bool?,
jsonRepresentation: json['jsonRepresentation'] as String?,
pendingUpgradeProductId: json['pendingUpgradeProductId'] as String?,
priceIncreaseStatus: json['priceIncreaseStatus'] as String?,
+ renewalBillingPlanType: json['renewalBillingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['renewalBillingPlanType'] as String) : null,
renewalDate: (json['renewalDate'] as num?)?.toDouble(),
renewalOfferId: json['renewalOfferId'] as String?,
renewalOfferType: json['renewalOfferType'] as String?,
@@ -3295,12 +3390,14 @@ class RenewalInfoIOS {
return {
'__typename': 'RenewalInfoIOS',
'autoRenewPreference': autoRenewPreference,
+ 'commitmentInfo': commitmentInfo?.toJson(),
'expirationReason': expirationReason,
'gracePeriodExpirationDate': gracePeriodExpirationDate,
'isInBillingRetry': isInBillingRetry,
'jsonRepresentation': jsonRepresentation,
'pendingUpgradeProductId': pendingUpgradeProductId,
'priceIncreaseStatus': priceIncreaseStatus,
+ 'renewalBillingPlanType': renewalBillingPlanType?.toJson(),
'renewalDate': renewalDate,
'renewalOfferId': renewalOfferId,
'renewalOfferType': renewalOfferType,
@@ -3384,15 +3481,46 @@ class RequestVerifyPurchaseWithIapkitResult {
}
}
+class SubscriptionCommitmentInfoIOS {
+ const SubscriptionCommitmentInfoIOS({
+ required this.displayPrice,
+ required this.period,
+ required this.price,
+ });
+
+ final String displayPrice;
+ final SubscriptionPeriodValueIOS period;
+ final double price;
+
+ factory SubscriptionCommitmentInfoIOS.fromJson(Map json) {
+ return SubscriptionCommitmentInfoIOS(
+ displayPrice: json['displayPrice'] as String,
+ period: SubscriptionPeriodValueIOS.fromJson(json['period'] as Map),
+ price: (json['price'] as num).toDouble(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'SubscriptionCommitmentInfoIOS',
+ 'displayPrice': displayPrice,
+ 'period': period.toJson(),
+ 'price': price,
+ };
+ }
+}
+
class SubscriptionInfoIOS {
const SubscriptionInfoIOS({
this.introductoryOffer,
+ this.pricingTerms,
this.promotionalOffers,
required this.subscriptionGroupId,
required this.subscriptionPeriod,
});
final SubscriptionOfferIOS? introductoryOffer;
+ final List? pricingTerms;
final List? promotionalOffers;
final String subscriptionGroupId;
final SubscriptionPeriodValueIOS subscriptionPeriod;
@@ -3400,6 +3528,7 @@ class SubscriptionInfoIOS {
factory SubscriptionInfoIOS.fromJson(Map json) {
return SubscriptionInfoIOS(
introductoryOffer: json['introductoryOffer'] != null ? SubscriptionOfferIOS.fromJson(json['introductoryOffer'] as Map) : null,
+ pricingTerms: (json['pricingTerms'] as List?) == null ? null : (json['pricingTerms'] as List?)!.map((e) => SubscriptionPricingTermsIOS.fromJson(e as Map)).toList(),
promotionalOffers: (json['promotionalOffers'] as List?) == null ? null : (json['promotionalOffers'] as List?)!.map((e) => SubscriptionOfferIOS.fromJson(e as Map)).toList(),
subscriptionGroupId: json['subscriptionGroupId'] as String,
subscriptionPeriod: SubscriptionPeriodValueIOS.fromJson(json['subscriptionPeriod'] as Map),
@@ -3410,6 +3539,7 @@ class SubscriptionInfoIOS {
return {
'__typename': 'SubscriptionInfoIOS',
'introductoryOffer': introductoryOffer?.toJson(),
+ 'pricingTerms': pricingTerms == null ? null : pricingTerms!.map((e) => e.toJson()).toList(),
'promotionalOffers': promotionalOffers == null ? null : promotionalOffers!.map((e) => e.toJson()).toList(),
'subscriptionGroupId': subscriptionGroupId,
'subscriptionPeriod': subscriptionPeriod.toJson(),
@@ -3650,6 +3780,47 @@ class SubscriptionPeriodValueIOS {
}
}
+class SubscriptionPricingTermsIOS {
+ const SubscriptionPricingTermsIOS({
+ required this.billingDisplayPrice,
+ required this.billingPeriod,
+ required this.billingPlanType,
+ required this.billingPrice,
+ required this.commitmentInfo,
+ this.subscriptionOffers,
+ });
+
+ final String billingDisplayPrice;
+ final SubscriptionPeriodValueIOS billingPeriod;
+ final SubscriptionBillingPlanTypeIOS billingPlanType;
+ final double billingPrice;
+ final SubscriptionCommitmentInfoIOS commitmentInfo;
+ final List? subscriptionOffers;
+
+ factory SubscriptionPricingTermsIOS.fromJson(Map json) {
+ return SubscriptionPricingTermsIOS(
+ billingDisplayPrice: json['billingDisplayPrice'] as String,
+ billingPeriod: SubscriptionPeriodValueIOS.fromJson(json['billingPeriod'] as Map),
+ billingPlanType: SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String),
+ billingPrice: (json['billingPrice'] as num).toDouble(),
+ commitmentInfo: SubscriptionCommitmentInfoIOS.fromJson(json['commitmentInfo'] as Map),
+ subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'SubscriptionPricingTermsIOS',
+ 'billingDisplayPrice': billingDisplayPrice,
+ 'billingPeriod': billingPeriod.toJson(),
+ 'billingPlanType': billingPlanType.toJson(),
+ 'billingPrice': billingPrice,
+ 'commitmentInfo': commitmentInfo.toJson(),
+ 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
+ };
+ }
+}
+
class SubscriptionStatusIOS {
const SubscriptionStatusIOS({
this.renewalInfo,
@@ -3675,6 +3846,39 @@ class SubscriptionStatusIOS {
}
}
+class TransactionCommitmentInfoIOS {
+ const TransactionCommitmentInfoIOS({
+ required this.billingPeriodNumber,
+ required this.commitmentExpiresDate,
+ required this.commitmentPrice,
+ required this.totalBillingPeriods,
+ });
+
+ final int billingPeriodNumber;
+ final double commitmentExpiresDate;
+ final double commitmentPrice;
+ final int totalBillingPeriods;
+
+ factory TransactionCommitmentInfoIOS.fromJson(Map json) {
+ return TransactionCommitmentInfoIOS(
+ billingPeriodNumber: json['billingPeriodNumber'] as int,
+ commitmentExpiresDate: (json['commitmentExpiresDate'] as num).toDouble(),
+ commitmentPrice: (json['commitmentPrice'] as num).toDouble(),
+ totalBillingPeriods: json['totalBillingPeriods'] as int,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'TransactionCommitmentInfoIOS',
+ 'billingPeriodNumber': billingPeriodNumber,
+ 'commitmentExpiresDate': commitmentExpiresDate,
+ 'commitmentPrice': commitmentPrice,
+ 'totalBillingPeriods': totalBillingPeriods,
+ };
+ }
+}
+
/// User Choice Billing event details (Android)
/// Fired when a user selects alternative billing in the User Choice Billing dialog
class UserChoiceBillingDetails {
@@ -4649,6 +4853,7 @@ class RequestSubscriptionIosProps {
this.advancedCommerceData,
this.andDangerouslyFinishTransactionAutomatically,
this.appAccountToken,
+ this.billingPlanType,
this.introductoryOfferEligibility,
this.promotionalOfferJWS,
this.quantity,
@@ -4664,6 +4869,9 @@ class RequestSubscriptionIosProps {
final String? advancedCommerceData;
final bool? andDangerouslyFinishTransactionAutomatically;
final String? appAccountToken;
+ /// Billing plan to use when purchasing an annual subscription that offers
+ /// monthly billing with a 12-month commitment (iOS 26.4+).
+ final SubscriptionBillingPlanTypeIOS? billingPlanType;
/// Override introductory offer eligibility (iOS 15+, WWDC 2025).
/// Set to true to indicate the user is eligible for introductory offer,
/// or false to indicate they are not. When nil, the system determines eligibility.
@@ -4689,6 +4897,7 @@ class RequestSubscriptionIosProps {
advancedCommerceData: json['advancedCommerceData'] as String?,
andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?,
appAccountToken: json['appAccountToken'] as String?,
+ billingPlanType: json['billingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String) : null,
introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?,
promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null,
quantity: json['quantity'] as int?,
@@ -4703,6 +4912,7 @@ class RequestSubscriptionIosProps {
'advancedCommerceData': advancedCommerceData,
'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically,
'appAccountToken': appAccountToken,
+ 'billingPlanType': billingPlanType?.toJson(),
'introductoryOfferEligibility': introductoryOfferEligibility,
'promotionalOfferJWS': promotionalOfferJWS?.toJson(),
'quantity': quantity,
diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd
index 76bc09e74..b471470ed 100644
--- a/libraries/godot-iap/addons/godot-iap/types.gd
+++ b/libraries/godot-iap/addons/godot-iap/types.gd
@@ -253,6 +253,15 @@ enum SubResponseCodeAndroid {
USER_INELIGIBLE = 2,
}
+enum SubscriptionBillingPlanTypeIOS {
+ ## Unknown or unsupported billing plan type.
+ UNKNOWN = 0,
+ ## Monthly billing with a 12-month commitment.
+ MONTHLY = 1,
+ ## Up-front billing for the full subscription period.
+ UP_FRONT = 2,
+}
+
enum SubscriptionOfferTypeIOS {
INTRODUCTORY = 0,
PROMOTIONAL = 1,
@@ -1632,6 +1641,8 @@ class ProductIOS:
var type_ios: ProductTypeIOS
## Standardized subscription offers.
var subscription_offers: Array[SubscriptionOffer] = []
+ ## iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ var pricing_terms_ios: Array[SubscriptionPricingTermsIOS] = []
## @deprecated Use subscriptionOffers instead for cross-platform compatibility.
var subscription_info_ios: SubscriptionInfoIOS
@@ -1685,6 +1696,14 @@ class ProductIOS:
else:
arr.append(item)
obj.subscription_offers = arr
+ if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null:
+ var arr = []
+ for item in data["pricingTermsIOS"]:
+ if item is Dictionary:
+ arr.append(SubscriptionPricingTermsIOS.from_dict(item))
+ else:
+ arr.append(item)
+ obj.pricing_terms_ios = arr
if data.has("subscriptionInfoIOS") and data["subscriptionInfoIOS"] != null:
if data["subscriptionInfoIOS"] is Dictionary:
obj.subscription_info_ios = SubscriptionInfoIOS.from_dict(data["subscriptionInfoIOS"])
@@ -1730,6 +1749,16 @@ class ProductIOS:
dict["subscriptionOffers"] = arr
else:
dict["subscriptionOffers"] = null
+ if pricing_terms_ios != null:
+ var arr = []
+ for item in pricing_terms_ios:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["pricingTermsIOS"] = arr
+ else:
+ dict["pricingTermsIOS"] = null
if subscription_info_ios != null and subscription_info_ios.has_method("to_dict"):
dict["subscriptionInfoIOS"] = subscription_info_ios.to_dict()
else:
@@ -1965,6 +1994,8 @@ class ProductSubscriptionIOS:
var type_ios: ProductTypeIOS
## Standardized subscription offers.
var subscription_offers: Array[SubscriptionOffer] = []
+ ## iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ var pricing_terms_ios: Array[SubscriptionPricingTermsIOS] = []
## App Store subscription group identifier for intro-offer eligibility checks.
var subscription_group_id_ios: Variant = null
## @deprecated Use subscriptionOffers for offer metadata and subscriptionGroupIdIOS for the App Store subscription group identifier.
@@ -2029,6 +2060,14 @@ class ProductSubscriptionIOS:
else:
arr.append(item)
obj.subscription_offers = arr
+ if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null:
+ var arr = []
+ for item in data["pricingTermsIOS"]:
+ if item is Dictionary:
+ arr.append(SubscriptionPricingTermsIOS.from_dict(item))
+ else:
+ arr.append(item)
+ obj.pricing_terms_ios = arr
if data.has("subscriptionGroupIdIOS") and data["subscriptionGroupIdIOS"] != null:
obj.subscription_group_id_ios = data["subscriptionGroupIdIOS"]
if data.has("subscriptionInfoIOS") and data["subscriptionInfoIOS"] != null:
@@ -2110,6 +2149,16 @@ class ProductSubscriptionIOS:
dict["subscriptionOffers"] = arr
else:
dict["subscriptionOffers"] = null
+ if pricing_terms_ios != null:
+ var arr = []
+ for item in pricing_terms_ios:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["pricingTermsIOS"] = arr
+ else:
+ dict["pricingTermsIOS"] = null
if subscription_group_id_ios != null:
dict["subscriptionGroupIdIOS"] = subscription_group_id_ios
if subscription_info_ios != null and subscription_info_ios.has_method("to_dict"):
@@ -2378,6 +2427,10 @@ class PurchaseIOS:
var currency_symbol_ios: Variant = null
var country_code_ios: Variant = null
var renewal_info_ios: RenewalInfoIOS
+ ## iOS 26.4+ billing plan selected for this transaction.
+ var billing_plan_type_ios: SubscriptionBillingPlanTypeIOS
+ ## iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ var commitment_info_ios: TransactionCommitmentInfoIOS
## Advanced Commerce API metadata (iOS 18.4+).
var advanced_commerce_info_ios: AdvancedCommerceInfoIOS
@@ -2469,6 +2522,17 @@ class PurchaseIOS:
obj.renewal_info_ios = RenewalInfoIOS.from_dict(data["renewalInfoIOS"])
else:
obj.renewal_info_ios = data["renewalInfoIOS"]
+ if data.has("billingPlanTypeIOS") and data["billingPlanTypeIOS"] != null:
+ var enum_str = data["billingPlanTypeIOS"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.billing_plan_type_ios = enum_str
+ if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null:
+ if data["commitmentInfoIOS"] is Dictionary:
+ obj.commitment_info_ios = TransactionCommitmentInfoIOS.from_dict(data["commitmentInfoIOS"])
+ else:
+ obj.commitment_info_ios = data["commitmentInfoIOS"]
if data.has("advancedCommerceInfoIOS") and data["advancedCommerceInfoIOS"] != null:
if data["advancedCommerceInfoIOS"] is Dictionary:
obj.advanced_commerce_info_ios = AdvancedCommerceInfoIOS.from_dict(data["advancedCommerceInfoIOS"])
@@ -2549,6 +2613,14 @@ class PurchaseIOS:
dict["renewalInfoIOS"] = renewal_info_ios.to_dict()
else:
dict["renewalInfoIOS"] = renewal_info_ios
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(billing_plan_type_ios):
+ dict["billingPlanTypeIOS"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type_ios]
+ else:
+ dict["billingPlanTypeIOS"] = billing_plan_type_ios
+ if commitment_info_ios != null and commitment_info_ios.has_method("to_dict"):
+ dict["commitmentInfoIOS"] = commitment_info_ios.to_dict()
+ else:
+ dict["commitmentInfoIOS"] = commitment_info_ios
if advanced_commerce_info_ios != null and advanced_commerce_info_ios.has_method("to_dict"):
dict["advancedCommerceInfoIOS"] = advanced_commerce_info_ios.to_dict()
else:
@@ -2596,6 +2668,43 @@ class RefundResultIOS:
dict["message"] = message
return dict
+class RenewalCommitmentInfoIOS:
+ var commitment_auto_renew_product_id: String = ""
+ var commitment_auto_renew_status: bool = false
+ var commitment_renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS
+ var commitment_renewal_date: float = 0.0
+ var commitment_renewal_price: float = 0.0
+
+ static func from_dict(data: Dictionary) -> RenewalCommitmentInfoIOS:
+ var obj = RenewalCommitmentInfoIOS.new()
+ if data.has("commitmentAutoRenewProductId") and data["commitmentAutoRenewProductId"] != null:
+ obj.commitment_auto_renew_product_id = data["commitmentAutoRenewProductId"]
+ if data.has("commitmentAutoRenewStatus") and data["commitmentAutoRenewStatus"] != null:
+ obj.commitment_auto_renew_status = data["commitmentAutoRenewStatus"]
+ if data.has("commitmentRenewalBillingPlanType") and data["commitmentRenewalBillingPlanType"] != null:
+ var enum_str = data["commitmentRenewalBillingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.commitment_renewal_billing_plan_type = enum_str
+ if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null:
+ obj.commitment_renewal_date = data["commitmentRenewalDate"]
+ if data.has("commitmentRenewalPrice") and data["commitmentRenewalPrice"] != null:
+ obj.commitment_renewal_price = data["commitmentRenewalPrice"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["commitmentAutoRenewProductId"] = commitment_auto_renew_product_id
+ dict["commitmentAutoRenewStatus"] = commitment_auto_renew_status
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(commitment_renewal_billing_plan_type):
+ dict["commitmentRenewalBillingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[commitment_renewal_billing_plan_type]
+ else:
+ dict["commitmentRenewalBillingPlanType"] = commitment_renewal_billing_plan_type
+ dict["commitmentRenewalDate"] = commitment_renewal_date
+ dict["commitmentRenewalPrice"] = commitment_renewal_price
+ return dict
+
## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
class RenewalInfoIOS:
var json_representation: Variant = null
@@ -2617,6 +2726,10 @@ class RenewalInfoIOS:
var renewal_offer_id: Variant = null
## Type of offer applied to next renewal
var renewal_offer_type: Variant = null
+ ## iOS 26.4+ billing plan that will renew after the current period.
+ var renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS
+ ## iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ var commitment_info: RenewalCommitmentInfoIOS
static func from_dict(data: Dictionary) -> RenewalInfoIOS:
var obj = RenewalInfoIOS.new()
@@ -2642,6 +2755,17 @@ class RenewalInfoIOS:
obj.renewal_offer_id = data["renewalOfferId"]
if data.has("renewalOfferType") and data["renewalOfferType"] != null:
obj.renewal_offer_type = data["renewalOfferType"]
+ if data.has("renewalBillingPlanType") and data["renewalBillingPlanType"] != null:
+ var enum_str = data["renewalBillingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.renewal_billing_plan_type = enum_str
+ if data.has("commitmentInfo") and data["commitmentInfo"] != null:
+ if data["commitmentInfo"] is Dictionary:
+ obj.commitment_info = RenewalCommitmentInfoIOS.from_dict(data["commitmentInfo"])
+ else:
+ obj.commitment_info = data["commitmentInfo"]
return obj
func to_dict() -> Dictionary:
@@ -2667,6 +2791,14 @@ class RenewalInfoIOS:
dict["renewalOfferId"] = renewal_offer_id
if renewal_offer_type != null:
dict["renewalOfferType"] = renewal_offer_type
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(renewal_billing_plan_type):
+ dict["renewalBillingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[renewal_billing_plan_type]
+ else:
+ dict["renewalBillingPlanType"] = renewal_billing_plan_type
+ if commitment_info != null and commitment_info.has_method("to_dict"):
+ dict["commitmentInfo"] = commitment_info.to_dict()
+ else:
+ dict["commitmentInfo"] = commitment_info
return dict
## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+
@@ -2729,8 +2861,37 @@ class RequestVerifyPurchaseWithIapkitResult:
dict["state"] = state
return dict
+class SubscriptionCommitmentInfoIOS:
+ var display_price: String = ""
+ var period: SubscriptionPeriodValueIOS
+ var price: float = 0.0
+
+ static func from_dict(data: Dictionary) -> SubscriptionCommitmentInfoIOS:
+ var obj = SubscriptionCommitmentInfoIOS.new()
+ if data.has("displayPrice") and data["displayPrice"] != null:
+ obj.display_price = data["displayPrice"]
+ 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("price") and data["price"] != null:
+ obj.price = data["price"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["displayPrice"] = display_price
+ if period != null and period.has_method("to_dict"):
+ dict["period"] = period.to_dict()
+ else:
+ dict["period"] = period
+ dict["price"] = price
+ return dict
+
class SubscriptionInfoIOS:
var introductory_offer: SubscriptionOfferIOS
+ var pricing_terms: Array[SubscriptionPricingTermsIOS] = []
var promotional_offers: Array[SubscriptionOfferIOS] = []
var subscription_group_id: String = ""
var subscription_period: SubscriptionPeriodValueIOS
@@ -2742,6 +2903,14 @@ class SubscriptionInfoIOS:
obj.introductory_offer = SubscriptionOfferIOS.from_dict(data["introductoryOffer"])
else:
obj.introductory_offer = data["introductoryOffer"]
+ if data.has("pricingTerms") and data["pricingTerms"] != null:
+ var arr = []
+ for item in data["pricingTerms"]:
+ if item is Dictionary:
+ arr.append(SubscriptionPricingTermsIOS.from_dict(item))
+ else:
+ arr.append(item)
+ obj.pricing_terms = arr
if data.has("promotionalOffers") and data["promotionalOffers"] != null:
var arr = []
for item in data["promotionalOffers"]:
@@ -2765,6 +2934,16 @@ class SubscriptionInfoIOS:
dict["introductoryOffer"] = introductory_offer.to_dict()
else:
dict["introductoryOffer"] = introductory_offer
+ if pricing_terms != null:
+ var arr = []
+ for item in pricing_terms:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["pricingTerms"] = arr
+ else:
+ dict["pricingTerms"] = null
if promotional_offers != null:
var arr = []
for item in promotional_offers:
@@ -3042,6 +3221,74 @@ class SubscriptionPeriodValueIOS:
dict["value"] = value
return dict
+class SubscriptionPricingTermsIOS:
+ var billing_display_price: String = ""
+ var billing_period: SubscriptionPeriodValueIOS
+ var billing_plan_type: SubscriptionBillingPlanTypeIOS
+ var billing_price: float = 0.0
+ var commitment_info: SubscriptionCommitmentInfoIOS
+ var subscription_offers: Array[SubscriptionOffer] = []
+
+ static func from_dict(data: Dictionary) -> SubscriptionPricingTermsIOS:
+ var obj = SubscriptionPricingTermsIOS.new()
+ if data.has("billingDisplayPrice") and data["billingDisplayPrice"] != null:
+ obj.billing_display_price = data["billingDisplayPrice"]
+ if data.has("billingPeriod") and data["billingPeriod"] != null:
+ if data["billingPeriod"] is Dictionary:
+ obj.billing_period = SubscriptionPeriodValueIOS.from_dict(data["billingPeriod"])
+ else:
+ obj.billing_period = data["billingPeriod"]
+ if data.has("billingPlanType") and data["billingPlanType"] != null:
+ var enum_str = data["billingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.billing_plan_type = enum_str
+ if data.has("billingPrice") and data["billingPrice"] != null:
+ obj.billing_price = data["billingPrice"]
+ if data.has("commitmentInfo") and data["commitmentInfo"] != null:
+ if data["commitmentInfo"] is Dictionary:
+ obj.commitment_info = SubscriptionCommitmentInfoIOS.from_dict(data["commitmentInfo"])
+ else:
+ obj.commitment_info = data["commitmentInfo"]
+ if data.has("subscriptionOffers") and data["subscriptionOffers"] != null:
+ var arr = []
+ for item in data["subscriptionOffers"]:
+ if item is Dictionary:
+ arr.append(SubscriptionOffer.from_dict(item))
+ else:
+ arr.append(item)
+ obj.subscription_offers = arr
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["billingDisplayPrice"] = billing_display_price
+ if billing_period != null and billing_period.has_method("to_dict"):
+ dict["billingPeriod"] = billing_period.to_dict()
+ else:
+ dict["billingPeriod"] = billing_period
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(billing_plan_type):
+ dict["billingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type]
+ else:
+ dict["billingPlanType"] = billing_plan_type
+ dict["billingPrice"] = billing_price
+ if commitment_info != null and commitment_info.has_method("to_dict"):
+ dict["commitmentInfo"] = commitment_info.to_dict()
+ else:
+ dict["commitmentInfo"] = commitment_info
+ if subscription_offers != null:
+ var arr = []
+ for item in subscription_offers:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["subscriptionOffers"] = arr
+ else:
+ dict["subscriptionOffers"] = null
+ return dict
+
class SubscriptionStatusIOS:
var state: String = ""
var renewal_info: RenewalInfoIOS
@@ -3066,6 +3313,32 @@ class SubscriptionStatusIOS:
dict["renewalInfo"] = renewal_info
return dict
+class TransactionCommitmentInfoIOS:
+ var billing_period_number: int = 0
+ var commitment_expires_date: float = 0.0
+ var commitment_price: float = 0.0
+ var total_billing_periods: int = 0
+
+ static func from_dict(data: Dictionary) -> TransactionCommitmentInfoIOS:
+ var obj = TransactionCommitmentInfoIOS.new()
+ if data.has("billingPeriodNumber") and data["billingPeriodNumber"] != null:
+ obj.billing_period_number = data["billingPeriodNumber"]
+ if data.has("commitmentExpiresDate") and data["commitmentExpiresDate"] != null:
+ obj.commitment_expires_date = data["commitmentExpiresDate"]
+ if data.has("commitmentPrice") and data["commitmentPrice"] != null:
+ obj.commitment_price = data["commitmentPrice"]
+ if data.has("totalBillingPeriods") and data["totalBillingPeriods"] != null:
+ obj.total_billing_periods = data["totalBillingPeriods"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["billingPeriodNumber"] = billing_period_number
+ dict["commitmentExpiresDate"] = commitment_expires_date
+ dict["commitmentPrice"] = commitment_price
+ dict["totalBillingPeriods"] = total_billing_periods
+ return dict
+
## User Choice Billing event details (Android) Fired when a user selects alternative billing in the User Choice Billing dialog
class UserChoiceBillingDetails:
## Token that must be reported to Google Play within 24 hours
@@ -4187,6 +4460,8 @@ class RequestSubscriptionIosProps:
var win_back_offer: WinBackOfferInputIOS
## JWS promotional offer (iOS 15+, WWDC 2025).
var promotional_offer_jws: PromotionalOfferJWSInputIOS
+ ## Billing plan to use when purchasing an annual subscription that offers
+ var billing_plan_type: SubscriptionBillingPlanTypeIOS
## Override introductory offer eligibility (iOS 15+, WWDC 2025).
var introductory_offer_eligibility: Variant = null
## Advanced commerce data token (iOS 15+).
@@ -4217,6 +4492,12 @@ class RequestSubscriptionIosProps:
obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"])
else:
obj.promotional_offer_jws = data["promotionalOfferJWS"]
+ if data.has("billingPlanType") and data["billingPlanType"] != null:
+ var enum_str = data["billingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.billing_plan_type = enum_str
if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
obj.introductory_offer_eligibility = data["introductoryOfferEligibility"]
if data.has("advancedCommerceData") and data["advancedCommerceData"] != null:
@@ -4248,6 +4529,11 @@ class RequestSubscriptionIosProps:
dict["promotionalOfferJWS"] = promotional_offer_jws.to_dict()
else:
dict["promotionalOfferJWS"] = promotional_offer_jws
+ if billing_plan_type != null:
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(billing_plan_type):
+ dict["billingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type]
+ else:
+ dict["billingPlanType"] = billing_plan_type
if introductory_offer_eligibility != null:
dict["introductoryOfferEligibility"] = introductory_offer_eligibility
if advanced_commerce_data != null:
@@ -4786,6 +5072,12 @@ const SUB_RESPONSE_CODE_ANDROID_VALUES = {
SubResponseCodeAndroid.USER_INELIGIBLE: "user-ineligible"
}
+const SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES = {
+ SubscriptionBillingPlanTypeIOS.UNKNOWN: "unknown",
+ SubscriptionBillingPlanTypeIOS.MONTHLY: "monthly",
+ SubscriptionBillingPlanTypeIOS.UP_FRONT: "up-front"
+}
+
const SUBSCRIPTION_OFFER_TYPE_IOS_VALUES = {
SubscriptionOfferTypeIOS.INTRODUCTORY: "introductory",
SubscriptionOfferTypeIOS.PROMOTIONAL: "promotional",
@@ -5055,6 +5347,12 @@ const SUB_RESPONSE_CODE_ANDROID_FROM_STRING = {
"user-ineligible": SubResponseCodeAndroid.USER_INELIGIBLE
}
+const SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING = {
+ "unknown": SubscriptionBillingPlanTypeIOS.UNKNOWN,
+ "monthly": SubscriptionBillingPlanTypeIOS.MONTHLY,
+ "up-front": SubscriptionBillingPlanTypeIOS.UP_FRONT
+}
+
const SUBSCRIPTION_OFFER_TYPE_IOS_FROM_STRING = {
"introductory": SubscriptionOfferTypeIOS.INTRODUCTORY,
"promotional": SubscriptionOfferTypeIOS.PROMOTIONAL,
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 6e4ffda48..e4073ca0b 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
@@ -903,6 +903,38 @@ public enum class SubResponseCodeAndroid(val rawValue: String) {
fun toJson(): String = rawValue
}
+public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) {
+ /**
+ * Unknown or unsupported billing plan type.
+ */
+ Unknown("unknown"),
+ /**
+ * Monthly billing with a 12-month commitment.
+ */
+ Monthly("monthly"),
+ /**
+ * Up-front billing for the full subscription period.
+ */
+ UpFront("up-front");
+
+ companion object {
+ fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = when (value) {
+ "unknown" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "UNKNOWN" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "Unknown" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "monthly" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "MONTHLY" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "Monthly" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront
+ "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront
+ "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront
+ else -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
Introductory("introductory"),
Promotional("promotional"),
@@ -2685,6 +2717,11 @@ public data class ProductIOS(
val jsonRepresentationIOS: String,
override val platform: IapPlatform = IapPlatform.Ios,
override val price: Double? = null,
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ val pricingTermsIOS: List? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -2715,6 +2752,7 @@ public data class ProductIOS(
jsonRepresentationIOS = json["jsonRepresentationIOS"] as? String ?: "",
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = (json["pricingTermsIOS"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
subscriptionInfoIOS = (json["subscriptionInfoIOS"] as? Map)?.let { SubscriptionInfoIOS.fromJson(it) },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
title = json["title"] as? String ?: "",
@@ -2737,6 +2775,7 @@ public data class ProductIOS(
"jsonRepresentationIOS" to jsonRepresentationIOS,
"platform" to platform.toJson(),
"price" to price,
+ "pricingTermsIOS" to pricingTermsIOS?.map { it.toJson() },
"subscriptionInfoIOS" to subscriptionInfoIOS?.toJson(),
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
"title" to title,
@@ -2897,6 +2936,11 @@ public data class ProductSubscriptionIOS(
val jsonRepresentationIOS: String,
override val platform: IapPlatform = IapPlatform.Ios,
override val price: Double? = null,
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ val pricingTermsIOS: List? = null,
/**
* App Store subscription group identifier for intro-offer eligibility checks.
*/
@@ -2938,6 +2982,7 @@ public data class ProductSubscriptionIOS(
jsonRepresentationIOS = json["jsonRepresentationIOS"] as? String ?: "",
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = (json["pricingTermsIOS"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
subscriptionGroupIdIOS = json["subscriptionGroupIdIOS"] as? String,
subscriptionInfoIOS = (json["subscriptionInfoIOS"] as? Map)?.let { SubscriptionInfoIOS.fromJson(it) },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
@@ -2969,6 +3014,7 @@ public data class ProductSubscriptionIOS(
"jsonRepresentationIOS" to jsonRepresentationIOS,
"platform" to platform.toJson(),
"price" to price,
+ "pricingTermsIOS" to pricingTermsIOS?.map { it.toJson() },
"subscriptionGroupIdIOS" to subscriptionGroupIdIOS,
"subscriptionInfoIOS" to subscriptionInfoIOS?.toJson(),
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
@@ -3125,6 +3171,14 @@ public data class PurchaseIOS(
val advancedCommerceInfoIOS: AdvancedCommerceInfoIOS? = null,
val appAccountToken: String? = null,
val appBundleIdIOS: String? = null,
+ /**
+ * iOS 26.4+ billing plan selected for this transaction.
+ */
+ val billingPlanTypeIOS: SubscriptionBillingPlanTypeIOS? = null,
+ /**
+ * iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ */
+ val commitmentInfoIOS: TransactionCommitmentInfoIOS? = null,
val countryCodeIOS: String? = null,
val currencyCodeIOS: String? = null,
val currencySymbolIOS: String? = null,
@@ -3168,6 +3222,8 @@ public data class PurchaseIOS(
advancedCommerceInfoIOS = (json["advancedCommerceInfoIOS"] as? Map)?.let { AdvancedCommerceInfoIOS.fromJson(it) },
appAccountToken = json["appAccountToken"] as? String,
appBundleIdIOS = json["appBundleIdIOS"] as? String,
+ billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) },
+ commitmentInfoIOS = (json["commitmentInfoIOS"] as? Map)?.let { TransactionCommitmentInfoIOS.fromJson(it) },
countryCodeIOS = json["countryCodeIOS"] as? String,
currencyCodeIOS = json["currencyCodeIOS"] as? String,
currencySymbolIOS = json["currencySymbolIOS"] as? String,
@@ -3209,6 +3265,8 @@ public data class PurchaseIOS(
"advancedCommerceInfoIOS" to advancedCommerceInfoIOS?.toJson(),
"appAccountToken" to appAccountToken,
"appBundleIdIOS" to appBundleIdIOS,
+ "billingPlanTypeIOS" to billingPlanTypeIOS?.toJson(),
+ "commitmentInfoIOS" to commitmentInfoIOS?.toJson(),
"countryCodeIOS" to countryCodeIOS,
"currencyCodeIOS" to currencyCodeIOS,
"currencySymbolIOS" to currencySymbolIOS,
@@ -3289,12 +3347,47 @@ public data class RefundResultIOS(
)
}
+public data class RenewalCommitmentInfoIOS(
+ val commitmentAutoRenewProductId: String,
+ val commitmentAutoRenewStatus: Boolean,
+ val commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS,
+ val commitmentRenewalDate: Double,
+ val commitmentRenewalPrice: Double
+) {
+
+ companion object {
+ fun fromJson(json: Map): RenewalCommitmentInfoIOS {
+ return RenewalCommitmentInfoIOS(
+ commitmentAutoRenewProductId = json["commitmentAutoRenewProductId"] as? String ?: "",
+ commitmentAutoRenewStatus = json["commitmentAutoRenewStatus"] as? Boolean ?: false,
+ commitmentRenewalBillingPlanType = (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown,
+ commitmentRenewalDate = (json["commitmentRenewalDate"] as? Number)?.toDouble() ?: 0.0,
+ commitmentRenewalPrice = (json["commitmentRenewalPrice"] as? Number)?.toDouble() ?: 0.0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "RenewalCommitmentInfoIOS",
+ "commitmentAutoRenewProductId" to commitmentAutoRenewProductId,
+ "commitmentAutoRenewStatus" to commitmentAutoRenewStatus,
+ "commitmentRenewalBillingPlanType" to commitmentRenewalBillingPlanType.toJson(),
+ "commitmentRenewalDate" to commitmentRenewalDate,
+ "commitmentRenewalPrice" to commitmentRenewalPrice,
+ )
+}
+
/**
* Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
* https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
*/
public data class RenewalInfoIOS(
val autoRenewPreference: String? = null,
+ /**
+ * iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ * 12-month commitment.
+ */
+ val commitmentInfo: RenewalCommitmentInfoIOS? = null,
/**
* When subscription expires due to cancellation/billing issue
* Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
@@ -3321,6 +3414,10 @@ public data class RenewalInfoIOS(
* Possible values: "AGREED", "PENDING", null (no price increase)
*/
val priceIncreaseStatus: String? = null,
+ /**
+ * iOS 26.4+ billing plan that will renew after the current period.
+ */
+ val renewalBillingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
* Expected renewal date (milliseconds since epoch)
* For active subscriptions, when the next renewal/charge will occur
@@ -3342,12 +3439,14 @@ public data class RenewalInfoIOS(
fun fromJson(json: Map): RenewalInfoIOS {
return RenewalInfoIOS(
autoRenewPreference = json["autoRenewPreference"] as? String,
+ commitmentInfo = (json["commitmentInfo"] as? Map)?.let { RenewalCommitmentInfoIOS.fromJson(it) },
expirationReason = json["expirationReason"] as? String,
gracePeriodExpirationDate = (json["gracePeriodExpirationDate"] as? Number)?.toDouble(),
isInBillingRetry = json["isInBillingRetry"] as? Boolean,
jsonRepresentation = json["jsonRepresentation"] as? String,
pendingUpgradeProductId = json["pendingUpgradeProductId"] as? String,
priceIncreaseStatus = json["priceIncreaseStatus"] as? String,
+ renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) },
renewalDate = (json["renewalDate"] as? Number)?.toDouble(),
renewalOfferId = json["renewalOfferId"] as? String,
renewalOfferType = json["renewalOfferType"] as? String,
@@ -3359,12 +3458,14 @@ public data class RenewalInfoIOS(
fun toJson(): Map = mapOf(
"__typename" to "RenewalInfoIOS",
"autoRenewPreference" to autoRenewPreference,
+ "commitmentInfo" to commitmentInfo?.toJson(),
"expirationReason" to expirationReason,
"gracePeriodExpirationDate" to gracePeriodExpirationDate,
"isInBillingRetry" to isInBillingRetry,
"jsonRepresentation" to jsonRepresentation,
"pendingUpgradeProductId" to pendingUpgradeProductId,
"priceIncreaseStatus" to priceIncreaseStatus,
+ "renewalBillingPlanType" to renewalBillingPlanType?.toJson(),
"renewalDate" to renewalDate,
"renewalOfferId" to renewalOfferId,
"renewalOfferType" to renewalOfferType,
@@ -3440,8 +3541,33 @@ public data class RequestVerifyPurchaseWithIapkitResult(
)
}
+public data class SubscriptionCommitmentInfoIOS(
+ val displayPrice: String,
+ val period: SubscriptionPeriodValueIOS,
+ val price: Double
+) {
+
+ companion object {
+ fun fromJson(json: Map): SubscriptionCommitmentInfoIOS {
+ return SubscriptionCommitmentInfoIOS(
+ displayPrice = json["displayPrice"] as? String ?: "",
+ period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
+ price = (json["price"] as? Number)?.toDouble() ?: 0.0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "SubscriptionCommitmentInfoIOS",
+ "displayPrice" to displayPrice,
+ "period" to period.toJson(),
+ "price" to price,
+ )
+}
+
public data class SubscriptionInfoIOS(
val introductoryOffer: SubscriptionOfferIOS? = null,
+ val pricingTerms: List? = null,
val promotionalOffers: List? = null,
val subscriptionGroupId: String,
val subscriptionPeriod: SubscriptionPeriodValueIOS
@@ -3451,6 +3577,7 @@ public data class SubscriptionInfoIOS(
fun fromJson(json: Map): SubscriptionInfoIOS {
return SubscriptionInfoIOS(
introductoryOffer = (json["introductoryOffer"] as? Map)?.let { SubscriptionOfferIOS.fromJson(it) },
+ pricingTerms = (json["pricingTerms"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
promotionalOffers = (json["promotionalOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOfferIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOfferIOS") },
subscriptionGroupId = json["subscriptionGroupId"] as? String ?: "",
subscriptionPeriod = (json["subscriptionPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
@@ -3461,6 +3588,7 @@ public data class SubscriptionInfoIOS(
fun toJson(): Map = mapOf(
"__typename" to "SubscriptionInfoIOS",
"introductoryOffer" to introductoryOffer?.toJson(),
+ "pricingTerms" to pricingTerms?.map { it.toJson() },
"promotionalOffers" to promotionalOffers?.map { it.toJson() },
"subscriptionGroupId" to subscriptionGroupId,
"subscriptionPeriod" to subscriptionPeriod.toJson(),
@@ -3710,6 +3838,39 @@ public data class SubscriptionPeriodValueIOS(
)
}
+public data class SubscriptionPricingTermsIOS(
+ val billingDisplayPrice: String,
+ val billingPeriod: SubscriptionPeriodValueIOS,
+ val billingPlanType: SubscriptionBillingPlanTypeIOS,
+ val billingPrice: Double,
+ val commitmentInfo: SubscriptionCommitmentInfoIOS,
+ val subscriptionOffers: List? = null
+) {
+
+ companion object {
+ fun fromJson(json: Map): SubscriptionPricingTermsIOS {
+ return SubscriptionPricingTermsIOS(
+ billingDisplayPrice = json["billingDisplayPrice"] as? String ?: "",
+ billingPeriod = (json["billingPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
+ billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown,
+ billingPrice = (json["billingPrice"] as? Number)?.toDouble() ?: 0.0,
+ commitmentInfo = (json["commitmentInfo"] as? Map)?.let { SubscriptionCommitmentInfoIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionCommitmentInfoIOS"),
+ subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "SubscriptionPricingTermsIOS",
+ "billingDisplayPrice" to billingDisplayPrice,
+ "billingPeriod" to billingPeriod.toJson(),
+ "billingPlanType" to billingPlanType.toJson(),
+ "billingPrice" to billingPrice,
+ "commitmentInfo" to commitmentInfo.toJson(),
+ "subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
+ )
+}
+
public data class SubscriptionStatusIOS(
val renewalInfo: RenewalInfoIOS? = null,
val state: String
@@ -3731,6 +3892,33 @@ public data class SubscriptionStatusIOS(
)
}
+public data class TransactionCommitmentInfoIOS(
+ val billingPeriodNumber: Int,
+ val commitmentExpiresDate: Double,
+ val commitmentPrice: Double,
+ val totalBillingPeriods: Int
+) {
+
+ companion object {
+ fun fromJson(json: Map): TransactionCommitmentInfoIOS {
+ return TransactionCommitmentInfoIOS(
+ billingPeriodNumber = (json["billingPeriodNumber"] as? Number)?.toInt() ?: 0,
+ commitmentExpiresDate = (json["commitmentExpiresDate"] as? Number)?.toDouble() ?: 0.0,
+ commitmentPrice = (json["commitmentPrice"] as? Number)?.toDouble() ?: 0.0,
+ totalBillingPeriods = (json["totalBillingPeriods"] as? Number)?.toInt() ?: 0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "TransactionCommitmentInfoIOS",
+ "billingPeriodNumber" to billingPeriodNumber,
+ "commitmentExpiresDate" to commitmentExpiresDate,
+ "commitmentPrice" to commitmentPrice,
+ "totalBillingPeriods" to totalBillingPeriods,
+ )
+}
+
/**
* User Choice Billing event details (Android)
* Fired when a user selects alternative billing in the User Choice Billing dialog
@@ -4753,6 +4941,11 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData: String? = null,
val andDangerouslyFinishTransactionAutomatically: Boolean? = null,
val appAccountToken: String? = null,
+ /**
+ * Billing plan to use when purchasing an annual subscription that offers
+ * monthly billing with a 12-month commitment (iOS 26.4+).
+ */
+ val billingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -4786,6 +4979,7 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData = json["advancedCommerceData"] as? String
val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean
val appAccountToken = json["appAccountToken"] as? String
+ val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }
val introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean
val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }
val quantity = (json["quantity"] as? Number)?.toInt()
@@ -4797,6 +4991,7 @@ public data class RequestSubscriptionIosProps(
advancedCommerceData = advancedCommerceData,
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
appAccountToken = appAccountToken,
+ billingPlanType = billingPlanType,
introductoryOfferEligibility = introductoryOfferEligibility,
promotionalOfferJWS = promotionalOfferJWS,
quantity = quantity,
@@ -4811,6 +5006,7 @@ public data class RequestSubscriptionIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "billingPlanType" to billingPlanType?.toJson(),
"introductoryOfferEligibility" to introductoryOfferEligibility,
"promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs
index c35b1bd38..339cb9ecf 100644
--- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs
+++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs
@@ -1524,6 +1524,62 @@ public static class SubResponseCodeAndroidExtensions
public static SubResponseCodeAndroid FromJson(string value) => SubResponseCodeAndroidJsonConverter.FromRawString(value);
}
+[JsonConverter(typeof(SubscriptionBillingPlanTypeIOSJsonConverter))]
+public enum SubscriptionBillingPlanTypeIOS
+{
+ /// Unknown or unsupported billing plan type.
+ Unknown,
+ /// Monthly billing with a 12-month commitment.
+ Monthly,
+ /// Up-front billing for the full subscription period.
+ UpFront
+}
+
+public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter
+{
+ private static readonly Dictionary _fromString = new()
+ {
+ ["unknown"] = SubscriptionBillingPlanTypeIOS.Unknown,
+ ["UNKNOWN"] = SubscriptionBillingPlanTypeIOS.Unknown,
+ ["Unknown"] = SubscriptionBillingPlanTypeIOS.Unknown,
+ ["monthly"] = SubscriptionBillingPlanTypeIOS.Monthly,
+ ["MONTHLY"] = SubscriptionBillingPlanTypeIOS.Monthly,
+ ["Monthly"] = SubscriptionBillingPlanTypeIOS.Monthly,
+ ["up-front"] = SubscriptionBillingPlanTypeIOS.UpFront,
+ ["UP_FRONT"] = SubscriptionBillingPlanTypeIOS.UpFront,
+ ["UpFront"] = SubscriptionBillingPlanTypeIOS.UpFront,
+ };
+
+ private static readonly Dictionary _toString = new()
+ {
+ [SubscriptionBillingPlanTypeIOS.Unknown] = "unknown",
+ [SubscriptionBillingPlanTypeIOS.Monthly] = "monthly",
+ [SubscriptionBillingPlanTypeIOS.UpFront] = "up-front",
+ };
+
+ public override SubscriptionBillingPlanTypeIOS Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var raw = reader.GetString();
+ if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value;
+ throw new JsonException($"Unknown SubscriptionBillingPlanTypeIOS value: {raw}");
+ }
+
+ public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(_toString[value]);
+ }
+
+ internal static string ToRawString(SubscriptionBillingPlanTypeIOS value) => _toString[value];
+ internal static SubscriptionBillingPlanTypeIOS FromRawString(string value) =>
+ _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionBillingPlanTypeIOS value: {value}");
+}
+
+public static class SubscriptionBillingPlanTypeIOSExtensions
+{
+ public static string ToJson(this SubscriptionBillingPlanTypeIOS value) => SubscriptionBillingPlanTypeIOSJsonConverter.ToRawString(value);
+ public static SubscriptionBillingPlanTypeIOS FromJson(string value) => SubscriptionBillingPlanTypeIOSJsonConverter.FromRawString(value);
+}
+
[JsonConverter(typeof(SubscriptionOfferTypeIOSJsonConverter))]
public enum SubscriptionOfferTypeIOS
{
@@ -2910,6 +2966,10 @@ public sealed record ProductIOS : Product, ProductCommon
public IapPlatform Platform { get; init; } = IapPlatform.IOS;
[JsonPropertyName("price")]
public double? Price { get; init; }
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ [JsonPropertyName("pricingTermsIOS")]
+ public IReadOnlyList? PricingTermsIOS { get; init; }
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
[JsonPropertyName("subscriptionInfoIOS")]
public SubscriptionInfoIOS? SubscriptionInfoIOS { get; init; }
@@ -3037,6 +3097,10 @@ public sealed record ProductSubscriptionIOS : ProductSubscription, ProductCommon
public IapPlatform Platform { get; init; } = IapPlatform.IOS;
[JsonPropertyName("price")]
public double? Price { get; init; }
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ [JsonPropertyName("pricingTermsIOS")]
+ public IReadOnlyList? PricingTermsIOS { get; init; }
/// App Store subscription group identifier for intro-offer eligibility checks.
[JsonPropertyName("subscriptionGroupIdIOS")]
public string? SubscriptionGroupIdIOS { get; init; }
@@ -3149,6 +3213,12 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon
public string? AppAccountToken { get; init; }
[JsonPropertyName("appBundleIdIOS")]
public string? AppBundleIdIOS { get; init; }
+ /// iOS 26.4+ billing plan selected for this transaction.
+ [JsonPropertyName("billingPlanTypeIOS")]
+ public SubscriptionBillingPlanTypeIOS? BillingPlanTypeIOS { get; init; }
+ /// iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ [JsonPropertyName("commitmentInfoIOS")]
+ public TransactionCommitmentInfoIOS? CommitmentInfoIOS { get; init; }
[JsonPropertyName("countryCodeIOS")]
public string? CountryCodeIOS { get; init; }
[JsonPropertyName("currencyCodeIOS")]
@@ -3234,12 +3304,30 @@ public sealed record RefundResultIOS
public required string Status { get; init; }
}
+public sealed record RenewalCommitmentInfoIOS
+{
+ [JsonPropertyName("commitmentAutoRenewProductId")]
+ public required string CommitmentAutoRenewProductId { get; init; }
+ [JsonPropertyName("commitmentAutoRenewStatus")]
+ public required bool CommitmentAutoRenewStatus { get; init; }
+ [JsonPropertyName("commitmentRenewalBillingPlanType")]
+ public required SubscriptionBillingPlanTypeIOS CommitmentRenewalBillingPlanType { get; init; }
+ [JsonPropertyName("commitmentRenewalDate")]
+ public required double CommitmentRenewalDate { get; init; }
+ [JsonPropertyName("commitmentRenewalPrice")]
+ public required double CommitmentRenewalPrice { get; init; }
+}
+
/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
public sealed record RenewalInfoIOS
{
[JsonPropertyName("autoRenewPreference")]
public string? AutoRenewPreference { get; init; }
+ /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ /// 12-month commitment.
+ [JsonPropertyName("commitmentInfo")]
+ public RenewalCommitmentInfoIOS? CommitmentInfo { get; init; }
/// When subscription expires due to cancellation/billing issue
/// Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
[JsonPropertyName("expirationReason")]
@@ -3262,6 +3350,9 @@ public sealed record RenewalInfoIOS
/// Possible values: "AGREED", "PENDING", null (no price increase)
[JsonPropertyName("priceIncreaseStatus")]
public string? PriceIncreaseStatus { get; init; }
+ /// iOS 26.4+ billing plan that will renew after the current period.
+ [JsonPropertyName("renewalBillingPlanType")]
+ public SubscriptionBillingPlanTypeIOS? RenewalBillingPlanType { get; init; }
/// Expected renewal date (milliseconds since epoch)
/// For active subscriptions, when the next renewal/charge will occur
[JsonPropertyName("renewalDate")]
@@ -3308,10 +3399,22 @@ public sealed record RequestVerifyPurchaseWithIapkitResult
public required IapStore Store { get; init; }
}
+public sealed record SubscriptionCommitmentInfoIOS
+{
+ [JsonPropertyName("displayPrice")]
+ public required string DisplayPrice { get; init; }
+ [JsonPropertyName("period")]
+ public required SubscriptionPeriodValueIOS Period { get; init; }
+ [JsonPropertyName("price")]
+ public required double Price { get; init; }
+}
+
public sealed record SubscriptionInfoIOS
{
[JsonPropertyName("introductoryOffer")]
public SubscriptionOfferIOS? IntroductoryOffer { get; init; }
+ [JsonPropertyName("pricingTerms")]
+ public IReadOnlyList? PricingTerms { get; init; }
[JsonPropertyName("promotionalOffers")]
public IReadOnlyList? PromotionalOffers { get; init; }
[JsonPropertyName("subscriptionGroupId")]
@@ -3441,6 +3544,22 @@ public sealed record SubscriptionPeriodValueIOS
public required int Value { get; init; }
}
+public sealed record SubscriptionPricingTermsIOS
+{
+ [JsonPropertyName("billingDisplayPrice")]
+ public required string BillingDisplayPrice { get; init; }
+ [JsonPropertyName("billingPeriod")]
+ public required SubscriptionPeriodValueIOS BillingPeriod { get; init; }
+ [JsonPropertyName("billingPlanType")]
+ public required SubscriptionBillingPlanTypeIOS BillingPlanType { get; init; }
+ [JsonPropertyName("billingPrice")]
+ public required double BillingPrice { get; init; }
+ [JsonPropertyName("commitmentInfo")]
+ public required SubscriptionCommitmentInfoIOS CommitmentInfo { get; init; }
+ [JsonPropertyName("subscriptionOffers")]
+ public IReadOnlyList? SubscriptionOffers { get; init; }
+}
+
public sealed record SubscriptionStatusIOS
{
[JsonPropertyName("renewalInfo")]
@@ -3449,6 +3568,18 @@ public sealed record SubscriptionStatusIOS
public required string State { get; init; }
}
+public sealed record TransactionCommitmentInfoIOS
+{
+ [JsonPropertyName("billingPeriodNumber")]
+ public required int BillingPeriodNumber { get; init; }
+ [JsonPropertyName("commitmentExpiresDate")]
+ public required double CommitmentExpiresDate { get; init; }
+ [JsonPropertyName("commitmentPrice")]
+ public required double CommitmentPrice { get; init; }
+ [JsonPropertyName("totalBillingPeriods")]
+ public required int TotalBillingPeriods { get; init; }
+}
+
/// User Choice Billing event details (Android)
/// Fired when a user selects alternative billing in the User Choice Billing dialog
public sealed record UserChoiceBillingDetails
@@ -3925,6 +4056,10 @@ public sealed record RequestSubscriptionIosProps
/// Back-deployed to iOS 15.
[JsonPropertyName("promotionalOfferJWS")]
public PromotionalOfferJWSInputIOS? PromotionalOfferJws { get; init; }
+ /// Billing plan to use when purchasing an annual subscription that offers
+ /// monthly billing with a 12-month commitment (iOS 26.4+).
+ [JsonPropertyName("billingPlanType")]
+ public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; }
/// Override introductory offer eligibility (iOS 15+, WWDC 2025).
/// Set to true to indicate the user is eligible for introductory offer,
/// or false to indicate they are not. When nil, the system determines eligibility.
diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts
index 518f5ffde..e62ef2b05 100644
--- a/libraries/react-native-iap/src/types.ts
+++ b/libraries/react-native-iap/src/types.ts
@@ -985,6 +985,11 @@ export interface ProductIOS extends ProductCommon {
jsonRepresentationIOS: string;
platform: 'ios';
price?: (number | null);
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ pricingTermsIOS?: (SubscriptionPricingTermsIOS[] | null);
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
* @deprecated Use subscriptionOffers instead
@@ -1108,6 +1113,11 @@ export interface ProductSubscriptionIOS extends ProductCommon {
jsonRepresentationIOS: string;
platform: 'ios';
price?: (number | null);
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ pricingTermsIOS?: (SubscriptionPricingTermsIOS[] | null);
/** App Store subscription group identifier for intro-offer eligibility checks. */
subscriptionGroupIdIOS?: (string | null);
/**
@@ -1234,6 +1244,10 @@ export interface PurchaseIOS extends PurchaseCommon {
advancedCommerceInfoIOS?: (AdvancedCommerceInfoIOS | null);
appAccountToken?: (string | null);
appBundleIdIOS?: (string | null);
+ /** iOS 26.4+ billing plan selected for this transaction. */
+ billingPlanTypeIOS?: (SubscriptionBillingPlanTypeIOS | null);
+ /** iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment. */
+ commitmentInfoIOS?: (TransactionCommitmentInfoIOS | null);
countryCodeIOS?: (string | null);
currencyCodeIOS?: (string | null);
currencySymbolIOS?: (string | null);
@@ -1454,12 +1468,25 @@ export interface RefundResultIOS {
status: string;
}
+export interface RenewalCommitmentInfoIOS {
+ commitmentAutoRenewProductId: string;
+ commitmentAutoRenewStatus: boolean;
+ commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS;
+ commitmentRenewalDate: number;
+ commitmentRenewalPrice: number;
+}
+
/**
* Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
* https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
*/
export interface RenewalInfoIOS {
autoRenewPreference?: (string | null);
+ /**
+ * iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ * 12-month commitment.
+ */
+ commitmentInfo?: (RenewalCommitmentInfoIOS | null);
/**
* When subscription expires due to cancellation/billing issue
* Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
@@ -1486,6 +1513,8 @@ export interface RenewalInfoIOS {
* Possible values: "AGREED", "PENDING", null (no price increase)
*/
priceIncreaseStatus?: (string | null);
+ /** iOS 26.4+ billing plan that will renew after the current period. */
+ renewalBillingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
* Expected renewal date (milliseconds since epoch)
* For active subscriptions, when the next renewal/charge will occur
@@ -1645,6 +1674,11 @@ export interface RequestSubscriptionIosProps {
advancedCommerceData?: (string | null);
andDangerouslyFinishTransactionAutomatically?: (boolean | null);
appAccountToken?: (string | null);
+ /**
+ * Billing plan to use when purchasing an annual subscription that offers
+ * monthly billing with a 12-month commitment (iOS 26.4+).
+ */
+ billingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -1777,8 +1811,17 @@ export interface Subscription {
export type SubscriptionPurchaseUpdatedArgs = (PurchaseUpdatedListenerOptions | null) | undefined;
+export type SubscriptionBillingPlanTypeIOS = 'unknown' | 'monthly' | 'up-front';
+
+export interface SubscriptionCommitmentInfoIOS {
+ displayPrice: string;
+ period: SubscriptionPeriodValueIOS;
+ price: number;
+}
+
export interface SubscriptionInfoIOS {
introductoryOffer?: (SubscriptionOfferIOS | null);
+ pricingTerms?: (SubscriptionPricingTermsIOS[] | null);
promotionalOffers?: (SubscriptionOfferIOS[] | null);
subscriptionGroupId: string;
subscriptionPeriod: SubscriptionPeriodValueIOS;
@@ -1900,6 +1943,15 @@ export interface SubscriptionPeriodValueIOS {
value: number;
}
+export interface SubscriptionPricingTermsIOS {
+ billingDisplayPrice: string;
+ billingPeriod: SubscriptionPeriodValueIOS;
+ billingPlanType: SubscriptionBillingPlanTypeIOS;
+ billingPrice: number;
+ commitmentInfo: SubscriptionCommitmentInfoIOS;
+ subscriptionOffers?: (SubscriptionOffer[] | null);
+}
+
/**
* Product-level subscription replacement parameters (Android)
* Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
@@ -1926,6 +1978,13 @@ export interface SubscriptionStatusIOS {
state: string;
}
+export interface TransactionCommitmentInfoIOS {
+ billingPeriodNumber: number;
+ commitmentExpiresDate: number;
+ commitmentPrice: number;
+ totalBillingPeriods: number;
+}
+
/**
* User Choice Billing event details (Android)
* Fired when a user selects alternative billing in the User Choice Billing dialog
diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift
index 0199cfc74..95327c44e 100644
--- a/packages/apple/Sources/Models/Types.swift
+++ b/packages/apple/Sources/Models/Types.swift
@@ -379,6 +379,15 @@ public enum SubResponseCodeAndroid: String, Codable, CaseIterable {
case userIneligible = "user-ineligible"
}
+public enum SubscriptionBillingPlanTypeIOS: String, Codable, CaseIterable {
+ /// Unknown or unsupported billing plan type.
+ case unknown = "unknown"
+ /// Monthly billing with a 12-month commitment.
+ case monthly = "monthly"
+ /// Up-front billing for the full subscription period.
+ case upFront = "up-front"
+}
+
public enum SubscriptionOfferTypeIOS: String, Codable, CaseIterable {
case introductory = "introductory"
case promotional = "promotional"
@@ -1012,6 +1021,9 @@ public struct ProductIOS: Codable, ProductCommon {
public var jsonRepresentationIOS: String
public var platform: IapPlatform = .ios
public var price: Double? = nil
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ public var pricingTermsIOS: [SubscriptionPricingTermsIOS]? = nil
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
public var subscriptionInfoIOS: SubscriptionInfoIOS? = nil
/// Standardized subscription offers.
@@ -1092,6 +1104,9 @@ public struct ProductSubscriptionIOS: Codable, ProductCommon {
public var jsonRepresentationIOS: String
public var platform: IapPlatform = .ios
public var price: Double? = nil
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ public var pricingTermsIOS: [SubscriptionPricingTermsIOS]? = nil
/// App Store subscription group identifier for intro-offer eligibility checks.
public var subscriptionGroupIdIOS: String? = nil
/// @deprecated Use subscriptionOffers for offer metadata and subscriptionGroupIdIOS for the App Store subscription group identifier.
@@ -1160,6 +1175,10 @@ public struct PurchaseIOS: Codable, PurchaseCommon {
public var advancedCommerceInfoIOS: AdvancedCommerceInfoIOS? = nil
public var appAccountToken: String? = nil
public var appBundleIdIOS: String? = nil
+ /// iOS 26.4+ billing plan selected for this transaction.
+ public var billingPlanTypeIOS: SubscriptionBillingPlanTypeIOS? = nil
+ /// iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ public var commitmentInfoIOS: TransactionCommitmentInfoIOS? = nil
public var countryCodeIOS: String? = nil
public var currencyCodeIOS: String? = nil
public var currencySymbolIOS: String? = nil
@@ -1206,10 +1225,21 @@ public struct RefundResultIOS: Codable {
public var status: String
}
+public struct RenewalCommitmentInfoIOS: Codable {
+ public var commitmentAutoRenewProductId: String
+ public var commitmentAutoRenewStatus: Bool
+ public var commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS
+ public var commitmentRenewalDate: Double
+ public var commitmentRenewalPrice: Double
+}
+
/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
public struct RenewalInfoIOS: Codable {
public var autoRenewPreference: String? = nil
+ /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ /// 12-month commitment.
+ public var commitmentInfo: RenewalCommitmentInfoIOS? = nil
/// When subscription expires due to cancellation/billing issue
/// Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
public var expirationReason: String? = nil
@@ -1226,6 +1256,8 @@ public struct RenewalInfoIOS: Codable {
/// User's response to subscription price increase
/// Possible values: "AGREED", "PENDING", null (no price increase)
public var priceIncreaseStatus: String? = nil
+ /// iOS 26.4+ billing plan that will renew after the current period.
+ public var renewalBillingPlanType: SubscriptionBillingPlanTypeIOS? = nil
/// Expected renewal date (milliseconds since epoch)
/// For active subscriptions, when the next renewal/charge will occur
public var renewalDate: Double? = nil
@@ -1260,8 +1292,15 @@ public struct RequestVerifyPurchaseWithIapkitResult: Codable {
public var store: IapStore
}
+public struct SubscriptionCommitmentInfoIOS: Codable {
+ public var displayPrice: String
+ public var period: SubscriptionPeriodValueIOS
+ public var price: Double
+}
+
public struct SubscriptionInfoIOS: Codable {
public var introductoryOffer: SubscriptionOfferIOS? = nil
+ public var pricingTerms: [SubscriptionPricingTermsIOS]? = nil
public var promotionalOffers: [SubscriptionOfferIOS]? = nil
public var subscriptionGroupId: String
public var subscriptionPeriod: SubscriptionPeriodValueIOS
@@ -1354,11 +1393,27 @@ public struct SubscriptionPeriodValueIOS: Codable {
public var value: Int
}
+public struct SubscriptionPricingTermsIOS: Codable {
+ public var billingDisplayPrice: String
+ public var billingPeriod: SubscriptionPeriodValueIOS
+ public var billingPlanType: SubscriptionBillingPlanTypeIOS
+ public var billingPrice: Double
+ public var commitmentInfo: SubscriptionCommitmentInfoIOS
+ public var subscriptionOffers: [SubscriptionOffer]? = nil
+}
+
public struct SubscriptionStatusIOS: Codable {
public var renewalInfo: RenewalInfoIOS? = nil
public var state: String
}
+public struct TransactionCommitmentInfoIOS: Codable {
+ public var billingPeriodNumber: Int
+ public var commitmentExpiresDate: Double
+ public var commitmentPrice: Double
+ public var totalBillingPeriods: Int
+}
+
/// User Choice Billing event details (Android)
/// Fired when a user selects alternative billing in the User Choice Billing dialog
public struct UserChoiceBillingDetails: Codable {
@@ -1928,6 +1983,9 @@ public struct RequestSubscriptionIosProps: Codable {
public var advancedCommerceData: String?
public var andDangerouslyFinishTransactionAutomatically: Bool?
public var appAccountToken: String?
+ /// Billing plan to use when purchasing an annual subscription that offers
+ /// monthly billing with a 12-month commitment (iOS 26.4+).
+ public var billingPlanType: SubscriptionBillingPlanTypeIOS?
/// Override introductory offer eligibility (iOS 15+, WWDC 2025).
/// Set to true to indicate the user is eligible for introductory offer,
/// or false to indicate they are not. When nil, the system determines eligibility.
@@ -1952,6 +2010,7 @@ public struct RequestSubscriptionIosProps: Codable {
advancedCommerceData: String? = nil,
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
+ billingPlanType: SubscriptionBillingPlanTypeIOS? = nil,
introductoryOfferEligibility: Bool? = nil,
promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
@@ -1962,6 +2021,7 @@ public struct RequestSubscriptionIosProps: Codable {
self.advancedCommerceData = advancedCommerceData
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
+ self.billingPlanType = billingPlanType
self.introductoryOfferEligibility = introductoryOfferEligibility
self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
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 c1674700f..b4d5d28fd 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
@@ -825,6 +825,35 @@ public enum class SubResponseCodeAndroid(val rawValue: String) {
fun toJson(): String = rawValue
}
+public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) {
+ /**
+ * Unknown or unsupported billing plan type.
+ */
+ Unknown("unknown"),
+ /**
+ * Monthly billing with a 12-month commitment.
+ */
+ Monthly("monthly"),
+ /**
+ * Up-front billing for the full subscription period.
+ */
+ UpFront("up-front");
+
+ companion object {
+ fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = when (value) {
+ "unknown" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "Unknown" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "monthly" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "Monthly" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront
+ "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront
+ else -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
Introductory("introductory"),
Promotional("promotional"),
@@ -2564,6 +2593,11 @@ public data class ProductIOS(
val jsonRepresentationIOS: String,
override val platform: IapPlatform = IapPlatform.Ios,
override val price: Double? = null,
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ val pricingTermsIOS: List? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -2594,6 +2628,7 @@ public data class ProductIOS(
jsonRepresentationIOS = json["jsonRepresentationIOS"] as? String ?: "",
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = (json["pricingTermsIOS"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
subscriptionInfoIOS = (json["subscriptionInfoIOS"] as? Map)?.let { SubscriptionInfoIOS.fromJson(it) },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
title = json["title"] as? String ?: "",
@@ -2616,6 +2651,7 @@ public data class ProductIOS(
"jsonRepresentationIOS" to jsonRepresentationIOS,
"platform" to platform.toJson(),
"price" to price,
+ "pricingTermsIOS" to pricingTermsIOS?.map { it.toJson() },
"subscriptionInfoIOS" to subscriptionInfoIOS?.toJson(),
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
"title" to title,
@@ -2776,6 +2812,11 @@ public data class ProductSubscriptionIOS(
val jsonRepresentationIOS: String,
override val platform: IapPlatform = IapPlatform.Ios,
override val price: Double? = null,
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ val pricingTermsIOS: List? = null,
/**
* App Store subscription group identifier for intro-offer eligibility checks.
*/
@@ -2817,6 +2858,7 @@ public data class ProductSubscriptionIOS(
jsonRepresentationIOS = json["jsonRepresentationIOS"] as? String ?: "",
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = (json["pricingTermsIOS"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
subscriptionGroupIdIOS = json["subscriptionGroupIdIOS"] as? String,
subscriptionInfoIOS = (json["subscriptionInfoIOS"] as? Map)?.let { SubscriptionInfoIOS.fromJson(it) },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
@@ -2848,6 +2890,7 @@ public data class ProductSubscriptionIOS(
"jsonRepresentationIOS" to jsonRepresentationIOS,
"platform" to platform.toJson(),
"price" to price,
+ "pricingTermsIOS" to pricingTermsIOS?.map { it.toJson() },
"subscriptionGroupIdIOS" to subscriptionGroupIdIOS,
"subscriptionInfoIOS" to subscriptionInfoIOS?.toJson(),
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
@@ -3004,6 +3047,14 @@ public data class PurchaseIOS(
val advancedCommerceInfoIOS: AdvancedCommerceInfoIOS? = null,
val appAccountToken: String? = null,
val appBundleIdIOS: String? = null,
+ /**
+ * iOS 26.4+ billing plan selected for this transaction.
+ */
+ val billingPlanTypeIOS: SubscriptionBillingPlanTypeIOS? = null,
+ /**
+ * iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ */
+ val commitmentInfoIOS: TransactionCommitmentInfoIOS? = null,
val countryCodeIOS: String? = null,
val currencyCodeIOS: String? = null,
val currencySymbolIOS: String? = null,
@@ -3047,6 +3098,8 @@ public data class PurchaseIOS(
advancedCommerceInfoIOS = (json["advancedCommerceInfoIOS"] as? Map)?.let { AdvancedCommerceInfoIOS.fromJson(it) },
appAccountToken = json["appAccountToken"] as? String,
appBundleIdIOS = json["appBundleIdIOS"] as? String,
+ billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) },
+ commitmentInfoIOS = (json["commitmentInfoIOS"] as? Map)?.let { TransactionCommitmentInfoIOS.fromJson(it) },
countryCodeIOS = json["countryCodeIOS"] as? String,
currencyCodeIOS = json["currencyCodeIOS"] as? String,
currencySymbolIOS = json["currencySymbolIOS"] as? String,
@@ -3088,6 +3141,8 @@ public data class PurchaseIOS(
"advancedCommerceInfoIOS" to advancedCommerceInfoIOS?.toJson(),
"appAccountToken" to appAccountToken,
"appBundleIdIOS" to appBundleIdIOS,
+ "billingPlanTypeIOS" to billingPlanTypeIOS?.toJson(),
+ "commitmentInfoIOS" to commitmentInfoIOS?.toJson(),
"countryCodeIOS" to countryCodeIOS,
"currencyCodeIOS" to currencyCodeIOS,
"currencySymbolIOS" to currencySymbolIOS,
@@ -3168,12 +3223,47 @@ public data class RefundResultIOS(
)
}
+public data class RenewalCommitmentInfoIOS(
+ val commitmentAutoRenewProductId: String,
+ val commitmentAutoRenewStatus: Boolean,
+ val commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS,
+ val commitmentRenewalDate: Double,
+ val commitmentRenewalPrice: Double
+) {
+
+ companion object {
+ fun fromJson(json: Map): RenewalCommitmentInfoIOS {
+ return RenewalCommitmentInfoIOS(
+ commitmentAutoRenewProductId = json["commitmentAutoRenewProductId"] as? String ?: "",
+ commitmentAutoRenewStatus = json["commitmentAutoRenewStatus"] as? Boolean ?: false,
+ commitmentRenewalBillingPlanType = (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown,
+ commitmentRenewalDate = (json["commitmentRenewalDate"] as? Number)?.toDouble() ?: 0.0,
+ commitmentRenewalPrice = (json["commitmentRenewalPrice"] as? Number)?.toDouble() ?: 0.0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "RenewalCommitmentInfoIOS",
+ "commitmentAutoRenewProductId" to commitmentAutoRenewProductId,
+ "commitmentAutoRenewStatus" to commitmentAutoRenewStatus,
+ "commitmentRenewalBillingPlanType" to commitmentRenewalBillingPlanType.toJson(),
+ "commitmentRenewalDate" to commitmentRenewalDate,
+ "commitmentRenewalPrice" to commitmentRenewalPrice,
+ )
+}
+
/**
* Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
* https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
*/
public data class RenewalInfoIOS(
val autoRenewPreference: String? = null,
+ /**
+ * iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ * 12-month commitment.
+ */
+ val commitmentInfo: RenewalCommitmentInfoIOS? = null,
/**
* When subscription expires due to cancellation/billing issue
* Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
@@ -3200,6 +3290,10 @@ public data class RenewalInfoIOS(
* Possible values: "AGREED", "PENDING", null (no price increase)
*/
val priceIncreaseStatus: String? = null,
+ /**
+ * iOS 26.4+ billing plan that will renew after the current period.
+ */
+ val renewalBillingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
* Expected renewal date (milliseconds since epoch)
* For active subscriptions, when the next renewal/charge will occur
@@ -3221,12 +3315,14 @@ public data class RenewalInfoIOS(
fun fromJson(json: Map): RenewalInfoIOS {
return RenewalInfoIOS(
autoRenewPreference = json["autoRenewPreference"] as? String,
+ commitmentInfo = (json["commitmentInfo"] as? Map)?.let { RenewalCommitmentInfoIOS.fromJson(it) },
expirationReason = json["expirationReason"] as? String,
gracePeriodExpirationDate = (json["gracePeriodExpirationDate"] as? Number)?.toDouble(),
isInBillingRetry = json["isInBillingRetry"] as? Boolean,
jsonRepresentation = json["jsonRepresentation"] as? String,
pendingUpgradeProductId = json["pendingUpgradeProductId"] as? String,
priceIncreaseStatus = json["priceIncreaseStatus"] as? String,
+ renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) },
renewalDate = (json["renewalDate"] as? Number)?.toDouble(),
renewalOfferId = json["renewalOfferId"] as? String,
renewalOfferType = json["renewalOfferType"] as? String,
@@ -3238,12 +3334,14 @@ public data class RenewalInfoIOS(
fun toJson(): Map = mapOf(
"__typename" to "RenewalInfoIOS",
"autoRenewPreference" to autoRenewPreference,
+ "commitmentInfo" to commitmentInfo?.toJson(),
"expirationReason" to expirationReason,
"gracePeriodExpirationDate" to gracePeriodExpirationDate,
"isInBillingRetry" to isInBillingRetry,
"jsonRepresentation" to jsonRepresentation,
"pendingUpgradeProductId" to pendingUpgradeProductId,
"priceIncreaseStatus" to priceIncreaseStatus,
+ "renewalBillingPlanType" to renewalBillingPlanType?.toJson(),
"renewalDate" to renewalDate,
"renewalOfferId" to renewalOfferId,
"renewalOfferType" to renewalOfferType,
@@ -3319,8 +3417,33 @@ public data class RequestVerifyPurchaseWithIapkitResult(
)
}
+public data class SubscriptionCommitmentInfoIOS(
+ val displayPrice: String,
+ val period: SubscriptionPeriodValueIOS,
+ val price: Double
+) {
+
+ companion object {
+ fun fromJson(json: Map): SubscriptionCommitmentInfoIOS {
+ return SubscriptionCommitmentInfoIOS(
+ displayPrice = json["displayPrice"] as? String ?: "",
+ period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
+ price = (json["price"] as? Number)?.toDouble() ?: 0.0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "SubscriptionCommitmentInfoIOS",
+ "displayPrice" to displayPrice,
+ "period" to period.toJson(),
+ "price" to price,
+ )
+}
+
public data class SubscriptionInfoIOS(
val introductoryOffer: SubscriptionOfferIOS? = null,
+ val pricingTerms: List? = null,
val promotionalOffers: List? = null,
val subscriptionGroupId: String,
val subscriptionPeriod: SubscriptionPeriodValueIOS
@@ -3330,6 +3453,7 @@ public data class SubscriptionInfoIOS(
fun fromJson(json: Map): SubscriptionInfoIOS {
return SubscriptionInfoIOS(
introductoryOffer = (json["introductoryOffer"] as? Map)?.let { SubscriptionOfferIOS.fromJson(it) },
+ pricingTerms = (json["pricingTerms"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
promotionalOffers = (json["promotionalOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOfferIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOfferIOS") },
subscriptionGroupId = json["subscriptionGroupId"] as? String ?: "",
subscriptionPeriod = (json["subscriptionPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
@@ -3340,6 +3464,7 @@ public data class SubscriptionInfoIOS(
fun toJson(): Map = mapOf(
"__typename" to "SubscriptionInfoIOS",
"introductoryOffer" to introductoryOffer?.toJson(),
+ "pricingTerms" to pricingTerms?.map { it.toJson() },
"promotionalOffers" to promotionalOffers?.map { it.toJson() },
"subscriptionGroupId" to subscriptionGroupId,
"subscriptionPeriod" to subscriptionPeriod.toJson(),
@@ -3589,6 +3714,39 @@ public data class SubscriptionPeriodValueIOS(
)
}
+public data class SubscriptionPricingTermsIOS(
+ val billingDisplayPrice: String,
+ val billingPeriod: SubscriptionPeriodValueIOS,
+ val billingPlanType: SubscriptionBillingPlanTypeIOS,
+ val billingPrice: Double,
+ val commitmentInfo: SubscriptionCommitmentInfoIOS,
+ val subscriptionOffers: List? = null
+) {
+
+ companion object {
+ fun fromJson(json: Map): SubscriptionPricingTermsIOS {
+ return SubscriptionPricingTermsIOS(
+ billingDisplayPrice = json["billingDisplayPrice"] as? String ?: "",
+ billingPeriod = (json["billingPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
+ billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown,
+ billingPrice = (json["billingPrice"] as? Number)?.toDouble() ?: 0.0,
+ commitmentInfo = (json["commitmentInfo"] as? Map)?.let { SubscriptionCommitmentInfoIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionCommitmentInfoIOS"),
+ subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "SubscriptionPricingTermsIOS",
+ "billingDisplayPrice" to billingDisplayPrice,
+ "billingPeriod" to billingPeriod.toJson(),
+ "billingPlanType" to billingPlanType.toJson(),
+ "billingPrice" to billingPrice,
+ "commitmentInfo" to commitmentInfo.toJson(),
+ "subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
+ )
+}
+
public data class SubscriptionStatusIOS(
val renewalInfo: RenewalInfoIOS? = null,
val state: String
@@ -3610,6 +3768,33 @@ public data class SubscriptionStatusIOS(
)
}
+public data class TransactionCommitmentInfoIOS(
+ val billingPeriodNumber: Int,
+ val commitmentExpiresDate: Double,
+ val commitmentPrice: Double,
+ val totalBillingPeriods: Int
+) {
+
+ companion object {
+ fun fromJson(json: Map): TransactionCommitmentInfoIOS {
+ return TransactionCommitmentInfoIOS(
+ billingPeriodNumber = (json["billingPeriodNumber"] as? Number)?.toInt() ?: 0,
+ commitmentExpiresDate = (json["commitmentExpiresDate"] as? Number)?.toDouble() ?: 0.0,
+ commitmentPrice = (json["commitmentPrice"] as? Number)?.toDouble() ?: 0.0,
+ totalBillingPeriods = (json["totalBillingPeriods"] as? Number)?.toInt() ?: 0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "TransactionCommitmentInfoIOS",
+ "billingPeriodNumber" to billingPeriodNumber,
+ "commitmentExpiresDate" to commitmentExpiresDate,
+ "commitmentPrice" to commitmentPrice,
+ "totalBillingPeriods" to totalBillingPeriods,
+ )
+}
+
/**
* User Choice Billing event details (Android)
* Fired when a user selects alternative billing in the User Choice Billing dialog
@@ -4632,6 +4817,11 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData: String? = null,
val andDangerouslyFinishTransactionAutomatically: Boolean? = null,
val appAccountToken: String? = null,
+ /**
+ * Billing plan to use when purchasing an annual subscription that offers
+ * monthly billing with a 12-month commitment (iOS 26.4+).
+ */
+ val billingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -4665,6 +4855,7 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData = json["advancedCommerceData"] as? String
val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean
val appAccountToken = json["appAccountToken"] as? String
+ val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }
val introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean
val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }
val quantity = (json["quantity"] as? Number)?.toInt()
@@ -4676,6 +4867,7 @@ public data class RequestSubscriptionIosProps(
advancedCommerceData = advancedCommerceData,
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
appAccountToken = appAccountToken,
+ billingPlanType = billingPlanType,
introductoryOfferEligibility = introductoryOfferEligibility,
promotionalOfferJWS = promotionalOfferJWS,
quantity = quantity,
@@ -4690,6 +4882,7 @@ public data class RequestSubscriptionIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "billingPlanType" to billingPlanType?.toJson(),
"introductoryOfferEligibility" to introductoryOfferEligibility,
"promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs
index c35b1bd38..339cb9ecf 100644
--- a/packages/gql/src/generated/Types.cs
+++ b/packages/gql/src/generated/Types.cs
@@ -1524,6 +1524,62 @@ public static class SubResponseCodeAndroidExtensions
public static SubResponseCodeAndroid FromJson(string value) => SubResponseCodeAndroidJsonConverter.FromRawString(value);
}
+[JsonConverter(typeof(SubscriptionBillingPlanTypeIOSJsonConverter))]
+public enum SubscriptionBillingPlanTypeIOS
+{
+ /// Unknown or unsupported billing plan type.
+ Unknown,
+ /// Monthly billing with a 12-month commitment.
+ Monthly,
+ /// Up-front billing for the full subscription period.
+ UpFront
+}
+
+public sealed class SubscriptionBillingPlanTypeIOSJsonConverter : JsonConverter
+{
+ private static readonly Dictionary _fromString = new()
+ {
+ ["unknown"] = SubscriptionBillingPlanTypeIOS.Unknown,
+ ["UNKNOWN"] = SubscriptionBillingPlanTypeIOS.Unknown,
+ ["Unknown"] = SubscriptionBillingPlanTypeIOS.Unknown,
+ ["monthly"] = SubscriptionBillingPlanTypeIOS.Monthly,
+ ["MONTHLY"] = SubscriptionBillingPlanTypeIOS.Monthly,
+ ["Monthly"] = SubscriptionBillingPlanTypeIOS.Monthly,
+ ["up-front"] = SubscriptionBillingPlanTypeIOS.UpFront,
+ ["UP_FRONT"] = SubscriptionBillingPlanTypeIOS.UpFront,
+ ["UpFront"] = SubscriptionBillingPlanTypeIOS.UpFront,
+ };
+
+ private static readonly Dictionary _toString = new()
+ {
+ [SubscriptionBillingPlanTypeIOS.Unknown] = "unknown",
+ [SubscriptionBillingPlanTypeIOS.Monthly] = "monthly",
+ [SubscriptionBillingPlanTypeIOS.UpFront] = "up-front",
+ };
+
+ public override SubscriptionBillingPlanTypeIOS Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var raw = reader.GetString();
+ if (raw is not null && _fromString.TryGetValue(raw, out var value)) return value;
+ throw new JsonException($"Unknown SubscriptionBillingPlanTypeIOS value: {raw}");
+ }
+
+ public override void Write(Utf8JsonWriter writer, SubscriptionBillingPlanTypeIOS value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(_toString[value]);
+ }
+
+ internal static string ToRawString(SubscriptionBillingPlanTypeIOS value) => _toString[value];
+ internal static SubscriptionBillingPlanTypeIOS FromRawString(string value) =>
+ _fromString.TryGetValue(value, out var v) ? v : throw new ArgumentException($"Unknown SubscriptionBillingPlanTypeIOS value: {value}");
+}
+
+public static class SubscriptionBillingPlanTypeIOSExtensions
+{
+ public static string ToJson(this SubscriptionBillingPlanTypeIOS value) => SubscriptionBillingPlanTypeIOSJsonConverter.ToRawString(value);
+ public static SubscriptionBillingPlanTypeIOS FromJson(string value) => SubscriptionBillingPlanTypeIOSJsonConverter.FromRawString(value);
+}
+
[JsonConverter(typeof(SubscriptionOfferTypeIOSJsonConverter))]
public enum SubscriptionOfferTypeIOS
{
@@ -2910,6 +2966,10 @@ public sealed record ProductIOS : Product, ProductCommon
public IapPlatform Platform { get; init; } = IapPlatform.IOS;
[JsonPropertyName("price")]
public double? Price { get; init; }
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ [JsonPropertyName("pricingTermsIOS")]
+ public IReadOnlyList? PricingTermsIOS { get; init; }
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
[JsonPropertyName("subscriptionInfoIOS")]
public SubscriptionInfoIOS? SubscriptionInfoIOS { get; init; }
@@ -3037,6 +3097,10 @@ public sealed record ProductSubscriptionIOS : ProductSubscription, ProductCommon
public IapPlatform Platform { get; init; } = IapPlatform.IOS;
[JsonPropertyName("price")]
public double? Price { get; init; }
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ [JsonPropertyName("pricingTermsIOS")]
+ public IReadOnlyList? PricingTermsIOS { get; init; }
/// App Store subscription group identifier for intro-offer eligibility checks.
[JsonPropertyName("subscriptionGroupIdIOS")]
public string? SubscriptionGroupIdIOS { get; init; }
@@ -3149,6 +3213,12 @@ public sealed record PurchaseIOS : Purchase, PurchaseCommon
public string? AppAccountToken { get; init; }
[JsonPropertyName("appBundleIdIOS")]
public string? AppBundleIdIOS { get; init; }
+ /// iOS 26.4+ billing plan selected for this transaction.
+ [JsonPropertyName("billingPlanTypeIOS")]
+ public SubscriptionBillingPlanTypeIOS? BillingPlanTypeIOS { get; init; }
+ /// iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ [JsonPropertyName("commitmentInfoIOS")]
+ public TransactionCommitmentInfoIOS? CommitmentInfoIOS { get; init; }
[JsonPropertyName("countryCodeIOS")]
public string? CountryCodeIOS { get; init; }
[JsonPropertyName("currencyCodeIOS")]
@@ -3234,12 +3304,30 @@ public sealed record RefundResultIOS
public required string Status { get; init; }
}
+public sealed record RenewalCommitmentInfoIOS
+{
+ [JsonPropertyName("commitmentAutoRenewProductId")]
+ public required string CommitmentAutoRenewProductId { get; init; }
+ [JsonPropertyName("commitmentAutoRenewStatus")]
+ public required bool CommitmentAutoRenewStatus { get; init; }
+ [JsonPropertyName("commitmentRenewalBillingPlanType")]
+ public required SubscriptionBillingPlanTypeIOS CommitmentRenewalBillingPlanType { get; init; }
+ [JsonPropertyName("commitmentRenewalDate")]
+ public required double CommitmentRenewalDate { get; init; }
+ [JsonPropertyName("commitmentRenewalPrice")]
+ public required double CommitmentRenewalPrice { get; init; }
+}
+
/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
public sealed record RenewalInfoIOS
{
[JsonPropertyName("autoRenewPreference")]
public string? AutoRenewPreference { get; init; }
+ /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ /// 12-month commitment.
+ [JsonPropertyName("commitmentInfo")]
+ public RenewalCommitmentInfoIOS? CommitmentInfo { get; init; }
/// When subscription expires due to cancellation/billing issue
/// Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
[JsonPropertyName("expirationReason")]
@@ -3262,6 +3350,9 @@ public sealed record RenewalInfoIOS
/// Possible values: "AGREED", "PENDING", null (no price increase)
[JsonPropertyName("priceIncreaseStatus")]
public string? PriceIncreaseStatus { get; init; }
+ /// iOS 26.4+ billing plan that will renew after the current period.
+ [JsonPropertyName("renewalBillingPlanType")]
+ public SubscriptionBillingPlanTypeIOS? RenewalBillingPlanType { get; init; }
/// Expected renewal date (milliseconds since epoch)
/// For active subscriptions, when the next renewal/charge will occur
[JsonPropertyName("renewalDate")]
@@ -3308,10 +3399,22 @@ public sealed record RequestVerifyPurchaseWithIapkitResult
public required IapStore Store { get; init; }
}
+public sealed record SubscriptionCommitmentInfoIOS
+{
+ [JsonPropertyName("displayPrice")]
+ public required string DisplayPrice { get; init; }
+ [JsonPropertyName("period")]
+ public required SubscriptionPeriodValueIOS Period { get; init; }
+ [JsonPropertyName("price")]
+ public required double Price { get; init; }
+}
+
public sealed record SubscriptionInfoIOS
{
[JsonPropertyName("introductoryOffer")]
public SubscriptionOfferIOS? IntroductoryOffer { get; init; }
+ [JsonPropertyName("pricingTerms")]
+ public IReadOnlyList? PricingTerms { get; init; }
[JsonPropertyName("promotionalOffers")]
public IReadOnlyList? PromotionalOffers { get; init; }
[JsonPropertyName("subscriptionGroupId")]
@@ -3441,6 +3544,22 @@ public sealed record SubscriptionPeriodValueIOS
public required int Value { get; init; }
}
+public sealed record SubscriptionPricingTermsIOS
+{
+ [JsonPropertyName("billingDisplayPrice")]
+ public required string BillingDisplayPrice { get; init; }
+ [JsonPropertyName("billingPeriod")]
+ public required SubscriptionPeriodValueIOS BillingPeriod { get; init; }
+ [JsonPropertyName("billingPlanType")]
+ public required SubscriptionBillingPlanTypeIOS BillingPlanType { get; init; }
+ [JsonPropertyName("billingPrice")]
+ public required double BillingPrice { get; init; }
+ [JsonPropertyName("commitmentInfo")]
+ public required SubscriptionCommitmentInfoIOS CommitmentInfo { get; init; }
+ [JsonPropertyName("subscriptionOffers")]
+ public IReadOnlyList? SubscriptionOffers { get; init; }
+}
+
public sealed record SubscriptionStatusIOS
{
[JsonPropertyName("renewalInfo")]
@@ -3449,6 +3568,18 @@ public sealed record SubscriptionStatusIOS
public required string State { get; init; }
}
+public sealed record TransactionCommitmentInfoIOS
+{
+ [JsonPropertyName("billingPeriodNumber")]
+ public required int BillingPeriodNumber { get; init; }
+ [JsonPropertyName("commitmentExpiresDate")]
+ public required double CommitmentExpiresDate { get; init; }
+ [JsonPropertyName("commitmentPrice")]
+ public required double CommitmentPrice { get; init; }
+ [JsonPropertyName("totalBillingPeriods")]
+ public required int TotalBillingPeriods { get; init; }
+}
+
/// User Choice Billing event details (Android)
/// Fired when a user selects alternative billing in the User Choice Billing dialog
public sealed record UserChoiceBillingDetails
@@ -3925,6 +4056,10 @@ public sealed record RequestSubscriptionIosProps
/// Back-deployed to iOS 15.
[JsonPropertyName("promotionalOfferJWS")]
public PromotionalOfferJWSInputIOS? PromotionalOfferJws { get; init; }
+ /// Billing plan to use when purchasing an annual subscription that offers
+ /// monthly billing with a 12-month commitment (iOS 26.4+).
+ [JsonPropertyName("billingPlanType")]
+ public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; }
/// Override introductory offer eligibility (iOS 15+, WWDC 2025).
/// Set to true to indicate the user is eligible for introductory offer,
/// or false to indicate they are not. When nil, the system determines eligibility.
diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt
index be4d7f6d4..10f083550 100644
--- a/packages/gql/src/generated/Types.kt
+++ b/packages/gql/src/generated/Types.kt
@@ -901,6 +901,38 @@ public enum class SubResponseCodeAndroid(val rawValue: String) {
fun toJson(): String = rawValue
}
+public enum class SubscriptionBillingPlanTypeIOS(val rawValue: String) {
+ /**
+ * Unknown or unsupported billing plan type.
+ */
+ Unknown("unknown"),
+ /**
+ * Monthly billing with a 12-month commitment.
+ */
+ Monthly("monthly"),
+ /**
+ * Up-front billing for the full subscription period.
+ */
+ UpFront("up-front")
+
+ companion object {
+ fun fromJson(value: String): SubscriptionBillingPlanTypeIOS = when (value) {
+ "unknown" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "UNKNOWN" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "Unknown" -> SubscriptionBillingPlanTypeIOS.Unknown
+ "monthly" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "MONTHLY" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "Monthly" -> SubscriptionBillingPlanTypeIOS.Monthly
+ "up-front" -> SubscriptionBillingPlanTypeIOS.UpFront
+ "UP_FRONT" -> SubscriptionBillingPlanTypeIOS.UpFront
+ "UpFront" -> SubscriptionBillingPlanTypeIOS.UpFront
+ else -> throw IllegalArgumentException("Unknown SubscriptionBillingPlanTypeIOS value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class SubscriptionOfferTypeIOS(val rawValue: String) {
Introductory("introductory"),
Promotional("promotional"),
@@ -2683,6 +2715,11 @@ public data class ProductIOS(
val jsonRepresentationIOS: String,
override val platform: IapPlatform = IapPlatform.Ios,
override val price: Double? = null,
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ val pricingTermsIOS: List? = null,
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
*/
@@ -2713,6 +2750,7 @@ public data class ProductIOS(
jsonRepresentationIOS = json["jsonRepresentationIOS"] as? String ?: "",
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = (json["pricingTermsIOS"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
subscriptionInfoIOS = (json["subscriptionInfoIOS"] as? Map)?.let { SubscriptionInfoIOS.fromJson(it) },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
title = json["title"] as? String ?: "",
@@ -2735,6 +2773,7 @@ public data class ProductIOS(
"jsonRepresentationIOS" to jsonRepresentationIOS,
"platform" to platform.toJson(),
"price" to price,
+ "pricingTermsIOS" to pricingTermsIOS?.map { it.toJson() },
"subscriptionInfoIOS" to subscriptionInfoIOS?.toJson(),
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
"title" to title,
@@ -2895,6 +2934,11 @@ public data class ProductSubscriptionIOS(
val jsonRepresentationIOS: String,
override val platform: IapPlatform = IapPlatform.Ios,
override val price: Double? = null,
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ val pricingTermsIOS: List? = null,
/**
* App Store subscription group identifier for intro-offer eligibility checks.
*/
@@ -2936,6 +2980,7 @@ public data class ProductSubscriptionIOS(
jsonRepresentationIOS = json["jsonRepresentationIOS"] as? String ?: "",
platform = (json["platform"] as? String)?.let { IapPlatform.fromJson(it) } ?: IapPlatform.Ios,
price = (json["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = (json["pricingTermsIOS"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
subscriptionGroupIdIOS = json["subscriptionGroupIdIOS"] as? String,
subscriptionInfoIOS = (json["subscriptionInfoIOS"] as? Map)?.let { SubscriptionInfoIOS.fromJson(it) },
subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
@@ -2967,6 +3012,7 @@ public data class ProductSubscriptionIOS(
"jsonRepresentationIOS" to jsonRepresentationIOS,
"platform" to platform.toJson(),
"price" to price,
+ "pricingTermsIOS" to pricingTermsIOS?.map { it.toJson() },
"subscriptionGroupIdIOS" to subscriptionGroupIdIOS,
"subscriptionInfoIOS" to subscriptionInfoIOS?.toJson(),
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
@@ -3123,6 +3169,14 @@ public data class PurchaseIOS(
val advancedCommerceInfoIOS: AdvancedCommerceInfoIOS? = null,
val appAccountToken: String? = null,
val appBundleIdIOS: String? = null,
+ /**
+ * iOS 26.4+ billing plan selected for this transaction.
+ */
+ val billingPlanTypeIOS: SubscriptionBillingPlanTypeIOS? = null,
+ /**
+ * iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ */
+ val commitmentInfoIOS: TransactionCommitmentInfoIOS? = null,
val countryCodeIOS: String? = null,
val currencyCodeIOS: String? = null,
val currencySymbolIOS: String? = null,
@@ -3166,6 +3220,8 @@ public data class PurchaseIOS(
advancedCommerceInfoIOS = (json["advancedCommerceInfoIOS"] as? Map)?.let { AdvancedCommerceInfoIOS.fromJson(it) },
appAccountToken = json["appAccountToken"] as? String,
appBundleIdIOS = json["appBundleIdIOS"] as? String,
+ billingPlanTypeIOS = (json["billingPlanTypeIOS"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) },
+ commitmentInfoIOS = (json["commitmentInfoIOS"] as? Map)?.let { TransactionCommitmentInfoIOS.fromJson(it) },
countryCodeIOS = json["countryCodeIOS"] as? String,
currencyCodeIOS = json["currencyCodeIOS"] as? String,
currencySymbolIOS = json["currencySymbolIOS"] as? String,
@@ -3207,6 +3263,8 @@ public data class PurchaseIOS(
"advancedCommerceInfoIOS" to advancedCommerceInfoIOS?.toJson(),
"appAccountToken" to appAccountToken,
"appBundleIdIOS" to appBundleIdIOS,
+ "billingPlanTypeIOS" to billingPlanTypeIOS?.toJson(),
+ "commitmentInfoIOS" to commitmentInfoIOS?.toJson(),
"countryCodeIOS" to countryCodeIOS,
"currencyCodeIOS" to currencyCodeIOS,
"currencySymbolIOS" to currencySymbolIOS,
@@ -3287,12 +3345,47 @@ public data class RefundResultIOS(
)
}
+public data class RenewalCommitmentInfoIOS(
+ val commitmentAutoRenewProductId: String,
+ val commitmentAutoRenewStatus: Boolean,
+ val commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS,
+ val commitmentRenewalDate: Double,
+ val commitmentRenewalPrice: Double
+) {
+
+ companion object {
+ fun fromJson(json: Map): RenewalCommitmentInfoIOS {
+ return RenewalCommitmentInfoIOS(
+ commitmentAutoRenewProductId = json["commitmentAutoRenewProductId"] as? String ?: "",
+ commitmentAutoRenewStatus = json["commitmentAutoRenewStatus"] as? Boolean ?: false,
+ commitmentRenewalBillingPlanType = (json["commitmentRenewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown,
+ commitmentRenewalDate = (json["commitmentRenewalDate"] as? Number)?.toDouble() ?: 0.0,
+ commitmentRenewalPrice = (json["commitmentRenewalPrice"] as? Number)?.toDouble() ?: 0.0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "RenewalCommitmentInfoIOS",
+ "commitmentAutoRenewProductId" to commitmentAutoRenewProductId,
+ "commitmentAutoRenewStatus" to commitmentAutoRenewStatus,
+ "commitmentRenewalBillingPlanType" to commitmentRenewalBillingPlanType.toJson(),
+ "commitmentRenewalDate" to commitmentRenewalDate,
+ "commitmentRenewalPrice" to commitmentRenewalPrice,
+ )
+}
+
/**
* Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
* https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
*/
public data class RenewalInfoIOS(
val autoRenewPreference: String? = null,
+ /**
+ * iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ * 12-month commitment.
+ */
+ val commitmentInfo: RenewalCommitmentInfoIOS? = null,
/**
* When subscription expires due to cancellation/billing issue
* Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
@@ -3319,6 +3412,10 @@ public data class RenewalInfoIOS(
* Possible values: "AGREED", "PENDING", null (no price increase)
*/
val priceIncreaseStatus: String? = null,
+ /**
+ * iOS 26.4+ billing plan that will renew after the current period.
+ */
+ val renewalBillingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
* Expected renewal date (milliseconds since epoch)
* For active subscriptions, when the next renewal/charge will occur
@@ -3340,12 +3437,14 @@ public data class RenewalInfoIOS(
fun fromJson(json: Map): RenewalInfoIOS {
return RenewalInfoIOS(
autoRenewPreference = json["autoRenewPreference"] as? String,
+ commitmentInfo = (json["commitmentInfo"] as? Map)?.let { RenewalCommitmentInfoIOS.fromJson(it) },
expirationReason = json["expirationReason"] as? String,
gracePeriodExpirationDate = (json["gracePeriodExpirationDate"] as? Number)?.toDouble(),
isInBillingRetry = json["isInBillingRetry"] as? Boolean,
jsonRepresentation = json["jsonRepresentation"] as? String,
pendingUpgradeProductId = json["pendingUpgradeProductId"] as? String,
priceIncreaseStatus = json["priceIncreaseStatus"] as? String,
+ renewalBillingPlanType = (json["renewalBillingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) },
renewalDate = (json["renewalDate"] as? Number)?.toDouble(),
renewalOfferId = json["renewalOfferId"] as? String,
renewalOfferType = json["renewalOfferType"] as? String,
@@ -3357,12 +3456,14 @@ public data class RenewalInfoIOS(
fun toJson(): Map = mapOf(
"__typename" to "RenewalInfoIOS",
"autoRenewPreference" to autoRenewPreference,
+ "commitmentInfo" to commitmentInfo?.toJson(),
"expirationReason" to expirationReason,
"gracePeriodExpirationDate" to gracePeriodExpirationDate,
"isInBillingRetry" to isInBillingRetry,
"jsonRepresentation" to jsonRepresentation,
"pendingUpgradeProductId" to pendingUpgradeProductId,
"priceIncreaseStatus" to priceIncreaseStatus,
+ "renewalBillingPlanType" to renewalBillingPlanType?.toJson(),
"renewalDate" to renewalDate,
"renewalOfferId" to renewalOfferId,
"renewalOfferType" to renewalOfferType,
@@ -3438,8 +3539,33 @@ public data class RequestVerifyPurchaseWithIapkitResult(
)
}
+public data class SubscriptionCommitmentInfoIOS(
+ val displayPrice: String,
+ val period: SubscriptionPeriodValueIOS,
+ val price: Double
+) {
+
+ companion object {
+ fun fromJson(json: Map): SubscriptionCommitmentInfoIOS {
+ return SubscriptionCommitmentInfoIOS(
+ displayPrice = json["displayPrice"] as? String ?: "",
+ period = (json["period"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
+ price = (json["price"] as? Number)?.toDouble() ?: 0.0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "SubscriptionCommitmentInfoIOS",
+ "displayPrice" to displayPrice,
+ "period" to period.toJson(),
+ "price" to price,
+ )
+}
+
public data class SubscriptionInfoIOS(
val introductoryOffer: SubscriptionOfferIOS? = null,
+ val pricingTerms: List? = null,
val promotionalOffers: List? = null,
val subscriptionGroupId: String,
val subscriptionPeriod: SubscriptionPeriodValueIOS
@@ -3449,6 +3575,7 @@ public data class SubscriptionInfoIOS(
fun fromJson(json: Map): SubscriptionInfoIOS {
return SubscriptionInfoIOS(
introductoryOffer = (json["introductoryOffer"] as? Map)?.let { SubscriptionOfferIOS.fromJson(it) },
+ pricingTerms = (json["pricingTerms"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionPricingTermsIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPricingTermsIOS") },
promotionalOffers = (json["promotionalOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOfferIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOfferIOS") },
subscriptionGroupId = json["subscriptionGroupId"] as? String ?: "",
subscriptionPeriod = (json["subscriptionPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
@@ -3459,6 +3586,7 @@ public data class SubscriptionInfoIOS(
fun toJson(): Map = mapOf(
"__typename" to "SubscriptionInfoIOS",
"introductoryOffer" to introductoryOffer?.toJson(),
+ "pricingTerms" to pricingTerms?.map { it.toJson() },
"promotionalOffers" to promotionalOffers?.map { it.toJson() },
"subscriptionGroupId" to subscriptionGroupId,
"subscriptionPeriod" to subscriptionPeriod.toJson(),
@@ -3708,6 +3836,39 @@ public data class SubscriptionPeriodValueIOS(
)
}
+public data class SubscriptionPricingTermsIOS(
+ val billingDisplayPrice: String,
+ val billingPeriod: SubscriptionPeriodValueIOS,
+ val billingPlanType: SubscriptionBillingPlanTypeIOS,
+ val billingPrice: Double,
+ val commitmentInfo: SubscriptionCommitmentInfoIOS,
+ val subscriptionOffers: List? = null
+) {
+
+ companion object {
+ fun fromJson(json: Map): SubscriptionPricingTermsIOS {
+ return SubscriptionPricingTermsIOS(
+ billingDisplayPrice = json["billingDisplayPrice"] as? String ?: "",
+ billingPeriod = (json["billingPeriod"] as? Map)?.let { SubscriptionPeriodValueIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionPeriodValueIOS"),
+ billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) } ?: SubscriptionBillingPlanTypeIOS.Unknown,
+ billingPrice = (json["billingPrice"] as? Number)?.toDouble() ?: 0.0,
+ commitmentInfo = (json["commitmentInfo"] as? Map)?.let { SubscriptionCommitmentInfoIOS.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionCommitmentInfoIOS"),
+ subscriptionOffers = (json["subscriptionOffers"] as? List<*>)?.mapNotNull { (it as? Map)?.let { SubscriptionOffer.fromJson(it) } ?: throw IllegalArgumentException("Missing required object for SubscriptionOffer") },
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "SubscriptionPricingTermsIOS",
+ "billingDisplayPrice" to billingDisplayPrice,
+ "billingPeriod" to billingPeriod.toJson(),
+ "billingPlanType" to billingPlanType.toJson(),
+ "billingPrice" to billingPrice,
+ "commitmentInfo" to commitmentInfo.toJson(),
+ "subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
+ )
+}
+
public data class SubscriptionStatusIOS(
val renewalInfo: RenewalInfoIOS? = null,
val state: String
@@ -3729,6 +3890,33 @@ public data class SubscriptionStatusIOS(
)
}
+public data class TransactionCommitmentInfoIOS(
+ val billingPeriodNumber: Int,
+ val commitmentExpiresDate: Double,
+ val commitmentPrice: Double,
+ val totalBillingPeriods: Int
+) {
+
+ companion object {
+ fun fromJson(json: Map): TransactionCommitmentInfoIOS {
+ return TransactionCommitmentInfoIOS(
+ billingPeriodNumber = (json["billingPeriodNumber"] as? Number)?.toInt() ?: 0,
+ commitmentExpiresDate = (json["commitmentExpiresDate"] as? Number)?.toDouble() ?: 0.0,
+ commitmentPrice = (json["commitmentPrice"] as? Number)?.toDouble() ?: 0.0,
+ totalBillingPeriods = (json["totalBillingPeriods"] as? Number)?.toInt() ?: 0,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "TransactionCommitmentInfoIOS",
+ "billingPeriodNumber" to billingPeriodNumber,
+ "commitmentExpiresDate" to commitmentExpiresDate,
+ "commitmentPrice" to commitmentPrice,
+ "totalBillingPeriods" to totalBillingPeriods,
+ )
+}
+
/**
* User Choice Billing event details (Android)
* Fired when a user selects alternative billing in the User Choice Billing dialog
@@ -4751,6 +4939,11 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData: String? = null,
val andDangerouslyFinishTransactionAutomatically: Boolean? = null,
val appAccountToken: String? = null,
+ /**
+ * Billing plan to use when purchasing an annual subscription that offers
+ * monthly billing with a 12-month commitment (iOS 26.4+).
+ */
+ val billingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -4784,6 +4977,7 @@ public data class RequestSubscriptionIosProps(
val advancedCommerceData = json["advancedCommerceData"] as? String
val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean
val appAccountToken = json["appAccountToken"] as? String
+ val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }
val introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean
val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }
val quantity = (json["quantity"] as? Number)?.toInt()
@@ -4795,6 +4989,7 @@ public data class RequestSubscriptionIosProps(
advancedCommerceData = advancedCommerceData,
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
appAccountToken = appAccountToken,
+ billingPlanType = billingPlanType,
introductoryOfferEligibility = introductoryOfferEligibility,
promotionalOfferJWS = promotionalOfferJWS,
quantity = quantity,
@@ -4809,6 +5004,7 @@ public data class RequestSubscriptionIosProps(
"advancedCommerceData" to advancedCommerceData,
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
+ "billingPlanType" to billingPlanType?.toJson(),
"introductoryOfferEligibility" to introductoryOfferEligibility,
"promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift
index 0199cfc74..95327c44e 100644
--- a/packages/gql/src/generated/Types.swift
+++ b/packages/gql/src/generated/Types.swift
@@ -379,6 +379,15 @@ public enum SubResponseCodeAndroid: String, Codable, CaseIterable {
case userIneligible = "user-ineligible"
}
+public enum SubscriptionBillingPlanTypeIOS: String, Codable, CaseIterable {
+ /// Unknown or unsupported billing plan type.
+ case unknown = "unknown"
+ /// Monthly billing with a 12-month commitment.
+ case monthly = "monthly"
+ /// Up-front billing for the full subscription period.
+ case upFront = "up-front"
+}
+
public enum SubscriptionOfferTypeIOS: String, Codable, CaseIterable {
case introductory = "introductory"
case promotional = "promotional"
@@ -1012,6 +1021,9 @@ public struct ProductIOS: Codable, ProductCommon {
public var jsonRepresentationIOS: String
public var platform: IapPlatform = .ios
public var price: Double? = nil
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ public var pricingTermsIOS: [SubscriptionPricingTermsIOS]? = nil
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
public var subscriptionInfoIOS: SubscriptionInfoIOS? = nil
/// Standardized subscription offers.
@@ -1092,6 +1104,9 @@ public struct ProductSubscriptionIOS: Codable, ProductCommon {
public var jsonRepresentationIOS: String
public var platform: IapPlatform = .ios
public var price: Double? = nil
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ public var pricingTermsIOS: [SubscriptionPricingTermsIOS]? = nil
/// App Store subscription group identifier for intro-offer eligibility checks.
public var subscriptionGroupIdIOS: String? = nil
/// @deprecated Use subscriptionOffers for offer metadata and subscriptionGroupIdIOS for the App Store subscription group identifier.
@@ -1160,6 +1175,10 @@ public struct PurchaseIOS: Codable, PurchaseCommon {
public var advancedCommerceInfoIOS: AdvancedCommerceInfoIOS? = nil
public var appAccountToken: String? = nil
public var appBundleIdIOS: String? = nil
+ /// iOS 26.4+ billing plan selected for this transaction.
+ public var billingPlanTypeIOS: SubscriptionBillingPlanTypeIOS? = nil
+ /// iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ public var commitmentInfoIOS: TransactionCommitmentInfoIOS? = nil
public var countryCodeIOS: String? = nil
public var currencyCodeIOS: String? = nil
public var currencySymbolIOS: String? = nil
@@ -1206,10 +1225,21 @@ public struct RefundResultIOS: Codable {
public var status: String
}
+public struct RenewalCommitmentInfoIOS: Codable {
+ public var commitmentAutoRenewProductId: String
+ public var commitmentAutoRenewStatus: Bool
+ public var commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS
+ public var commitmentRenewalDate: Double
+ public var commitmentRenewalPrice: Double
+}
+
/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
public struct RenewalInfoIOS: Codable {
public var autoRenewPreference: String? = nil
+ /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ /// 12-month commitment.
+ public var commitmentInfo: RenewalCommitmentInfoIOS? = nil
/// When subscription expires due to cancellation/billing issue
/// Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
public var expirationReason: String? = nil
@@ -1226,6 +1256,8 @@ public struct RenewalInfoIOS: Codable {
/// User's response to subscription price increase
/// Possible values: "AGREED", "PENDING", null (no price increase)
public var priceIncreaseStatus: String? = nil
+ /// iOS 26.4+ billing plan that will renew after the current period.
+ public var renewalBillingPlanType: SubscriptionBillingPlanTypeIOS? = nil
/// Expected renewal date (milliseconds since epoch)
/// For active subscriptions, when the next renewal/charge will occur
public var renewalDate: Double? = nil
@@ -1260,8 +1292,15 @@ public struct RequestVerifyPurchaseWithIapkitResult: Codable {
public var store: IapStore
}
+public struct SubscriptionCommitmentInfoIOS: Codable {
+ public var displayPrice: String
+ public var period: SubscriptionPeriodValueIOS
+ public var price: Double
+}
+
public struct SubscriptionInfoIOS: Codable {
public var introductoryOffer: SubscriptionOfferIOS? = nil
+ public var pricingTerms: [SubscriptionPricingTermsIOS]? = nil
public var promotionalOffers: [SubscriptionOfferIOS]? = nil
public var subscriptionGroupId: String
public var subscriptionPeriod: SubscriptionPeriodValueIOS
@@ -1354,11 +1393,27 @@ public struct SubscriptionPeriodValueIOS: Codable {
public var value: Int
}
+public struct SubscriptionPricingTermsIOS: Codable {
+ public var billingDisplayPrice: String
+ public var billingPeriod: SubscriptionPeriodValueIOS
+ public var billingPlanType: SubscriptionBillingPlanTypeIOS
+ public var billingPrice: Double
+ public var commitmentInfo: SubscriptionCommitmentInfoIOS
+ public var subscriptionOffers: [SubscriptionOffer]? = nil
+}
+
public struct SubscriptionStatusIOS: Codable {
public var renewalInfo: RenewalInfoIOS? = nil
public var state: String
}
+public struct TransactionCommitmentInfoIOS: Codable {
+ public var billingPeriodNumber: Int
+ public var commitmentExpiresDate: Double
+ public var commitmentPrice: Double
+ public var totalBillingPeriods: Int
+}
+
/// User Choice Billing event details (Android)
/// Fired when a user selects alternative billing in the User Choice Billing dialog
public struct UserChoiceBillingDetails: Codable {
@@ -1928,6 +1983,9 @@ public struct RequestSubscriptionIosProps: Codable {
public var advancedCommerceData: String?
public var andDangerouslyFinishTransactionAutomatically: Bool?
public var appAccountToken: String?
+ /// Billing plan to use when purchasing an annual subscription that offers
+ /// monthly billing with a 12-month commitment (iOS 26.4+).
+ public var billingPlanType: SubscriptionBillingPlanTypeIOS?
/// Override introductory offer eligibility (iOS 15+, WWDC 2025).
/// Set to true to indicate the user is eligible for introductory offer,
/// or false to indicate they are not. When nil, the system determines eligibility.
@@ -1952,6 +2010,7 @@ public struct RequestSubscriptionIosProps: Codable {
advancedCommerceData: String? = nil,
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
+ billingPlanType: SubscriptionBillingPlanTypeIOS? = nil,
introductoryOfferEligibility: Bool? = nil,
promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
@@ -1962,6 +2021,7 @@ public struct RequestSubscriptionIosProps: Codable {
self.advancedCommerceData = advancedCommerceData
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
+ self.billingPlanType = billingPlanType
self.introductoryOfferEligibility = introductoryOfferEligibility
self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart
index 9723c028e..bbe4f26b5 100644
--- a/packages/gql/src/generated/types.dart
+++ b/packages/gql/src/generated/types.dart
@@ -795,6 +795,33 @@ enum SubResponseCodeAndroid {
String toJson() => value;
}
+enum SubscriptionBillingPlanTypeIOS {
+ /// Unknown or unsupported billing plan type.
+ Unknown('unknown'),
+ /// Monthly billing with a 12-month commitment.
+ Monthly('monthly'),
+ /// Up-front billing for the full subscription period.
+ UpFront('up-front');
+
+ const SubscriptionBillingPlanTypeIOS(this.value);
+ final String value;
+
+ factory SubscriptionBillingPlanTypeIOS.fromJson(String value) {
+ final normalized = value.toLowerCase().replaceAll('_', '-');
+ switch (normalized) {
+ case 'unknown':
+ return SubscriptionBillingPlanTypeIOS.Unknown;
+ case 'monthly':
+ return SubscriptionBillingPlanTypeIOS.Monthly;
+ case 'up-front':
+ return SubscriptionBillingPlanTypeIOS.UpFront;
+ }
+ throw ArgumentError('Unknown SubscriptionBillingPlanTypeIOS value: $value');
+ }
+
+ String toJson() => value;
+}
+
enum SubscriptionOfferTypeIOS {
Introductory('introductory'),
Promotional('promotional'),
@@ -2500,6 +2527,7 @@ class ProductIOS extends Product implements ProductCommon {
required this.jsonRepresentationIOS,
this.platform = IapPlatform.IOS,
this.price,
+ this.pricingTermsIOS,
this.subscriptionInfoIOS,
this.subscriptionOffers,
required this.title,
@@ -2518,6 +2546,9 @@ class ProductIOS extends Product implements ProductCommon {
final String jsonRepresentationIOS;
final IapPlatform platform;
final double? price;
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ final List? pricingTermsIOS;
/// @deprecated Use subscriptionOffers instead for cross-platform compatibility.
final SubscriptionInfoIOS? subscriptionInfoIOS;
/// Standardized subscription offers.
@@ -2542,6 +2573,7 @@ class ProductIOS extends Product implements ProductCommon {
jsonRepresentationIOS: json['jsonRepresentationIOS'] as String,
platform: IapPlatform.fromJson(json['platform'] as String),
price: (json['price'] as num?)?.toDouble(),
+ pricingTermsIOS: (json['pricingTermsIOS'] as List?) == null ? null : (json['pricingTermsIOS'] as List?)!.map((e) => SubscriptionPricingTermsIOS.fromJson(e as Map)).toList(),
subscriptionInfoIOS: json['subscriptionInfoIOS'] != null ? SubscriptionInfoIOS.fromJson(json['subscriptionInfoIOS'] as Map) : null,
subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
title: json['title'] as String,
@@ -2565,6 +2597,7 @@ class ProductIOS extends Product implements ProductCommon {
'jsonRepresentationIOS': jsonRepresentationIOS,
'platform': platform.toJson(),
'price': price,
+ 'pricingTermsIOS': pricingTermsIOS == null ? null : pricingTermsIOS!.map((e) => e.toJson()).toList(),
'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(),
'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
'title': title,
@@ -2737,6 +2770,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
required this.jsonRepresentationIOS,
this.platform = IapPlatform.IOS,
this.price,
+ this.pricingTermsIOS,
this.subscriptionGroupIdIOS,
this.subscriptionInfoIOS,
this.subscriptionOffers,
@@ -2765,6 +2799,9 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
final String jsonRepresentationIOS;
final IapPlatform platform;
final double? price;
+ /// iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ /// monthly subscriptions with a 12-month commitment.
+ final List? pricingTermsIOS;
/// App Store subscription group identifier for intro-offer eligibility checks.
final String? subscriptionGroupIdIOS;
/// @deprecated Use subscriptionOffers for offer metadata and subscriptionGroupIdIOS for the App Store subscription group identifier.
@@ -2798,6 +2835,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
jsonRepresentationIOS: json['jsonRepresentationIOS'] as String,
platform: IapPlatform.fromJson(json['platform'] as String),
price: (json['price'] as num?)?.toDouble(),
+ pricingTermsIOS: (json['pricingTermsIOS'] as List?) == null ? null : (json['pricingTermsIOS'] as List?)!.map((e) => SubscriptionPricingTermsIOS.fromJson(e as Map)).toList(),
subscriptionGroupIdIOS: json['subscriptionGroupIdIOS'] as String?,
subscriptionInfoIOS: json['subscriptionInfoIOS'] != null ? SubscriptionInfoIOS.fromJson(json['subscriptionInfoIOS'] as Map) : null,
subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
@@ -2830,6 +2868,7 @@ class ProductSubscriptionIOS extends ProductSubscription implements ProductCommo
'jsonRepresentationIOS': jsonRepresentationIOS,
'platform': platform.toJson(),
'price': price,
+ 'pricingTermsIOS': pricingTermsIOS == null ? null : pricingTermsIOS!.map((e) => e.toJson()).toList(),
'subscriptionGroupIdIOS': subscriptionGroupIdIOS,
'subscriptionInfoIOS': subscriptionInfoIOS?.toJson(),
'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
@@ -3016,6 +3055,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
this.advancedCommerceInfoIOS,
this.appAccountToken,
this.appBundleIdIOS,
+ this.billingPlanTypeIOS,
+ this.commitmentInfoIOS,
this.countryCodeIOS,
this.currencyCodeIOS,
this.currencySymbolIOS,
@@ -3057,6 +3098,10 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
final AdvancedCommerceInfoIOS? advancedCommerceInfoIOS;
final String? appAccountToken;
final String? appBundleIdIOS;
+ /// iOS 26.4+ billing plan selected for this transaction.
+ final SubscriptionBillingPlanTypeIOS? billingPlanTypeIOS;
+ /// iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ final TransactionCommitmentInfoIOS? commitmentInfoIOS;
final String? countryCodeIOS;
final String? currencyCodeIOS;
final String? currencySymbolIOS;
@@ -3097,6 +3142,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
advancedCommerceInfoIOS: json['advancedCommerceInfoIOS'] != null ? AdvancedCommerceInfoIOS.fromJson(json['advancedCommerceInfoIOS'] as Map) : null,
appAccountToken: json['appAccountToken'] as String?,
appBundleIdIOS: json['appBundleIdIOS'] as String?,
+ billingPlanTypeIOS: json['billingPlanTypeIOS'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanTypeIOS'] as String) : null,
+ commitmentInfoIOS: json['commitmentInfoIOS'] != null ? TransactionCommitmentInfoIOS.fromJson(json['commitmentInfoIOS'] as Map) : null,
countryCodeIOS: json['countryCodeIOS'] as String?,
currencyCodeIOS: json['currencyCodeIOS'] as String?,
currencySymbolIOS: json['currencySymbolIOS'] as String?,
@@ -3140,6 +3187,8 @@ class PurchaseIOS extends Purchase implements PurchaseCommon {
'advancedCommerceInfoIOS': advancedCommerceInfoIOS?.toJson(),
'appAccountToken': appAccountToken,
'appBundleIdIOS': appBundleIdIOS,
+ 'billingPlanTypeIOS': billingPlanTypeIOS?.toJson(),
+ 'commitmentInfoIOS': commitmentInfoIOS?.toJson(),
'countryCodeIOS': countryCodeIOS,
'currencyCodeIOS': currencyCodeIOS,
'currencySymbolIOS': currencySymbolIOS,
@@ -3231,17 +3280,56 @@ class RefundResultIOS {
}
}
+class RenewalCommitmentInfoIOS {
+ const RenewalCommitmentInfoIOS({
+ required this.commitmentAutoRenewProductId,
+ required this.commitmentAutoRenewStatus,
+ required this.commitmentRenewalBillingPlanType,
+ required this.commitmentRenewalDate,
+ required this.commitmentRenewalPrice,
+ });
+
+ final String commitmentAutoRenewProductId;
+ final bool commitmentAutoRenewStatus;
+ final SubscriptionBillingPlanTypeIOS commitmentRenewalBillingPlanType;
+ final double commitmentRenewalDate;
+ final double commitmentRenewalPrice;
+
+ factory RenewalCommitmentInfoIOS.fromJson(Map json) {
+ return RenewalCommitmentInfoIOS(
+ commitmentAutoRenewProductId: json['commitmentAutoRenewProductId'] as String,
+ commitmentAutoRenewStatus: json['commitmentAutoRenewStatus'] as bool,
+ commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS.fromJson(json['commitmentRenewalBillingPlanType'] as String),
+ commitmentRenewalDate: (json['commitmentRenewalDate'] as num).toDouble(),
+ commitmentRenewalPrice: (json['commitmentRenewalPrice'] as num).toDouble(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'RenewalCommitmentInfoIOS',
+ 'commitmentAutoRenewProductId': commitmentAutoRenewProductId,
+ 'commitmentAutoRenewStatus': commitmentAutoRenewStatus,
+ 'commitmentRenewalBillingPlanType': commitmentRenewalBillingPlanType.toJson(),
+ 'commitmentRenewalDate': commitmentRenewalDate,
+ 'commitmentRenewalPrice': commitmentRenewalPrice,
+ };
+ }
+}
+
/// Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
/// https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
class RenewalInfoIOS {
const RenewalInfoIOS({
this.autoRenewPreference,
+ this.commitmentInfo,
this.expirationReason,
this.gracePeriodExpirationDate,
this.isInBillingRetry,
this.jsonRepresentation,
this.pendingUpgradeProductId,
this.priceIncreaseStatus,
+ this.renewalBillingPlanType,
this.renewalDate,
this.renewalOfferId,
this.renewalOfferType,
@@ -3249,6 +3337,9 @@ class RenewalInfoIOS {
});
final String? autoRenewPreference;
+ /// iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ /// 12-month commitment.
+ final RenewalCommitmentInfoIOS? commitmentInfo;
/// When subscription expires due to cancellation/billing issue
/// Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
final String? expirationReason;
@@ -3265,6 +3356,8 @@ class RenewalInfoIOS {
/// User's response to subscription price increase
/// Possible values: "AGREED", "PENDING", null (no price increase)
final String? priceIncreaseStatus;
+ /// iOS 26.4+ billing plan that will renew after the current period.
+ final SubscriptionBillingPlanTypeIOS? renewalBillingPlanType;
/// Expected renewal date (milliseconds since epoch)
/// For active subscriptions, when the next renewal/charge will occur
final double? renewalDate;
@@ -3278,12 +3371,14 @@ class RenewalInfoIOS {
factory RenewalInfoIOS.fromJson(Map json) {
return RenewalInfoIOS(
autoRenewPreference: json['autoRenewPreference'] as String?,
+ commitmentInfo: json['commitmentInfo'] != null ? RenewalCommitmentInfoIOS.fromJson(json['commitmentInfo'] as Map) : null,
expirationReason: json['expirationReason'] as String?,
gracePeriodExpirationDate: (json['gracePeriodExpirationDate'] as num?)?.toDouble(),
isInBillingRetry: json['isInBillingRetry'] as bool?,
jsonRepresentation: json['jsonRepresentation'] as String?,
pendingUpgradeProductId: json['pendingUpgradeProductId'] as String?,
priceIncreaseStatus: json['priceIncreaseStatus'] as String?,
+ renewalBillingPlanType: json['renewalBillingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['renewalBillingPlanType'] as String) : null,
renewalDate: (json['renewalDate'] as num?)?.toDouble(),
renewalOfferId: json['renewalOfferId'] as String?,
renewalOfferType: json['renewalOfferType'] as String?,
@@ -3295,12 +3390,14 @@ class RenewalInfoIOS {
return {
'__typename': 'RenewalInfoIOS',
'autoRenewPreference': autoRenewPreference,
+ 'commitmentInfo': commitmentInfo?.toJson(),
'expirationReason': expirationReason,
'gracePeriodExpirationDate': gracePeriodExpirationDate,
'isInBillingRetry': isInBillingRetry,
'jsonRepresentation': jsonRepresentation,
'pendingUpgradeProductId': pendingUpgradeProductId,
'priceIncreaseStatus': priceIncreaseStatus,
+ 'renewalBillingPlanType': renewalBillingPlanType?.toJson(),
'renewalDate': renewalDate,
'renewalOfferId': renewalOfferId,
'renewalOfferType': renewalOfferType,
@@ -3384,15 +3481,46 @@ class RequestVerifyPurchaseWithIapkitResult {
}
}
+class SubscriptionCommitmentInfoIOS {
+ const SubscriptionCommitmentInfoIOS({
+ required this.displayPrice,
+ required this.period,
+ required this.price,
+ });
+
+ final String displayPrice;
+ final SubscriptionPeriodValueIOS period;
+ final double price;
+
+ factory SubscriptionCommitmentInfoIOS.fromJson(Map json) {
+ return SubscriptionCommitmentInfoIOS(
+ displayPrice: json['displayPrice'] as String,
+ period: SubscriptionPeriodValueIOS.fromJson(json['period'] as Map),
+ price: (json['price'] as num).toDouble(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'SubscriptionCommitmentInfoIOS',
+ 'displayPrice': displayPrice,
+ 'period': period.toJson(),
+ 'price': price,
+ };
+ }
+}
+
class SubscriptionInfoIOS {
const SubscriptionInfoIOS({
this.introductoryOffer,
+ this.pricingTerms,
this.promotionalOffers,
required this.subscriptionGroupId,
required this.subscriptionPeriod,
});
final SubscriptionOfferIOS? introductoryOffer;
+ final List? pricingTerms;
final List? promotionalOffers;
final String subscriptionGroupId;
final SubscriptionPeriodValueIOS subscriptionPeriod;
@@ -3400,6 +3528,7 @@ class SubscriptionInfoIOS {
factory SubscriptionInfoIOS.fromJson(Map json) {
return SubscriptionInfoIOS(
introductoryOffer: json['introductoryOffer'] != null ? SubscriptionOfferIOS.fromJson(json['introductoryOffer'] as Map) : null,
+ pricingTerms: (json['pricingTerms'] as List?) == null ? null : (json['pricingTerms'] as List?)!.map((e) => SubscriptionPricingTermsIOS.fromJson(e as Map)).toList(),
promotionalOffers: (json['promotionalOffers'] as List?) == null ? null : (json['promotionalOffers'] as List?)!.map((e) => SubscriptionOfferIOS.fromJson(e as Map)).toList(),
subscriptionGroupId: json['subscriptionGroupId'] as String,
subscriptionPeriod: SubscriptionPeriodValueIOS.fromJson(json['subscriptionPeriod'] as Map),
@@ -3410,6 +3539,7 @@ class SubscriptionInfoIOS {
return {
'__typename': 'SubscriptionInfoIOS',
'introductoryOffer': introductoryOffer?.toJson(),
+ 'pricingTerms': pricingTerms == null ? null : pricingTerms!.map((e) => e.toJson()).toList(),
'promotionalOffers': promotionalOffers == null ? null : promotionalOffers!.map((e) => e.toJson()).toList(),
'subscriptionGroupId': subscriptionGroupId,
'subscriptionPeriod': subscriptionPeriod.toJson(),
@@ -3650,6 +3780,47 @@ class SubscriptionPeriodValueIOS {
}
}
+class SubscriptionPricingTermsIOS {
+ const SubscriptionPricingTermsIOS({
+ required this.billingDisplayPrice,
+ required this.billingPeriod,
+ required this.billingPlanType,
+ required this.billingPrice,
+ required this.commitmentInfo,
+ this.subscriptionOffers,
+ });
+
+ final String billingDisplayPrice;
+ final SubscriptionPeriodValueIOS billingPeriod;
+ final SubscriptionBillingPlanTypeIOS billingPlanType;
+ final double billingPrice;
+ final SubscriptionCommitmentInfoIOS commitmentInfo;
+ final List? subscriptionOffers;
+
+ factory SubscriptionPricingTermsIOS.fromJson(Map json) {
+ return SubscriptionPricingTermsIOS(
+ billingDisplayPrice: json['billingDisplayPrice'] as String,
+ billingPeriod: SubscriptionPeriodValueIOS.fromJson(json['billingPeriod'] as Map),
+ billingPlanType: SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String),
+ billingPrice: (json['billingPrice'] as num).toDouble(),
+ commitmentInfo: SubscriptionCommitmentInfoIOS.fromJson(json['commitmentInfo'] as Map),
+ subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => SubscriptionOffer.fromJson(e as Map)).toList(),
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'SubscriptionPricingTermsIOS',
+ 'billingDisplayPrice': billingDisplayPrice,
+ 'billingPeriod': billingPeriod.toJson(),
+ 'billingPlanType': billingPlanType.toJson(),
+ 'billingPrice': billingPrice,
+ 'commitmentInfo': commitmentInfo.toJson(),
+ 'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
+ };
+ }
+}
+
class SubscriptionStatusIOS {
const SubscriptionStatusIOS({
this.renewalInfo,
@@ -3675,6 +3846,39 @@ class SubscriptionStatusIOS {
}
}
+class TransactionCommitmentInfoIOS {
+ const TransactionCommitmentInfoIOS({
+ required this.billingPeriodNumber,
+ required this.commitmentExpiresDate,
+ required this.commitmentPrice,
+ required this.totalBillingPeriods,
+ });
+
+ final int billingPeriodNumber;
+ final double commitmentExpiresDate;
+ final double commitmentPrice;
+ final int totalBillingPeriods;
+
+ factory TransactionCommitmentInfoIOS.fromJson(Map json) {
+ return TransactionCommitmentInfoIOS(
+ billingPeriodNumber: json['billingPeriodNumber'] as int,
+ commitmentExpiresDate: (json['commitmentExpiresDate'] as num).toDouble(),
+ commitmentPrice: (json['commitmentPrice'] as num).toDouble(),
+ totalBillingPeriods: json['totalBillingPeriods'] as int,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'TransactionCommitmentInfoIOS',
+ 'billingPeriodNumber': billingPeriodNumber,
+ 'commitmentExpiresDate': commitmentExpiresDate,
+ 'commitmentPrice': commitmentPrice,
+ 'totalBillingPeriods': totalBillingPeriods,
+ };
+ }
+}
+
/// User Choice Billing event details (Android)
/// Fired when a user selects alternative billing in the User Choice Billing dialog
class UserChoiceBillingDetails {
@@ -4649,6 +4853,7 @@ class RequestSubscriptionIosProps {
this.advancedCommerceData,
this.andDangerouslyFinishTransactionAutomatically,
this.appAccountToken,
+ this.billingPlanType,
this.introductoryOfferEligibility,
this.promotionalOfferJWS,
this.quantity,
@@ -4664,6 +4869,9 @@ class RequestSubscriptionIosProps {
final String? advancedCommerceData;
final bool? andDangerouslyFinishTransactionAutomatically;
final String? appAccountToken;
+ /// Billing plan to use when purchasing an annual subscription that offers
+ /// monthly billing with a 12-month commitment (iOS 26.4+).
+ final SubscriptionBillingPlanTypeIOS? billingPlanType;
/// Override introductory offer eligibility (iOS 15+, WWDC 2025).
/// Set to true to indicate the user is eligible for introductory offer,
/// or false to indicate they are not. When nil, the system determines eligibility.
@@ -4689,6 +4897,7 @@ class RequestSubscriptionIosProps {
advancedCommerceData: json['advancedCommerceData'] as String?,
andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?,
appAccountToken: json['appAccountToken'] as String?,
+ billingPlanType: json['billingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String) : null,
introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?,
promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null,
quantity: json['quantity'] as int?,
@@ -4703,6 +4912,7 @@ class RequestSubscriptionIosProps {
'advancedCommerceData': advancedCommerceData,
'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically,
'appAccountToken': appAccountToken,
+ 'billingPlanType': billingPlanType?.toJson(),
'introductoryOfferEligibility': introductoryOfferEligibility,
'promotionalOfferJWS': promotionalOfferJWS?.toJson(),
'quantity': quantity,
diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd
index 76bc09e74..b471470ed 100644
--- a/packages/gql/src/generated/types.gd
+++ b/packages/gql/src/generated/types.gd
@@ -253,6 +253,15 @@ enum SubResponseCodeAndroid {
USER_INELIGIBLE = 2,
}
+enum SubscriptionBillingPlanTypeIOS {
+ ## Unknown or unsupported billing plan type.
+ UNKNOWN = 0,
+ ## Monthly billing with a 12-month commitment.
+ MONTHLY = 1,
+ ## Up-front billing for the full subscription period.
+ UP_FRONT = 2,
+}
+
enum SubscriptionOfferTypeIOS {
INTRODUCTORY = 0,
PROMOTIONAL = 1,
@@ -1632,6 +1641,8 @@ class ProductIOS:
var type_ios: ProductTypeIOS
## Standardized subscription offers.
var subscription_offers: Array[SubscriptionOffer] = []
+ ## iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ var pricing_terms_ios: Array[SubscriptionPricingTermsIOS] = []
## @deprecated Use subscriptionOffers instead for cross-platform compatibility.
var subscription_info_ios: SubscriptionInfoIOS
@@ -1685,6 +1696,14 @@ class ProductIOS:
else:
arr.append(item)
obj.subscription_offers = arr
+ if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null:
+ var arr = []
+ for item in data["pricingTermsIOS"]:
+ if item is Dictionary:
+ arr.append(SubscriptionPricingTermsIOS.from_dict(item))
+ else:
+ arr.append(item)
+ obj.pricing_terms_ios = arr
if data.has("subscriptionInfoIOS") and data["subscriptionInfoIOS"] != null:
if data["subscriptionInfoIOS"] is Dictionary:
obj.subscription_info_ios = SubscriptionInfoIOS.from_dict(data["subscriptionInfoIOS"])
@@ -1730,6 +1749,16 @@ class ProductIOS:
dict["subscriptionOffers"] = arr
else:
dict["subscriptionOffers"] = null
+ if pricing_terms_ios != null:
+ var arr = []
+ for item in pricing_terms_ios:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["pricingTermsIOS"] = arr
+ else:
+ dict["pricingTermsIOS"] = null
if subscription_info_ios != null and subscription_info_ios.has_method("to_dict"):
dict["subscriptionInfoIOS"] = subscription_info_ios.to_dict()
else:
@@ -1965,6 +1994,8 @@ class ProductSubscriptionIOS:
var type_ios: ProductTypeIOS
## Standardized subscription offers.
var subscription_offers: Array[SubscriptionOffer] = []
+ ## iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ var pricing_terms_ios: Array[SubscriptionPricingTermsIOS] = []
## App Store subscription group identifier for intro-offer eligibility checks.
var subscription_group_id_ios: Variant = null
## @deprecated Use subscriptionOffers for offer metadata and subscriptionGroupIdIOS for the App Store subscription group identifier.
@@ -2029,6 +2060,14 @@ class ProductSubscriptionIOS:
else:
arr.append(item)
obj.subscription_offers = arr
+ if data.has("pricingTermsIOS") and data["pricingTermsIOS"] != null:
+ var arr = []
+ for item in data["pricingTermsIOS"]:
+ if item is Dictionary:
+ arr.append(SubscriptionPricingTermsIOS.from_dict(item))
+ else:
+ arr.append(item)
+ obj.pricing_terms_ios = arr
if data.has("subscriptionGroupIdIOS") and data["subscriptionGroupIdIOS"] != null:
obj.subscription_group_id_ios = data["subscriptionGroupIdIOS"]
if data.has("subscriptionInfoIOS") and data["subscriptionInfoIOS"] != null:
@@ -2110,6 +2149,16 @@ class ProductSubscriptionIOS:
dict["subscriptionOffers"] = arr
else:
dict["subscriptionOffers"] = null
+ if pricing_terms_ios != null:
+ var arr = []
+ for item in pricing_terms_ios:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["pricingTermsIOS"] = arr
+ else:
+ dict["pricingTermsIOS"] = null
if subscription_group_id_ios != null:
dict["subscriptionGroupIdIOS"] = subscription_group_id_ios
if subscription_info_ios != null and subscription_info_ios.has_method("to_dict"):
@@ -2378,6 +2427,10 @@ class PurchaseIOS:
var currency_symbol_ios: Variant = null
var country_code_ios: Variant = null
var renewal_info_ios: RenewalInfoIOS
+ ## iOS 26.4+ billing plan selected for this transaction.
+ var billing_plan_type_ios: SubscriptionBillingPlanTypeIOS
+ ## iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment.
+ var commitment_info_ios: TransactionCommitmentInfoIOS
## Advanced Commerce API metadata (iOS 18.4+).
var advanced_commerce_info_ios: AdvancedCommerceInfoIOS
@@ -2469,6 +2522,17 @@ class PurchaseIOS:
obj.renewal_info_ios = RenewalInfoIOS.from_dict(data["renewalInfoIOS"])
else:
obj.renewal_info_ios = data["renewalInfoIOS"]
+ if data.has("billingPlanTypeIOS") and data["billingPlanTypeIOS"] != null:
+ var enum_str = data["billingPlanTypeIOS"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.billing_plan_type_ios = enum_str
+ if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null:
+ if data["commitmentInfoIOS"] is Dictionary:
+ obj.commitment_info_ios = TransactionCommitmentInfoIOS.from_dict(data["commitmentInfoIOS"])
+ else:
+ obj.commitment_info_ios = data["commitmentInfoIOS"]
if data.has("advancedCommerceInfoIOS") and data["advancedCommerceInfoIOS"] != null:
if data["advancedCommerceInfoIOS"] is Dictionary:
obj.advanced_commerce_info_ios = AdvancedCommerceInfoIOS.from_dict(data["advancedCommerceInfoIOS"])
@@ -2549,6 +2613,14 @@ class PurchaseIOS:
dict["renewalInfoIOS"] = renewal_info_ios.to_dict()
else:
dict["renewalInfoIOS"] = renewal_info_ios
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(billing_plan_type_ios):
+ dict["billingPlanTypeIOS"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type_ios]
+ else:
+ dict["billingPlanTypeIOS"] = billing_plan_type_ios
+ if commitment_info_ios != null and commitment_info_ios.has_method("to_dict"):
+ dict["commitmentInfoIOS"] = commitment_info_ios.to_dict()
+ else:
+ dict["commitmentInfoIOS"] = commitment_info_ios
if advanced_commerce_info_ios != null and advanced_commerce_info_ios.has_method("to_dict"):
dict["advancedCommerceInfoIOS"] = advanced_commerce_info_ios.to_dict()
else:
@@ -2596,6 +2668,43 @@ class RefundResultIOS:
dict["message"] = message
return dict
+class RenewalCommitmentInfoIOS:
+ var commitment_auto_renew_product_id: String = ""
+ var commitment_auto_renew_status: bool = false
+ var commitment_renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS
+ var commitment_renewal_date: float = 0.0
+ var commitment_renewal_price: float = 0.0
+
+ static func from_dict(data: Dictionary) -> RenewalCommitmentInfoIOS:
+ var obj = RenewalCommitmentInfoIOS.new()
+ if data.has("commitmentAutoRenewProductId") and data["commitmentAutoRenewProductId"] != null:
+ obj.commitment_auto_renew_product_id = data["commitmentAutoRenewProductId"]
+ if data.has("commitmentAutoRenewStatus") and data["commitmentAutoRenewStatus"] != null:
+ obj.commitment_auto_renew_status = data["commitmentAutoRenewStatus"]
+ if data.has("commitmentRenewalBillingPlanType") and data["commitmentRenewalBillingPlanType"] != null:
+ var enum_str = data["commitmentRenewalBillingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.commitment_renewal_billing_plan_type = enum_str
+ if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null:
+ obj.commitment_renewal_date = data["commitmentRenewalDate"]
+ if data.has("commitmentRenewalPrice") and data["commitmentRenewalPrice"] != null:
+ obj.commitment_renewal_price = data["commitmentRenewalPrice"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["commitmentAutoRenewProductId"] = commitment_auto_renew_product_id
+ dict["commitmentAutoRenewStatus"] = commitment_auto_renew_status
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(commitment_renewal_billing_plan_type):
+ dict["commitmentRenewalBillingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[commitment_renewal_billing_plan_type]
+ else:
+ dict["commitmentRenewalBillingPlanType"] = commitment_renewal_billing_plan_type
+ dict["commitmentRenewalDate"] = commitment_renewal_date
+ dict["commitmentRenewalPrice"] = commitment_renewal_price
+ return dict
+
## Subscription renewal information from Product.SubscriptionInfo.RenewalInfo https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
class RenewalInfoIOS:
var json_representation: Variant = null
@@ -2617,6 +2726,10 @@ class RenewalInfoIOS:
var renewal_offer_id: Variant = null
## Type of offer applied to next renewal
var renewal_offer_type: Variant = null
+ ## iOS 26.4+ billing plan that will renew after the current period.
+ var renewal_billing_plan_type: SubscriptionBillingPlanTypeIOS
+ ## iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ var commitment_info: RenewalCommitmentInfoIOS
static func from_dict(data: Dictionary) -> RenewalInfoIOS:
var obj = RenewalInfoIOS.new()
@@ -2642,6 +2755,17 @@ class RenewalInfoIOS:
obj.renewal_offer_id = data["renewalOfferId"]
if data.has("renewalOfferType") and data["renewalOfferType"] != null:
obj.renewal_offer_type = data["renewalOfferType"]
+ if data.has("renewalBillingPlanType") and data["renewalBillingPlanType"] != null:
+ var enum_str = data["renewalBillingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.renewal_billing_plan_type = enum_str
+ if data.has("commitmentInfo") and data["commitmentInfo"] != null:
+ if data["commitmentInfo"] is Dictionary:
+ obj.commitment_info = RenewalCommitmentInfoIOS.from_dict(data["commitmentInfo"])
+ else:
+ obj.commitment_info = data["commitmentInfo"]
return obj
func to_dict() -> Dictionary:
@@ -2667,6 +2791,14 @@ class RenewalInfoIOS:
dict["renewalOfferId"] = renewal_offer_id
if renewal_offer_type != null:
dict["renewalOfferType"] = renewal_offer_type
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(renewal_billing_plan_type):
+ dict["renewalBillingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[renewal_billing_plan_type]
+ else:
+ dict["renewalBillingPlanType"] = renewal_billing_plan_type
+ if commitment_info != null and commitment_info.has_method("to_dict"):
+ dict["commitmentInfo"] = commitment_info.to_dict()
+ else:
+ dict["commitmentInfo"] = commitment_info
return dict
## Rental details for one-time purchase products that can be rented (Android) Available in Google Play Billing Library 7.0+
@@ -2729,8 +2861,37 @@ class RequestVerifyPurchaseWithIapkitResult:
dict["state"] = state
return dict
+class SubscriptionCommitmentInfoIOS:
+ var display_price: String = ""
+ var period: SubscriptionPeriodValueIOS
+ var price: float = 0.0
+
+ static func from_dict(data: Dictionary) -> SubscriptionCommitmentInfoIOS:
+ var obj = SubscriptionCommitmentInfoIOS.new()
+ if data.has("displayPrice") and data["displayPrice"] != null:
+ obj.display_price = data["displayPrice"]
+ 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("price") and data["price"] != null:
+ obj.price = data["price"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["displayPrice"] = display_price
+ if period != null and period.has_method("to_dict"):
+ dict["period"] = period.to_dict()
+ else:
+ dict["period"] = period
+ dict["price"] = price
+ return dict
+
class SubscriptionInfoIOS:
var introductory_offer: SubscriptionOfferIOS
+ var pricing_terms: Array[SubscriptionPricingTermsIOS] = []
var promotional_offers: Array[SubscriptionOfferIOS] = []
var subscription_group_id: String = ""
var subscription_period: SubscriptionPeriodValueIOS
@@ -2742,6 +2903,14 @@ class SubscriptionInfoIOS:
obj.introductory_offer = SubscriptionOfferIOS.from_dict(data["introductoryOffer"])
else:
obj.introductory_offer = data["introductoryOffer"]
+ if data.has("pricingTerms") and data["pricingTerms"] != null:
+ var arr = []
+ for item in data["pricingTerms"]:
+ if item is Dictionary:
+ arr.append(SubscriptionPricingTermsIOS.from_dict(item))
+ else:
+ arr.append(item)
+ obj.pricing_terms = arr
if data.has("promotionalOffers") and data["promotionalOffers"] != null:
var arr = []
for item in data["promotionalOffers"]:
@@ -2765,6 +2934,16 @@ class SubscriptionInfoIOS:
dict["introductoryOffer"] = introductory_offer.to_dict()
else:
dict["introductoryOffer"] = introductory_offer
+ if pricing_terms != null:
+ var arr = []
+ for item in pricing_terms:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["pricingTerms"] = arr
+ else:
+ dict["pricingTerms"] = null
if promotional_offers != null:
var arr = []
for item in promotional_offers:
@@ -3042,6 +3221,74 @@ class SubscriptionPeriodValueIOS:
dict["value"] = value
return dict
+class SubscriptionPricingTermsIOS:
+ var billing_display_price: String = ""
+ var billing_period: SubscriptionPeriodValueIOS
+ var billing_plan_type: SubscriptionBillingPlanTypeIOS
+ var billing_price: float = 0.0
+ var commitment_info: SubscriptionCommitmentInfoIOS
+ var subscription_offers: Array[SubscriptionOffer] = []
+
+ static func from_dict(data: Dictionary) -> SubscriptionPricingTermsIOS:
+ var obj = SubscriptionPricingTermsIOS.new()
+ if data.has("billingDisplayPrice") and data["billingDisplayPrice"] != null:
+ obj.billing_display_price = data["billingDisplayPrice"]
+ if data.has("billingPeriod") and data["billingPeriod"] != null:
+ if data["billingPeriod"] is Dictionary:
+ obj.billing_period = SubscriptionPeriodValueIOS.from_dict(data["billingPeriod"])
+ else:
+ obj.billing_period = data["billingPeriod"]
+ if data.has("billingPlanType") and data["billingPlanType"] != null:
+ var enum_str = data["billingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.billing_plan_type = enum_str
+ if data.has("billingPrice") and data["billingPrice"] != null:
+ obj.billing_price = data["billingPrice"]
+ if data.has("commitmentInfo") and data["commitmentInfo"] != null:
+ if data["commitmentInfo"] is Dictionary:
+ obj.commitment_info = SubscriptionCommitmentInfoIOS.from_dict(data["commitmentInfo"])
+ else:
+ obj.commitment_info = data["commitmentInfo"]
+ if data.has("subscriptionOffers") and data["subscriptionOffers"] != null:
+ var arr = []
+ for item in data["subscriptionOffers"]:
+ if item is Dictionary:
+ arr.append(SubscriptionOffer.from_dict(item))
+ else:
+ arr.append(item)
+ obj.subscription_offers = arr
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["billingDisplayPrice"] = billing_display_price
+ if billing_period != null and billing_period.has_method("to_dict"):
+ dict["billingPeriod"] = billing_period.to_dict()
+ else:
+ dict["billingPeriod"] = billing_period
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(billing_plan_type):
+ dict["billingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type]
+ else:
+ dict["billingPlanType"] = billing_plan_type
+ dict["billingPrice"] = billing_price
+ if commitment_info != null and commitment_info.has_method("to_dict"):
+ dict["commitmentInfo"] = commitment_info.to_dict()
+ else:
+ dict["commitmentInfo"] = commitment_info
+ if subscription_offers != null:
+ var arr = []
+ for item in subscription_offers:
+ if item != null and item.has_method("to_dict"):
+ arr.append(item.to_dict())
+ else:
+ arr.append(item)
+ dict["subscriptionOffers"] = arr
+ else:
+ dict["subscriptionOffers"] = null
+ return dict
+
class SubscriptionStatusIOS:
var state: String = ""
var renewal_info: RenewalInfoIOS
@@ -3066,6 +3313,32 @@ class SubscriptionStatusIOS:
dict["renewalInfo"] = renewal_info
return dict
+class TransactionCommitmentInfoIOS:
+ var billing_period_number: int = 0
+ var commitment_expires_date: float = 0.0
+ var commitment_price: float = 0.0
+ var total_billing_periods: int = 0
+
+ static func from_dict(data: Dictionary) -> TransactionCommitmentInfoIOS:
+ var obj = TransactionCommitmentInfoIOS.new()
+ if data.has("billingPeriodNumber") and data["billingPeriodNumber"] != null:
+ obj.billing_period_number = data["billingPeriodNumber"]
+ if data.has("commitmentExpiresDate") and data["commitmentExpiresDate"] != null:
+ obj.commitment_expires_date = data["commitmentExpiresDate"]
+ if data.has("commitmentPrice") and data["commitmentPrice"] != null:
+ obj.commitment_price = data["commitmentPrice"]
+ if data.has("totalBillingPeriods") and data["totalBillingPeriods"] != null:
+ obj.total_billing_periods = data["totalBillingPeriods"]
+ return obj
+
+ func to_dict() -> Dictionary:
+ var dict = {}
+ dict["billingPeriodNumber"] = billing_period_number
+ dict["commitmentExpiresDate"] = commitment_expires_date
+ dict["commitmentPrice"] = commitment_price
+ dict["totalBillingPeriods"] = total_billing_periods
+ return dict
+
## User Choice Billing event details (Android) Fired when a user selects alternative billing in the User Choice Billing dialog
class UserChoiceBillingDetails:
## Token that must be reported to Google Play within 24 hours
@@ -4187,6 +4460,8 @@ class RequestSubscriptionIosProps:
var win_back_offer: WinBackOfferInputIOS
## JWS promotional offer (iOS 15+, WWDC 2025).
var promotional_offer_jws: PromotionalOfferJWSInputIOS
+ ## Billing plan to use when purchasing an annual subscription that offers
+ var billing_plan_type: SubscriptionBillingPlanTypeIOS
## Override introductory offer eligibility (iOS 15+, WWDC 2025).
var introductory_offer_eligibility: Variant = null
## Advanced commerce data token (iOS 15+).
@@ -4217,6 +4492,12 @@ class RequestSubscriptionIosProps:
obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"])
else:
obj.promotional_offer_jws = data["promotionalOfferJWS"]
+ if data.has("billingPlanType") and data["billingPlanType"] != null:
+ var enum_str = data["billingPlanType"]
+ if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ else:
+ obj.billing_plan_type = enum_str
if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
obj.introductory_offer_eligibility = data["introductoryOfferEligibility"]
if data.has("advancedCommerceData") and data["advancedCommerceData"] != null:
@@ -4248,6 +4529,11 @@ class RequestSubscriptionIosProps:
dict["promotionalOfferJWS"] = promotional_offer_jws.to_dict()
else:
dict["promotionalOfferJWS"] = promotional_offer_jws
+ if billing_plan_type != null:
+ if SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES.has(billing_plan_type):
+ dict["billingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type]
+ else:
+ dict["billingPlanType"] = billing_plan_type
if introductory_offer_eligibility != null:
dict["introductoryOfferEligibility"] = introductory_offer_eligibility
if advanced_commerce_data != null:
@@ -4786,6 +5072,12 @@ const SUB_RESPONSE_CODE_ANDROID_VALUES = {
SubResponseCodeAndroid.USER_INELIGIBLE: "user-ineligible"
}
+const SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES = {
+ SubscriptionBillingPlanTypeIOS.UNKNOWN: "unknown",
+ SubscriptionBillingPlanTypeIOS.MONTHLY: "monthly",
+ SubscriptionBillingPlanTypeIOS.UP_FRONT: "up-front"
+}
+
const SUBSCRIPTION_OFFER_TYPE_IOS_VALUES = {
SubscriptionOfferTypeIOS.INTRODUCTORY: "introductory",
SubscriptionOfferTypeIOS.PROMOTIONAL: "promotional",
@@ -5055,6 +5347,12 @@ const SUB_RESPONSE_CODE_ANDROID_FROM_STRING = {
"user-ineligible": SubResponseCodeAndroid.USER_INELIGIBLE
}
+const SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING = {
+ "unknown": SubscriptionBillingPlanTypeIOS.UNKNOWN,
+ "monthly": SubscriptionBillingPlanTypeIOS.MONTHLY,
+ "up-front": SubscriptionBillingPlanTypeIOS.UP_FRONT
+}
+
const SUBSCRIPTION_OFFER_TYPE_IOS_FROM_STRING = {
"introductory": SubscriptionOfferTypeIOS.INTRODUCTORY,
"promotional": SubscriptionOfferTypeIOS.PROMOTIONAL,
diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts
index 518f5ffde..e62ef2b05 100644
--- a/packages/gql/src/generated/types.ts
+++ b/packages/gql/src/generated/types.ts
@@ -985,6 +985,11 @@ export interface ProductIOS extends ProductCommon {
jsonRepresentationIOS: string;
platform: 'ios';
price?: (number | null);
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ pricingTermsIOS?: (SubscriptionPricingTermsIOS[] | null);
/**
* @deprecated Use subscriptionOffers instead for cross-platform compatibility.
* @deprecated Use subscriptionOffers instead
@@ -1108,6 +1113,11 @@ export interface ProductSubscriptionIOS extends ProductCommon {
jsonRepresentationIOS: string;
platform: 'ios';
price?: (number | null);
+ /**
+ * iOS 26.4+ subscription pricing terms, including billing plan metadata for
+ * monthly subscriptions with a 12-month commitment.
+ */
+ pricingTermsIOS?: (SubscriptionPricingTermsIOS[] | null);
/** App Store subscription group identifier for intro-offer eligibility checks. */
subscriptionGroupIdIOS?: (string | null);
/**
@@ -1234,6 +1244,10 @@ export interface PurchaseIOS extends PurchaseCommon {
advancedCommerceInfoIOS?: (AdvancedCommerceInfoIOS | null);
appAccountToken?: (string | null);
appBundleIdIOS?: (string | null);
+ /** iOS 26.4+ billing plan selected for this transaction. */
+ billingPlanTypeIOS?: (SubscriptionBillingPlanTypeIOS | null);
+ /** iOS 26.4+ progress information for monthly subscriptions with a 12-month commitment. */
+ commitmentInfoIOS?: (TransactionCommitmentInfoIOS | null);
countryCodeIOS?: (string | null);
currencyCodeIOS?: (string | null);
currencySymbolIOS?: (string | null);
@@ -1454,12 +1468,25 @@ export interface RefundResultIOS {
status: string;
}
+export interface RenewalCommitmentInfoIOS {
+ commitmentAutoRenewProductId: string;
+ commitmentAutoRenewStatus: boolean;
+ commitmentRenewalBillingPlanType: SubscriptionBillingPlanTypeIOS;
+ commitmentRenewalDate: number;
+ commitmentRenewalPrice: number;
+}
+
/**
* Subscription renewal information from Product.SubscriptionInfo.RenewalInfo
* https://developer.apple.com/documentation/storekit/product/subscriptioninfo/renewalinfo
*/
export interface RenewalInfoIOS {
autoRenewPreference?: (string | null);
+ /**
+ * iOS 26.4+ renewal commitment metadata for monthly subscriptions with a
+ * 12-month commitment.
+ */
+ commitmentInfo?: (RenewalCommitmentInfoIOS | null);
/**
* When subscription expires due to cancellation/billing issue
* Possible values: "VOLUNTARY", "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", "PRODUCT_NOT_AVAILABLE", "UNKNOWN"
@@ -1486,6 +1513,8 @@ export interface RenewalInfoIOS {
* Possible values: "AGREED", "PENDING", null (no price increase)
*/
priceIncreaseStatus?: (string | null);
+ /** iOS 26.4+ billing plan that will renew after the current period. */
+ renewalBillingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
* Expected renewal date (milliseconds since epoch)
* For active subscriptions, when the next renewal/charge will occur
@@ -1645,6 +1674,11 @@ export interface RequestSubscriptionIosProps {
advancedCommerceData?: (string | null);
andDangerouslyFinishTransactionAutomatically?: (boolean | null);
appAccountToken?: (string | null);
+ /**
+ * Billing plan to use when purchasing an annual subscription that offers
+ * monthly billing with a 12-month commitment (iOS 26.4+).
+ */
+ billingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -1777,8 +1811,17 @@ export interface Subscription {
export type SubscriptionPurchaseUpdatedArgs = (PurchaseUpdatedListenerOptions | null) | undefined;
+export type SubscriptionBillingPlanTypeIOS = 'unknown' | 'monthly' | 'up-front';
+
+export interface SubscriptionCommitmentInfoIOS {
+ displayPrice: string;
+ period: SubscriptionPeriodValueIOS;
+ price: number;
+}
+
export interface SubscriptionInfoIOS {
introductoryOffer?: (SubscriptionOfferIOS | null);
+ pricingTerms?: (SubscriptionPricingTermsIOS[] | null);
promotionalOffers?: (SubscriptionOfferIOS[] | null);
subscriptionGroupId: string;
subscriptionPeriod: SubscriptionPeriodValueIOS;
@@ -1900,6 +1943,15 @@ export interface SubscriptionPeriodValueIOS {
value: number;
}
+export interface SubscriptionPricingTermsIOS {
+ billingDisplayPrice: string;
+ billingPeriod: SubscriptionPeriodValueIOS;
+ billingPlanType: SubscriptionBillingPlanTypeIOS;
+ billingPrice: number;
+ commitmentInfo: SubscriptionCommitmentInfoIOS;
+ subscriptionOffers?: (SubscriptionOffer[] | null);
+}
+
/**
* Product-level subscription replacement parameters (Android)
* Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
@@ -1926,6 +1978,13 @@ export interface SubscriptionStatusIOS {
state: string;
}
+export interface TransactionCommitmentInfoIOS {
+ billingPeriodNumber: number;
+ commitmentExpiresDate: number;
+ commitmentPrice: number;
+ totalBillingPeriods: number;
+}
+
/**
* User Choice Billing event details (Android)
* Fired when a user selects alternative billing in the User Choice Billing dialog
From f497cebbd07d1e29796dfe7d6bf380daf6e6e00e Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:16:04 +0900
Subject: [PATCH 3/8] feat(apple): support ios billing plan purchases
---
.../ApiDefinition.cs | 3 +-
.../Sources/Helpers/StoreKitTypesBridge.swift | 154 ++++++++++++++++++
.../apple/Sources/OpenIapModule+ObjC.swift | 8 +
packages/apple/Sources/OpenIapModule.swift | 3 +
packages/apple/Sources/OpenIapStore.swift | 4 +-
packages/apple/Tests/OpenIapTests.swift | 5 +
6 files changed, 175 insertions(+), 2 deletions(-)
diff --git a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs
index fb06f43b5..54b8fda7d 100644
--- a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs
+++ b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs
@@ -56,7 +56,7 @@ interface OpenIapModule
[Async]
void RequestSubscription(string sku, [NullAllowed] NSDictionary offer, Action completion);
- [Export("requestSubscriptionWithSku:offer:introductoryOfferEligibility:promotionalOfferJWS:winBackOfferId:completion:")]
+ [Export("requestSubscriptionWithSku:offer:introductoryOfferEligibility:promotionalOfferJWS:winBackOfferId:billingPlanType:completion:")]
[Async]
void RequestSubscriptionExtended(
string sku,
@@ -64,6 +64,7 @@ void RequestSubscriptionExtended(
[NullAllowed] NSNumber introductoryOfferEligibility,
[NullAllowed] NSDictionary promotionalOfferJWS,
[NullAllowed] string winBackOfferId,
+ [NullAllowed] string billingPlanType,
Action completion);
[Export("restorePurchasesWithCompletion:")]
diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
index 0844c818a..a110db8ba 100644
--- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
+++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
@@ -27,6 +27,7 @@ enum StoreKitTypesBridge {
jsonRepresentationIOS: String(data: product.jsonRepresentation, encoding: .utf8) ?? "",
platform: .ios,
price: NSDecimalNumber(decimal: product.price).doubleValue,
+ pricingTermsIOS: makeSubscriptionPricingTerms(from: product.subscription),
subscriptionInfoIOS: makeSubscriptionInfo(from: product.subscription),
title: product.displayName,
type: productType(from: product.type),
@@ -92,6 +93,7 @@ enum StoreKitTypesBridge {
jsonRepresentationIOS: String(data: product.jsonRepresentation, encoding: .utf8) ?? "",
platform: .ios,
price: NSDecimalNumber(decimal: product.price).doubleValue,
+ pricingTermsIOS: makeSubscriptionPricingTerms(from: subscription),
subscriptionGroupIdIOS: subscription.subscriptionGroupID,
subscriptionInfoIOS: makeSubscriptionInfo(from: product.subscription),
subscriptionOffers: standardizedOffers.isEmpty ? nil : standardizedOffers,
@@ -136,6 +138,8 @@ enum StoreKitTypesBridge {
advancedCommerceInfoIOS: advancedCommerceInfo,
appAccountToken: transaction.appAccountToken?.uuidString,
appBundleIdIOS: transaction.appBundleID,
+ billingPlanTypeIOS: billingPlanTypeIOS(from: transaction),
+ commitmentInfoIOS: transactionCommitmentInfoIOS(from: transaction),
countryCodeIOS: {
if #available(iOS 17.0, tvOS 17.0, watchOS 10.0, *) {
transaction.storefront.countryCode
@@ -292,12 +296,14 @@ enum StoreKitTypesBridge {
}()
let renewalInfo = RenewalInfoIOS(
autoRenewPreference: info.autoRenewPreference,
+ commitmentInfo: renewalCommitmentInfoIOS(from: info),
expirationReason: info.expirationReason?.rawValue.description,
gracePeriodExpirationDate: info.gracePeriodExpirationDate?.milliseconds,
isInBillingRetry: nil, // Not available in RenewalInfo, available in Status
jsonRepresentation: nil,
pendingUpgradeProductId: pendingProductId,
priceIncreaseStatus: priceIncrease,
+ renewalBillingPlanType: renewalBillingPlanTypeIOS(from: info),
renewalDate: info.renewalDate?.milliseconds,
renewalOfferId: offerInfo?.id,
renewalOfferType: offerInfo?.type,
@@ -344,12 +350,14 @@ enum StoreKitTypesBridge {
}()
let renewalInfo = RenewalInfoIOS(
autoRenewPreference: info.autoRenewPreference,
+ commitmentInfo: renewalCommitmentInfoIOS(from: info),
expirationReason: info.expirationReason?.rawValue.description,
gracePeriodExpirationDate: info.gracePeriodExpirationDate?.milliseconds,
isInBillingRetry: nil, // Not available in RenewalInfo, available in Status
jsonRepresentation: nil,
pendingUpgradeProductId: pendingProductId,
priceIncreaseStatus: priceIncrease,
+ renewalBillingPlanType: renewalBillingPlanTypeIOS(from: info),
renewalDate: info.renewalDate?.milliseconds,
renewalOfferId: offerInfo?.id,
renewalOfferType: offerInfo?.type,
@@ -398,6 +406,38 @@ enum StoreKitTypesBridge {
// Subscription-only options (only available on RequestSubscriptionIosProps)
if let subscriptionProps = props as? RequestSubscriptionIosProps {
+ // Billing plan selection for annual subscriptions with monthly commitment (iOS 26.4+)
+ if let billingPlanType = subscriptionProps.billingPlanType {
+ #if swift(>=6.3)
+ if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
+ switch billingPlanType {
+ case .monthly:
+ options.insert(.billingPlanType(.monthly))
+ case .upFront:
+ options.insert(.billingPlanType(.upFront))
+ case .unknown:
+ throw PurchaseError.make(
+ code: .developerError,
+ productId: props.sku,
+ message: "billingPlanType must be monthly or up-front."
+ )
+ }
+ } else {
+ throw PurchaseError.make(
+ code: .featureNotSupported,
+ productId: props.sku,
+ message: "billingPlanType requires iOS 26.4+ / macOS 26.4+ / tvOS 26.4+ / watchOS 26.4+ / visionOS 26.4+."
+ )
+ }
+ #else
+ throw PurchaseError.make(
+ code: .featureNotSupported,
+ productId: props.sku,
+ message: "billingPlanType requires Xcode 26.4+ / Swift 6.3+."
+ )
+ #endif
+ }
+
// Win-back offers (iOS 18+)
// Used to re-engage churned subscribers
if let winBackInput = subscriptionProps.winBackOffer {
@@ -519,6 +559,31 @@ enum StoreKitTypesBridge {
}
#endif
}
+
+ static func renewalCommitmentInfoIOS(from info: StoreKit.Product.SubscriptionInfo.RenewalInfo) -> RenewalCommitmentInfoIOS? {
+ #if swift(>=6.3)
+ if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
+ guard let commitment = info.commitmentInfo else { return nil }
+ return RenewalCommitmentInfoIOS(
+ commitmentAutoRenewProductId: commitment.autoRenewPreference,
+ commitmentAutoRenewStatus: commitment.willAutoRenew,
+ commitmentRenewalBillingPlanType: billingPlanTypeIOS(from: commitment.renewalBillingPlanType),
+ commitmentRenewalDate: commitment.renewalDate.milliseconds,
+ commitmentRenewalPrice: NSDecimalNumber(decimal: commitment.renewalPrice).doubleValue
+ )
+ }
+ #endif
+ return nil
+ }
+
+ static func renewalBillingPlanTypeIOS(from info: StoreKit.Product.SubscriptionInfo.RenewalInfo) -> SubscriptionBillingPlanTypeIOS? {
+ #if swift(>=6.3)
+ if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
+ return info.renewalBillingPlanType.map { billingPlanTypeIOS(from: $0) }
+ }
+ #endif
+ return nil
+ }
}
@available(iOS 15.0, macOS 14.0, tvOS 16.0, watchOS 8.0, *)
@@ -529,6 +594,7 @@ private extension StoreKitTypesBridge {
let promos = makeSubscriptionOffers(from: info.promotionalOffers, type: .promotional)
return SubscriptionInfoIOS(
introductoryOffer: intro,
+ pricingTerms: makeSubscriptionPricingTerms(from: info),
promotionalOffers: promos.isEmpty ? nil : promos,
subscriptionGroupId: info.subscriptionGroupID,
subscriptionPeriod: SubscriptionPeriodValueIOS(
@@ -557,6 +623,85 @@ private extension StoreKitTypesBridge {
)
}
+ static func makeSubscriptionPricingTerms(from info: StoreKit.Product.SubscriptionInfo?) -> [SubscriptionPricingTermsIOS]? {
+ guard let info else { return nil }
+ #if swift(>=6.3)
+ if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
+ let terms = info.pricingTerms.map { makeSubscriptionPricingTerm(from: $0) }
+ return terms.isEmpty ? nil : terms
+ }
+ #endif
+ return nil
+ }
+
+ #if swift(>=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))
+ }
+ return SubscriptionPricingTermsIOS(
+ billingDisplayPrice: terms.billingDisplayPrice,
+ billingPeriod: SubscriptionPeriodValueIOS(
+ unit: terms.billingPeriod.unit.subscriptionPeriodIOS,
+ value: terms.billingPeriod.value
+ ),
+ billingPlanType: billingPlanTypeIOS(from: terms.billingPlanType),
+ billingPrice: NSDecimalNumber(decimal: terms.billingPrice).doubleValue,
+ commitmentInfo: SubscriptionCommitmentInfoIOS(
+ displayPrice: terms.commitmentInfo.displayPrice,
+ period: SubscriptionPeriodValueIOS(
+ unit: terms.commitmentInfo.period.unit.subscriptionPeriodIOS,
+ value: terms.commitmentInfo.period.value
+ ),
+ price: NSDecimalNumber(decimal: terms.commitmentInfo.price).doubleValue
+ ),
+ subscriptionOffers: offers.isEmpty ? nil : offers
+ )
+ }
+ #endif
+
+ #if swift(>=6.3)
+ @available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *)
+ static func billingPlanTypeIOS(from type: StoreKit.Product.SubscriptionInfo.BillingPlanType) -> SubscriptionBillingPlanTypeIOS {
+ if type == .monthly {
+ return .monthly
+ }
+ if type == .upFront {
+ return .upFront
+ }
+ return SubscriptionBillingPlanTypeIOS(rawValue: type.rawValue) ?? .unknown
+ }
+ #endif
+
+ static func billingPlanTypeIOS(from transaction: StoreKit.Transaction) -> SubscriptionBillingPlanTypeIOS? {
+ #if swift(>=6.3)
+ if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
+ return transaction.billingPlanType.map { billingPlanTypeIOS(from: $0) }
+ }
+ #endif
+ return nil
+ }
+
+ static func transactionCommitmentInfoIOS(from transaction: StoreKit.Transaction) -> TransactionCommitmentInfoIOS? {
+ #if swift(>=6.3)
+ if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
+ guard let commitment = transaction.commitmentInfo else { return nil }
+ return TransactionCommitmentInfoIOS(
+ billingPeriodNumber: intClamping(commitment.billingPeriodNumber),
+ commitmentExpiresDate: commitment.expirationDate.milliseconds,
+ commitmentPrice: NSDecimalNumber(decimal: commitment.price).doubleValue,
+ totalBillingPeriods: intClamping(commitment.totalBillingPeriods)
+ )
+ }
+ #endif
+ return nil
+ }
+
+ static func intClamping(_ value: UInt64) -> Int {
+ value > UInt64(Int.max) ? Int.max : Int(value)
+ }
+
/// Converts StoreKit subscription offers to standardized cross-platform SubscriptionOffer type.
/// This is the new format that works across iOS and Android.
static func makeStandardizedSubscriptionOffers(from subscription: StoreKit.Product.SubscriptionInfo) -> [SubscriptionOffer] {
@@ -602,6 +747,15 @@ private extension StoreKitTypesBridge {
)
}
+ static func discountOfferType(from offer: StoreKit.Product.SubscriptionOffer) -> DiscountOfferType {
+ #if swift(>=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 makeDiscounts(from subscription: StoreKit.Product.SubscriptionInfo, product: StoreKit.Product) -> [DiscountIOS]? {
var discounts: [DiscountIOS] = []
diff --git a/packages/apple/Sources/OpenIapModule+ObjC.swift b/packages/apple/Sources/OpenIapModule+ObjC.swift
index ef2b2d7e3..3b60deca3 100644
--- a/packages/apple/Sources/OpenIapModule+ObjC.swift
+++ b/packages/apple/Sources/OpenIapModule+ObjC.swift
@@ -202,6 +202,7 @@ import StoreKit
introductoryOfferEligibility: nil,
promotionalOfferJWS: nil,
winBackOfferId: nil,
+ billingPlanType: nil,
completion: completion
)
}
@@ -213,6 +214,7 @@ import StoreKit
/// - introductoryOfferEligibility: Override introductory offer eligibility (iOS 15+, WWDC 2025)
/// - promotionalOfferJWS: JWS promotional offer dict with "offerId" and "jws" keys (iOS 15+, WWDC 2025)
/// - winBackOfferId: Win-back offer ID (iOS 18+)
+ /// - billingPlanType: Billing plan type ("monthly" or "up-front", iOS 26.4+)
/// - completion: Completion handler
@objc func requestSubscriptionWithSku(
_ sku: String,
@@ -220,6 +222,7 @@ import StoreKit
introductoryOfferEligibility: NSNumber?,
promotionalOfferJWS: [String: Any]?,
winBackOfferId: String?,
+ billingPlanType: String?,
completion: @escaping (Any?, Error?) -> Void
) {
Task {
@@ -258,9 +261,14 @@ import StoreKit
nil
}
+ let billingPlan = billingPlanType.map {
+ SubscriptionBillingPlanTypeIOS(rawValue: $0) ?? .unknown
+ }
+
let iosProps = RequestSubscriptionIosProps(
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
+ billingPlanType: billingPlan,
introductoryOfferEligibility: introductoryOfferEligibility?.boolValue,
promotionalOfferJWS: jwsOffer,
sku: sku,
diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift
index a44190578..7c98858c5 100644
--- a/packages/apple/Sources/OpenIapModule.swift
+++ b/packages/apple/Sources/OpenIapModule.swift
@@ -195,6 +195,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
jsonRepresentationIOS: String(data: product.jsonRepresentation, encoding: .utf8) ?? "",
platform: .ios,
price: NSDecimalNumber(decimal: product.price).doubleValue,
+ pricingTermsIOS: nil,
subscriptionGroupIdIOS: nil,
subscriptionInfoIOS: nil, // StoreKit: Non-renewing subscriptions have no subscription metadata
subscriptionPeriodNumberIOS: nil,
@@ -1029,7 +1030,9 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol {
let jsonString = String(data: info.jsonRepresentation, encoding: .utf8) ?? info.jsonRepresentation.base64EncodedString()
renewalInfo = RenewalInfoIOS(
autoRenewPreference: info.autoRenewPreference,
+ commitmentInfo: StoreKitTypesBridge.renewalCommitmentInfoIOS(from: info),
jsonRepresentation: jsonString,
+ renewalBillingPlanType: StoreKitTypesBridge.renewalBillingPlanTypeIOS(from: info),
willAutoRenew: info.willAutoRenew
)
case .unverified:
diff --git a/packages/apple/Sources/OpenIapStore.swift b/packages/apple/Sources/OpenIapStore.swift
index f710f5e5f..65ac2361c 100644
--- a/packages/apple/Sources/OpenIapStore.swift
+++ b/packages/apple/Sources/OpenIapStore.swift
@@ -259,7 +259,8 @@ public final class OpenIapStore: ObservableObject {
quantity: Int? = nil,
appAccountToken: String? = nil,
withOffer: DiscountOfferInputIOS? = nil,
- advancedCommerceData: String? = nil
+ advancedCommerceData: String? = nil,
+ billingPlanType: SubscriptionBillingPlanTypeIOS? = nil
) async throws -> OpenIAP.Purchase? {
switch type {
case .subs:
@@ -267,6 +268,7 @@ public final class OpenIapStore: ObservableObject {
advancedCommerceData: advancedCommerceData,
andDangerouslyFinishTransactionAutomatically: autoFinish,
appAccountToken: appAccountToken,
+ billingPlanType: billingPlanType,
quantity: quantity,
sku: sku,
withOffer: withOffer
diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift
index 83b0d202e..705a85882 100644
--- a/packages/apple/Tests/OpenIapTests.swift
+++ b/packages/apple/Tests/OpenIapTests.swift
@@ -627,6 +627,7 @@ final class OpenIapTests: XCTestCase {
advancedCommerceData: "campaign_data",
andDangerouslyFinishTransactionAutomatically: false,
appAccountToken: "user-uuid-123",
+ billingPlanType: .monthly,
introductoryOfferEligibility: true,
promotionalOfferJWS: jwsOffer,
quantity: 1,
@@ -641,6 +642,7 @@ final class OpenIapTests: XCTestCase {
XCTAssertEqual(props.appAccountToken, "user-uuid-123")
XCTAssertEqual(props.advancedCommerceData, "campaign_data")
XCTAssertEqual(props.andDangerouslyFinishTransactionAutomatically, false)
+ XCTAssertEqual(props.billingPlanType, .monthly)
XCTAssertEqual(props.introductoryOfferEligibility, true)
XCTAssertEqual(props.winBackOffer?.offerId, "winback_offer")
XCTAssertEqual(props.promotionalOfferJWS?.offerId, "promo_offer")
@@ -651,6 +653,7 @@ final class OpenIapTests: XCTestCase {
let decoded = try JSONDecoder().decode(RequestSubscriptionIosProps.self, from: data)
XCTAssertEqual(decoded.sku, props.sku)
+ XCTAssertEqual(decoded.billingPlanType, props.billingPlanType)
XCTAssertEqual(decoded.introductoryOfferEligibility, props.introductoryOfferEligibility)
XCTAssertEqual(decoded.winBackOffer?.offerId, props.winBackOffer?.offerId)
XCTAssertEqual(decoded.promotionalOfferJWS?.offerId, props.promotionalOfferJWS?.offerId)
@@ -662,6 +665,7 @@ final class OpenIapTests: XCTestCase {
advancedCommerceData: nil,
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
+ billingPlanType: .upFront,
introductoryOfferEligibility: true,
promotionalOfferJWS: PromotionalOfferJWSInputIOS(jws: "jws", offerId: "offer"),
quantity: nil,
@@ -672,6 +676,7 @@ final class OpenIapTests: XCTestCase {
// These fields exist on RequestSubscriptionIosProps
XCTAssertEqual(subscriptionProps.introductoryOfferEligibility, true)
+ XCTAssertEqual(subscriptionProps.billingPlanType, .upFront)
XCTAssertNotNil(subscriptionProps.promotionalOfferJWS)
XCTAssertNotNil(subscriptionProps.winBackOffer)
From 921488fd772693354ffae07f44be6bf743724358 Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:16:15 +0900
Subject: [PATCH 4/8] feat(sdk): wire ios billing plan option
---
.../expo-iap/src/__tests__/index.test.ts | 2 +
.../flutter_inapp_purchase/lib/builders.dart | 23 ++++++++
.../flutter_inapp_purchase/lib/utils.dart | 5 ++
.../test/builders_unit_test.dart | 16 ++++-
.../flutter_inapp_purchase_channel_test.dart | 2 +
.../godot-iap/addons/godot-iap/godot_iap.gd | 11 +++-
.../Sources/GodotIap/GodotIap.swift | 58 +++++++++++++++++++
.../github/hyochan/kmpiap/dsl/PurchaseDsl.kt | 6 ++
.../hyochan/kmpiap/InAppPurchaseTest.kt | 4 +-
.../github/hyochan/kmpiap/InAppPurchaseIOS.kt | 45 ++++++++++++++
.../react-native-iap/ios/HybridRnIap.swift | 3 +
.../src/__tests__/index.test.ts | 2 +
libraries/react-native-iap/src/index.ts | 3 +
.../react-native-iap/src/specs/RnIap.nitro.ts | 7 +++
14 files changed, 182 insertions(+), 5 deletions(-)
diff --git a/libraries/expo-iap/src/__tests__/index.test.ts b/libraries/expo-iap/src/__tests__/index.test.ts
index cb614aaf0..807389842 100644
--- a/libraries/expo-iap/src/__tests__/index.test.ts
+++ b/libraries/expo-iap/src/__tests__/index.test.ts
@@ -872,6 +872,7 @@ describe('Public API (index.ts)', () => {
request: {
apple: {
sku: 'com.example.subscription.monthly',
+ billingPlanType: 'monthly',
introductoryOfferEligibility: true,
promotionalOfferJWS: {
offerId: 'promo-offer',
@@ -890,6 +891,7 @@ describe('Public API (index.ts)', () => {
request: {
ios: {
sku: 'com.example.subscription.monthly',
+ billingPlanType: 'monthly',
introductoryOfferEligibility: true,
promotionalOfferJWS: {
offerId: 'promo-offer',
diff --git a/libraries/flutter_inapp_purchase/lib/builders.dart b/libraries/flutter_inapp_purchase/lib/builders.dart
index cc3296bf3..910cfadcc 100644
--- a/libraries/flutter_inapp_purchase/lib/builders.dart
+++ b/libraries/flutter_inapp_purchase/lib/builders.dart
@@ -9,6 +9,21 @@ class RequestPurchaseIosBuilder {
DiscountOfferInputIOS? withOffer;
String? advancedCommerceData;
+ /// Win-back offer to apply when this builder is used with subscriptions
+ /// (iOS 18+)
+ WinBackOfferInputIOS? winBackOffer;
+
+ /// JWS promotional offer when this builder is used with subscriptions
+ /// (iOS 15+, WWDC 2025)
+ PromotionalOfferJWSInputIOS? promotionalOfferJWS;
+
+ /// Billing plan for annual subscriptions with monthly commitment (iOS 26.4+)
+ SubscriptionBillingPlanTypeIOS? billingPlanType;
+
+ /// Override introductory offer eligibility when this builder is used with
+ /// subscriptions (iOS 15+)
+ bool? introductoryOfferEligibility;
+
RequestPurchaseIosBuilder();
RequestPurchaseIosProps build() {
@@ -39,6 +54,9 @@ class RequestSubscriptionIosBuilder {
/// JWS promotional offer (iOS 15+, WWDC 2025)
PromotionalOfferJWSInputIOS? promotionalOfferJWS;
+ /// Billing plan for annual subscriptions with monthly commitment (iOS 26.4+)
+ SubscriptionBillingPlanTypeIOS? billingPlanType;
+
/// Override introductory offer eligibility (iOS 15+)
bool? introductoryOfferEligibility;
@@ -53,6 +71,7 @@ class RequestSubscriptionIosBuilder {
quantity: quantity,
withOffer: withOffer,
advancedCommerceData: advancedCommerceData,
+ billingPlanType: billingPlanType,
winBackOffer: winBackOffer,
promotionalOfferJWS: promotionalOfferJWS,
introductoryOfferEligibility: introductoryOfferEligibility,
@@ -174,6 +193,10 @@ class RequestPurchaseBuilder {
quantity: iosProps.quantity,
withOffer: iosProps.withOffer,
advancedCommerceData: iosProps.advancedCommerceData,
+ billingPlanType: ios.billingPlanType,
+ winBackOffer: ios.winBackOffer,
+ promotionalOfferJWS: ios.promotionalOfferJWS,
+ introductoryOfferEligibility: ios.introductoryOfferEligibility,
);
final androidSub = androidProps == null
diff --git a/libraries/flutter_inapp_purchase/lib/utils.dart b/libraries/flutter_inapp_purchase/lib/utils.dart
index 98bd9feb4..fcd063a84 100644
--- a/libraries/flutter_inapp_purchase/lib/utils.dart
+++ b/libraries/flutter_inapp_purchase/lib/utils.dart
@@ -82,6 +82,11 @@ Map? buildIosPurchasePayload(
payload['advancedCommerceData'] = advancedCommerceData;
}
+ final String? billingPlanType = propsJson['billingPlanType'] as String?;
+ if (billingPlanType != null && billingPlanType.isNotEmpty) {
+ payload['billingPlanType'] = billingPlanType;
+ }
+
final dynamic introductoryOfferEligibility =
propsJson['introductoryOfferEligibility'];
if (introductoryOfferEligibility is bool) {
diff --git a/libraries/flutter_inapp_purchase/test/builders_unit_test.dart b/libraries/flutter_inapp_purchase/test/builders_unit_test.dart
index 6558b892f..912921d46 100644
--- a/libraries/flutter_inapp_purchase/test/builders_unit_test.dart
+++ b/libraries/flutter_inapp_purchase/test/builders_unit_test.dart
@@ -38,7 +38,8 @@ void main() {
..sku = 'com.example.subscription'
..appAccountToken = 'user456'
..quantity = 1
- ..advancedCommerceData = 'sub_campaign';
+ ..advancedCommerceData = 'sub_campaign'
+ ..billingPlanType = SubscriptionBillingPlanTypeIOS.Monthly;
final props = builder.build();
@@ -46,6 +47,7 @@ void main() {
expect(props.appAccountToken, 'user456');
expect(props.quantity, 1);
expect(props.advancedCommerceData, 'sub_campaign');
+ expect(props.billingPlanType, SubscriptionBillingPlanTypeIOS.Monthly);
});
});
@@ -244,6 +246,18 @@ void main() {
'campaign',
);
});
+
+ test('builds subscription with iOS billing plan type', () {
+ final builder = RequestPurchaseBuilder()
+ ..ios.sku = 'sub'
+ ..ios.billingPlanType = SubscriptionBillingPlanTypeIOS.Monthly
+ ..type = ProductQueryType.Subs;
+
+ final props = builder.build();
+ final json = props.toJson();
+
+ expect(json['requestSubscription']['ios']['billingPlanType'], 'monthly');
+ });
});
group('RequestSubscriptionBuilder', () {
diff --git a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart
index ed5557f9d..e7cf2a837 100644
--- a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart
+++ b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart
@@ -522,6 +522,7 @@ void main() {
const props = types.RequestPurchaseProps.subs((
apple: types.RequestSubscriptionIosProps(
sku: 'ios.sub',
+ billingPlanType: types.SubscriptionBillingPlanTypeIOS.Monthly,
introductoryOfferEligibility: true,
promotionalOfferJWS: types.PromotionalOfferJWSInputIOS(
offerId: 'promo-offer',
@@ -546,6 +547,7 @@ void main() {
expect(payload['sku'], 'ios.sub');
expect(payload['type'], 'subs');
+ expect(payload['billingPlanType'], 'monthly');
expect(payload['introductoryOfferEligibility'], isTrue);
final promotionalOfferJWS = Map.from(
payload['promotionalOfferJWS'] as Map,
diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd
index 32baa822a..38aef4994 100644
--- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd
+++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd
@@ -399,8 +399,8 @@ func _request_purchase_raw(args: Dictionary) -> Dictionary:
purchase_error.emit({ "code": "not-prepared", "message": "Native plugin not available" })
return { "success": false, "error": "Native plugin not available" }
- # Support both "requestPurchase" (from to_dict) and "request" (legacy)
- var request = args.get("requestPurchase", args.get("request", {}))
+ # Support "requestPurchase", "requestSubscription", and legacy "request".
+ var request = args.get("requestPurchase", args.get("requestSubscription", args.get("request", {})))
var purchase_type = args.get("type", "in-app")
var result_raw = null
@@ -429,7 +429,12 @@ func _request_purchase_raw(args: Dictionary) -> Dictionary:
var sku = apple_props.get("sku", "")
if sku.is_empty():
return { "success": false, "error": "Invalid request: SKU is required" }
- result_raw = _native_plugin.call("requestPurchase", sku)
+ var ios_payload = { "type": purchase_type }
+ if purchase_type == "subs":
+ ios_payload["requestSubscription"] = { "ios": apple_props }
+ else:
+ ios_payload["requestPurchase"] = { "ios": apple_props }
+ result_raw = _native_plugin.call("requestPurchaseWithPayload", JSON.stringify(ios_payload))
else:
return { "success": false, "error": "Unsupported platform" }
diff --git a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift
index a9428f0ff..0dd92ab8a 100644
--- a/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift
+++ b/libraries/godot-iap/ios-gdextension/Sources/GodotIap/GodotIap.swift
@@ -275,6 +275,56 @@ public class GodotIap: RefCounted, @unchecked Sendable {
return "{\"status\": \"pending\"}"
}
+ @Callable
+ public func requestPurchaseWithPayload(argsJson: String) -> String {
+ GodotIapLog.payload("requestPurchaseWithPayload", payload: argsJson)
+
+ guard let data = argsJson.data(using: .utf8),
+ let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return jsonString([
+ "success": false,
+ "code": ErrorCode.developerError.rawValue,
+ "error": "Invalid request payload"
+ ])
+ }
+
+ let purchaseProps: RequestPurchaseProps
+ do {
+ purchaseProps = try GodotIapHelper.decodeRequestPurchaseProps(from: payload)
+ } catch {
+ return jsonString([
+ "success": false,
+ "code": ErrorCode.developerError.rawValue,
+ "error": error.localizedDescription
+ ])
+ }
+
+ Task { [weak self] in
+ do {
+ let result = try await self?.openIap.requestPurchase(purchaseProps)
+
+ switch result {
+ case .purchase(let purchase):
+ if purchase == nil {
+ await self?.emitPurchaseError(code: ErrorCode.userCancelled.rawValue, message: "Purchase was cancelled")
+ }
+ case .purchases(let purchases):
+ if purchases?.isEmpty ?? true {
+ await self?.emitPurchaseError(code: ErrorCode.userCancelled.rawValue, message: "Purchase was cancelled")
+ }
+ case .none:
+ await self?.emitPurchaseError(code: ErrorCode.userCancelled.rawValue, message: "Purchase was cancelled")
+ }
+ } catch let error as PurchaseError {
+ await self?.emitPurchaseError(code: error.code.rawValue, message: error.message)
+ } catch {
+ await self?.emitPurchaseError(code: ErrorCode.purchaseError.rawValue, message: error.localizedDescription)
+ }
+ }
+
+ return "{\"status\": \"pending\"}"
+ }
+
@Callable
public func finishTransaction(argsJson: String) -> String {
GodotIapLog.payload("finishTransaction", payload: argsJson)
@@ -1521,6 +1571,14 @@ public class GodotIap: RefCounted, @unchecked Sendable {
return dictionary
}
+ private func jsonString(_ object: [String: Any]) -> String {
+ guard let data = try? JSONSerialization.data(withJSONObject: object),
+ let string = String(data: data, encoding: .utf8) else {
+ return "{\"success\": false, \"code\": \"unknown\", \"error\": \"JSON serialization failed\"}"
+ }
+ return string
+ }
+
private func purchaseToDictionary(_ purchase: Purchase) -> [String: Any] {
var transactionId: String = ""
switch purchase {
diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt
index 4aaafd77a..42773e86f 100644
--- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt
+++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt
@@ -13,6 +13,7 @@ import io.github.hyochan.kmpiap.openiap.RequestSubscriptionIosProps
import io.github.hyochan.kmpiap.openiap.RequestSubscriptionPropsByPlatforms
import io.github.hyochan.kmpiap.openiap.DiscountOfferInputIOS
import io.github.hyochan.kmpiap.openiap.PromotionalOfferJWSInputIOS
+import io.github.hyochan.kmpiap.openiap.SubscriptionBillingPlanTypeIOS
import io.github.hyochan.kmpiap.openiap.WinBackOfferInputIOS
/**
@@ -140,6 +141,10 @@ class IosOptionsBuilder {
* Added in openiap-apple v1.3.7
*/
var advancedCommerceData: String? = null
+ /**
+ * Billing plan for annual subscriptions with monthly commitment (iOS 26.4+).
+ */
+ var billingPlanType: SubscriptionBillingPlanTypeIOS? = null
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
@@ -180,6 +185,7 @@ class IosOptionsBuilder {
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
withOffer = withOffer,
advancedCommerceData = advancedCommerceData,
+ billingPlanType = billingPlanType,
introductoryOfferEligibility = introductoryOfferEligibility,
promotionalOfferJWS = promotionalOfferJWS,
winBackOffer = winBackOffer
diff --git a/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt b/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt
index cea608196..50d37fcee 100644
--- a/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt
+++ b/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt
@@ -168,11 +168,13 @@ class InAppPurchaseTest {
fun testRequestSubscriptionIosPropsWithAdvancedCommerceData() {
val props = RequestSubscriptionIosProps(
sku = "com.example.subscription.monthly",
- advancedCommerceData = "campaign_q4_2025"
+ advancedCommerceData = "campaign_q4_2025",
+ billingPlanType = SubscriptionBillingPlanTypeIOS.Monthly
)
assertEquals("com.example.subscription.monthly", props.sku)
assertEquals("campaign_q4_2025", props.advancedCommerceData)
+ assertEquals(SubscriptionBillingPlanTypeIOS.Monthly, props.billingPlanType)
}
// =========================================================================
diff --git a/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt b/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
index cc7903a1b..545ade47a 100644
--- a/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
+++ b/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
@@ -1101,6 +1101,12 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
PurchaseIOS(
appAccountToken = map["appAccountToken"] as? String,
appBundleIdIOS = map["appBundleIdIOS"] as? String,
+ billingPlanTypeIOS = (map["billingPlanTypeIOS"] as? String)?.let {
+ SubscriptionBillingPlanTypeIOS.fromJson(it)
+ },
+ commitmentInfoIOS = convertAnyToTransactionCommitmentInfoIOS(
+ map["commitmentInfoIOS"]
+ ),
countryCodeIOS = map["countryCodeIOS"] as? String,
currencyCodeIOS = map["currencyCodeIOS"] as? String,
currencySymbolIOS = map["currencySymbolIOS"] as? String,
@@ -1125,6 +1131,7 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
quantityIOS = (map["quantityIOS"] as? Number)?.toInt(),
reasonIOS = map["reasonIOS"] as? String,
reasonStringRepresentationIOS = map["reasonStringRepresentationIOS"] as? String,
+ renewalInfoIOS = convertAnyToRenewalInfoIOS(map["renewalInfoIOS"]),
revocationDateIOS = (map["revocationDateIOS"] as? Number)?.toDouble(),
revocationReasonIOS = map["revocationReasonIOS"] as? String,
storefrontCountryCodeIOS = map["storefrontCountryCodeIOS"] as? String,
@@ -1142,6 +1149,32 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
}
}
+ private fun mapFromAny(data: Any?): Map? {
+ val dict = data as? Map<*, *> ?: return null
+ return dict.entries.associate { (key, value) -> key.toString() to value }
+ }
+
+ private fun convertAnyToTransactionCommitmentInfoIOS(data: Any?): TransactionCommitmentInfoIOS? {
+ return mapFromAny(data)?.let { map ->
+ runCatching { TransactionCommitmentInfoIOS.fromJson(map) }.getOrNull()
+ }
+ }
+
+ private fun convertAnyToRenewalInfoIOS(data: Any?): RenewalInfoIOS? {
+ return mapFromAny(data)?.let { map ->
+ runCatching { RenewalInfoIOS.fromJson(map) }.getOrNull()
+ }
+ }
+
+ private fun convertAnyListToSubscriptionPricingTermsIOS(data: Any?): List? {
+ val list = data as? List<*> ?: return null
+ return list.mapNotNull { item ->
+ mapFromAny(item)?.let { map ->
+ runCatching { SubscriptionPricingTermsIOS.fromJson(map) }.getOrNull()
+ }
+ }.ifEmpty { null }
+ }
+
@Suppress("UNCHECKED_CAST")
private fun convertAnyListToPurchases(data: Any?): List {
if (data == null) return emptyList()
@@ -1205,6 +1238,9 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
jsonRepresentationIOS = map["jsonRepresentationIOS"] as? String ?: "",
platform = IapPlatform.Ios,
price = (map["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = convertAnyListToSubscriptionPricingTermsIOS(
+ map["pricingTermsIOS"]
+ ),
subscriptionInfoIOS = null, // Complex object, handle separately if needed
subscriptionOffers = subscriptionOffers.ifEmpty { null },
title = map["title"] as? String ?: "",
@@ -1253,6 +1289,9 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
jsonRepresentationIOS = map["jsonRepresentationIOS"] as? String ?: "",
platform = IapPlatform.Ios,
price = (map["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = convertAnyListToSubscriptionPricingTermsIOS(
+ map["pricingTermsIOS"]
+ ),
subscriptionInfoIOS = null, // Complex object
subscriptionOffers = subscriptionOffers.ifEmpty { null },
subscriptionGroupIdIOS = map["subscriptionGroupIdIOS"] as? String,
@@ -1315,7 +1354,13 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
jsonRepresentationIOS = map["jsonRepresentationIOS"] as? String ?: "",
platform = IapPlatform.Ios,
price = (map["price"] as? Number)?.toDouble(),
+ pricingTermsIOS = convertAnyListToSubscriptionPricingTermsIOS(
+ map["pricingTermsIOS"]
+ ),
subscriptionInfoIOS = null, // Complex object
+ subscriptionOffers = convertAnyListToSubscriptionOffers(
+ map["subscriptionOffers"] ?: map["offers"]
+ ).ifEmpty { null },
title = map["title"] as? String ?: "",
type = (map["type"] as? String)?.let { ProductType.fromJson(it) }
?: ProductType.InApp,
diff --git a/libraries/react-native-iap/ios/HybridRnIap.swift b/libraries/react-native-iap/ios/HybridRnIap.swift
index 14034b81b..27fd448a8 100644
--- a/libraries/react-native-iap/ios/HybridRnIap.swift
+++ b/libraries/react-native-iap/ios/HybridRnIap.swift
@@ -208,6 +208,9 @@ class HybridRnIap: HybridRnIapSpec {
if case .second(let advancedCommerceData) = iosRequest.advancedCommerceData {
iosPayload["advancedCommerceData"] = advancedCommerceData
}
+ if case .second(let billingPlanType) = iosRequest.billingPlanType {
+ iosPayload["billingPlanType"] = billingPlanType
+ }
// WWDC 2025 / iOS 18+ subscription offer fields
if case .second(let introductoryOfferEligibility) = iosRequest.introductoryOfferEligibility {
iosPayload["introductoryOfferEligibility"] = introductoryOfferEligibility
diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts
index 580e79b60..1382a8bb6 100644
--- a/libraries/react-native-iap/src/__tests__/index.test.ts
+++ b/libraries/react-native-iap/src/__tests__/index.test.ts
@@ -860,6 +860,7 @@ describe('Public API (src/index.ts)', () => {
request: {
apple: {
sku: 'premium_sub',
+ billingPlanType: 'monthly',
introductoryOfferEligibility: false,
promotionalOfferJWS: {
offerId: 'promo-offer',
@@ -873,6 +874,7 @@ describe('Public API (src/index.ts)', () => {
type: 'subs',
});
const passed = mockIap.requestPurchase.mock.calls.pop()?.[0];
+ expect(passed.ios.billingPlanType).toBe('monthly');
expect(passed.ios.introductoryOfferEligibility).toBe(false);
expect(passed.ios.promotionalOfferJWS).toEqual({
offerId: 'promo-offer',
diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts
index 0f65e6fd0..b38bf0f0d 100644
--- a/libraries/react-native-iap/src/index.ts
+++ b/libraries/react-native-iap/src/index.ts
@@ -1721,6 +1721,9 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
}
if (isSubs) {
const subscriptionRequest = iosRequest as RequestSubscriptionIosProps;
+ if (subscriptionRequest.billingPlanType) {
+ iosPayload.billingPlanType = subscriptionRequest.billingPlanType;
+ }
if (
subscriptionRequest.introductoryOfferEligibility !== undefined
) {
diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts
index b15fcd220..0458e080b 100644
--- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts
+++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts
@@ -49,6 +49,7 @@ import type {
RequestPurchaseIosProps,
RequestPurchaseResult,
RequestSubscriptionAndroidProps,
+ RequestSubscriptionIosProps,
UserChoiceBillingDetails,
PaymentModeIOS,
SubscriptionProductReplacementParamsAndroid,
@@ -174,6 +175,12 @@ export interface NitroRequestPurchaseIos {
* @platform iOS
*/
advancedCommerceData?: RequestPurchaseIosProps['advancedCommerceData'];
+ /**
+ * Billing plan to use for annual subscriptions that offer monthly billing with
+ * a 12-month commitment (iOS 26.4+).
+ * @platform iOS
+ */
+ billingPlanType?: RequestSubscriptionIosProps['billingPlanType'];
/**
* Override introductory offer eligibility (iOS 15+, WWDC 2025).
* Set to true to indicate the user is eligible for introductory offer,
From ba8a67fda619d4414d25cb798fde7f75e8e0dbb8 Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:16:28 +0900
Subject: [PATCH 5/8] docs: document ios billing plans
---
packages/docs/src/components/CodeBlock.tsx | 10 +
packages/docs/src/lib/searchData.ts | 26 +-
.../src/pages/docs/apis/request-purchase.tsx | 24 ++
.../docs/features/subscription/index.tsx | 210 ++++++++++++
packages/docs/src/pages/docs/index.tsx | 9 +
packages/docs/src/pages/docs/types/index.tsx | 18 +
.../pages/docs/types/ios/renewal-info-ios.tsx | 24 ++
.../ios/subscription-billing-plan-ios.tsx | 320 ++++++++++++++++++
.../docs/src/pages/docs/types/product.tsx | 13 +
.../docs/src/pages/docs/types/purchase.tsx | 24 ++
.../docs/types/request-purchase-props.tsx | 16 +
.../pages/docs/types/subscription-product.tsx | 16 +
.../docs/src/pages/docs/updates/releases.tsx | 249 ++++++++++++++
13 files changed, 958 insertions(+), 1 deletion(-)
create mode 100644 packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx
diff --git a/packages/docs/src/components/CodeBlock.tsx b/packages/docs/src/components/CodeBlock.tsx
index a79353055..43ce47c87 100644
--- a/packages/docs/src/components/CodeBlock.tsx
+++ b/packages/docs/src/components/CodeBlock.tsx
@@ -93,10 +93,20 @@ const TYPE_LINKS: Record = {
PaymentModeIOS: '/docs/types/ios/payment-mode-ios',
RenewalInfo: '/docs/types/ios/renewal-info-ios',
RenewalInfoIOS: '/docs/types/ios/renewal-info-ios',
+ RenewalCommitmentInfoIOS:
+ '/docs/types/ios/subscription-billing-plan-ios#renewal-commitment-info-ios',
+ SubscriptionBillingPlanTypeIOS:
+ '/docs/types/ios/subscription-billing-plan-ios#subscription-billing-plan-type-ios',
+ SubscriptionCommitmentInfoIOS:
+ '/docs/types/ios/subscription-billing-plan-ios#subscription-commitment-info-ios',
SubscriptionPeriod: '/docs/types/ios/subscription-period-ios',
SubscriptionPeriodIOS: '/docs/types/ios/subscription-period-ios',
+ SubscriptionPricingTermsIOS:
+ '/docs/types/ios/subscription-billing-plan-ios#subscription-pricing-terms-ios',
SubscriptionStatus: '/docs/types/ios/subscription-status-ios',
SubscriptionStatusIOS: '/docs/types/ios/subscription-status-ios',
+ TransactionCommitmentInfoIOS:
+ '/docs/types/ios/subscription-billing-plan-ios#transaction-commitment-info-ios',
// Android-only
OneTimePurchaseOfferDetailAndroid:
'/docs/types/android/one-time-purchase-offer-detail-android',
diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts
index 296162402..b06518af2 100644
--- a/packages/docs/src/lib/searchData.ts
+++ b/packages/docs/src/lib/searchData.ts
@@ -571,6 +571,14 @@ export const apiData: ApiItem[] = [
'Server-side purchase validation with verifyPurchase and verifyPurchaseWithProvider (IAPKit)',
path: '/docs/features/validation',
},
+ {
+ id: 'ios-commitment-billing-plans',
+ title: 'iOS Commitment Billing Plans',
+ category: 'Subscription',
+ description:
+ 'StoreKit 26.4 monthly billing with 12-month commitment, up-front billing, pricingTermsIOS, billingPlanType, and commitmentInfoIOS',
+ path: '/docs/features/subscription#ios-commitment-billing-plans',
+ },
{
id: 'debugging-page',
title: 'Debugging',
@@ -677,7 +685,7 @@ export const apiData: ApiItem[] = [
title: 'iOS Types',
category: 'Types',
description:
- 'DiscountOffer, SubscriptionStatusIOS, PaymentMode, AppTransaction',
+ 'DiscountOffer, SubscriptionStatusIOS, PaymentMode, AppTransaction, SubscriptionBillingPlanTypeIOS',
path: '/docs/types#ios-types',
},
{
@@ -720,6 +728,22 @@ export const apiData: ApiItem[] = [
description: 'iOS subscription period units: Day, Week, Month, Year',
path: '/docs/types/ios/subscription-period-ios',
},
+ {
+ id: 'subscription-billing-plan-ios',
+ title: 'Subscription Billing Plan iOS',
+ category: 'Types (iOS)',
+ description:
+ 'StoreKit 26.4 billing plan and commitment types: SubscriptionBillingPlanTypeIOS, SubscriptionPricingTermsIOS, TransactionCommitmentInfoIOS, RenewalCommitmentInfoIOS',
+ path: '/docs/types/ios/subscription-billing-plan-ios',
+ },
+ {
+ id: 'subscription-billing-plan-type-ios',
+ title: 'SubscriptionBillingPlanTypeIOS',
+ category: 'Types (iOS)',
+ description:
+ 'iOS subscription billing plan values: unknown, monthly, up-front',
+ path: '/docs/types/ios/subscription-billing-plan-ios#subscription-billing-plan-type-ios',
+ },
{
id: 'payment-mode',
title: 'PaymentMode',
diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx
index 131402497..24ece312b 100644
--- a/packages/docs/src/pages/docs/apis/request-purchase.tsx
+++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx
@@ -160,6 +160,19 @@ type RequestPurchaseProps =
{' '}
— iOS. Quantity for consumable bulk purchases.
+
+ request.apple.billingPlanType{' '}
+
+ (subscriptions only, optional,{' '}
+ 'monthly' | 'up-front')
+ {' '}
+ — iOS 26.4+. Selects the StoreKit subscription
+ billing plan, such as monthly billing with a 12-month commitment. See{' '}
+
+ iOS commitment billing plans
+
+ .
+
request.google.skus{' '}
@@ -250,6 +263,17 @@ await requestPurchase({
type: 'subs',
});
+// iOS subscription with monthly billing and a 12-month commitment
+await requestPurchase({
+ request: {
+ apple: {
+ sku: 'com.app.annual',
+ billingPlanType: 'monthly',
+ },
+ },
+ type: 'subs',
+});
+
// --- Or via the useIAP() hook (also exported from react-native-iap) ---
// useIAP wires the purchase listeners for you and exposes the same
// requestPurchase function — handle the result inside onPurchaseSuccess.
diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx
index d09d1dc70..47adc9c39 100644
--- a/packages/docs/src/pages/docs/features/subscription/index.tsx
+++ b/packages/docs/src/pages/docs/features/subscription/index.tsx
@@ -23,6 +23,216 @@ function Subscription() {
subscription management in your app.
+
+
+ iOS Commitment Billing Plans
+
+
+ StoreKit 26.4 supports auto-renewable subscriptions that can be paid
+ monthly while committing the customer to a longer period, such as a
+ 12-month commitment, or paid up front for the full subscription
+ period. OpenIAP exposes these iOS billing plans without changing the
+ Android subscription-offer flow.
+
+
+
+ Fetch products first and read pricingTermsIOS to show
+ the available monthly and up-front plans.
+
+
+ Pass billingPlanType: 'monthly' when the user
+ chooses monthly billing with commitment.
+
+
+ Read PurchaseIOS.billingPlanTypeIOS and{' '}
+ PurchaseIOS.commitmentInfoIOS after purchase to record
+ the selected commitment.
+
+
+ Use RenewalInfoIOS.renewalBillingPlanType and{' '}
+ RenewalInfoIOS.commitmentInfo for upcoming commitment
+ renewal state when StoreKit returns it.
+
+
+
+
+
+ Availability: billing plan selection requires
+ StoreKit 26.4 runtime support. If the app runs on an older Apple OS
+ version, omit billingPlanType and let StoreKit purchase
+ the default plan. Never send unknown as a purchase
+ option.
+
+
+
+
+
+
+
OpenIAP field
+
Use it for
+
+
+
+
+
+ ProductSubscriptionIOS.pricingTermsIOS
+
+
Displaying StoreKit billing choices before purchase.
+
+
+
+ RequestSubscriptionIosProps.billingPlanType
+
+
+ Selecting monthly or up-front.
+
+
+
+
+ PurchaseIOS.billingPlanTypeIOS
+
+
+ Persisting the plan selected on the completed transaction.
+
+
+
+
+ PurchaseIOS.commitmentInfoIOS
+
+
+ Tracking current billing period, total periods, expiration, and
+ commitment price.
+
+
+
Subscription Offers
diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx
index 1185ed3ff..8f013c46b 100644
--- a/packages/docs/src/pages/docs/index.tsx
+++ b/packages/docs/src/pages/docs/index.tsx
@@ -31,6 +31,7 @@ import TypesVerifyPurchaseWithProviderResult from './types/verify-purchase-with-
import TypesDiscountOfferIOS from './types/ios/discount-offer-ios';
import TypesDiscountIOS from './types/ios/discount-ios';
import TypesSubscriptionPeriodIOS from './types/ios/subscription-period-ios';
+import TypesSubscriptionBillingPlanIOS from './types/ios/subscription-billing-plan-ios';
import TypesPaymentModeIOS from './types/ios/payment-mode-ios';
import TypesSubscriptionStatusIOS from './types/ios/subscription-status-ios';
import TypesAppTransactionIOS from './types/ios/app-transaction-ios';
@@ -309,6 +310,10 @@ function Docs() {
to: '/docs/types/ios/subscription-period-ios',
label: 'SubscriptionPeriodIOS',
},
+ {
+ to: '/docs/types/ios/subscription-billing-plan-ios',
+ label: 'SubscriptionBillingPlanIOS',
+ },
{
to: '/docs/types/ios/payment-mode-ios',
label: 'PaymentModeIOS',
@@ -877,6 +882,10 @@ function Docs() {
path="types/ios/subscription-period-ios"
element={}
/>
+ }
+ />
}
diff --git a/packages/docs/src/pages/docs/types/index.tsx b/packages/docs/src/pages/docs/types/index.tsx
index 276c85354..bc9b42601 100644
--- a/packages/docs/src/pages/docs/types/index.tsx
+++ b/packages/docs/src/pages/docs/types/index.tsx
@@ -34,6 +34,18 @@ const LEGACY_ANCHOR_REDIRECTS: Record = {
'discount-offer-ios': '/docs/types/ios/discount-offer-ios',
'discount-ios': '/docs/types/ios/discount-ios',
'subscription-period-ios': '/docs/types/ios/subscription-period-ios',
+ 'subscription-billing-plan-ios':
+ '/docs/types/ios/subscription-billing-plan-ios',
+ 'subscription-billing-plan-type-ios':
+ '/docs/types/ios/subscription-billing-plan-ios#subscription-billing-plan-type-ios',
+ 'subscription-pricing-terms-ios':
+ '/docs/types/ios/subscription-billing-plan-ios#subscription-pricing-terms-ios',
+ 'subscription-commitment-info-ios':
+ '/docs/types/ios/subscription-billing-plan-ios#subscription-commitment-info-ios',
+ 'transaction-commitment-info-ios':
+ '/docs/types/ios/subscription-billing-plan-ios#transaction-commitment-info-ios',
+ 'renewal-commitment-info-ios':
+ '/docs/types/ios/subscription-billing-plan-ios#renewal-commitment-info-ios',
'payment-mode-ios': '/docs/types/ios/payment-mode-ios',
'subscription-status-ios': '/docs/types/ios/subscription-status-ios',
'app-transaction-ios': '/docs/types/ios/app-transaction-ios',
@@ -192,6 +204,12 @@ const IOS_TYPES: TypeRow[] = [
name: 'SubscriptionPeriodIOS',
description: 'iOS subscription period units (Day/Week/Month/Year).',
},
+ {
+ to: '/docs/types/ios/subscription-billing-plan-ios',
+ name: 'SubscriptionBillingPlanIOS',
+ description:
+ 'StoreKit 26.4 billing plan and commitment types for iOS subscriptions.',
+ },
{
to: '/docs/types/ios/payment-mode-ios',
name: 'PaymentModeIOS',
diff --git a/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx b/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx
index ecf75d2af..a9260f4a1 100644
--- a/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx
+++ b/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx
@@ -1,3 +1,4 @@
+import { Link } from 'react-router-dom';
import AnchorLink from '../../../../components/AnchorLink';
import SEO from '../../../../components/SEO';
import { useScrollToHash } from '../../../../hooks/useScrollToHash';
@@ -129,6 +130,29 @@ function RenewalInfoIOS() {
"SUBSCRIPTION_OFFER_CODE", "WIN_BACK".
+
+
+ renewalBillingPlanType
+
+
+ StoreKit 26.4 billing plan type for the next renewal when
+ available: monthly, up-front, or{' '}
+ unknown.
+
+
+
+
+ commitmentInfo
+
+
+ Commitment renewal details, including auto-renew product,
+ renewal billing plan type, renewal date, and renewal price. See{' '}
+
+ RenewalCommitmentInfoIOS
+
+ .
+
+
jsonRepresentation
diff --git a/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx b/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx
new file mode 100644
index 000000000..494494a60
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx
@@ -0,0 +1,320 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../../components/AnchorLink';
+import CodeBlock from '../../../../components/CodeBlock';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function SubscriptionBillingPlanIOS() {
+ useScrollToHash();
+
+ return (
+
+
+
+ iOS{' '}
+ Subscription Billing Plans
+
+
+ StoreKit 26.4 adds billing plans for auto-renewable subscriptions,
+ including monthly billing with a 12-month commitment and up-front
+ billing for the full subscription period. OpenIAP exposes the selected
+ billing plan on purchase results, available pricing terms on fetched
+ products, and commitment renewal details when StoreKit returns them.
+
+
+
+
+ Availability: billing plan selection requires iOS,
+ macOS, tvOS, watchOS, or visionOS 26.4+ and an SDK compiled with
+ StoreKit 26.4 symbols. On older runtimes, omit{' '}
+ billingPlanType and use the store's default billing plan.
+
+
+
+
+
+ SubscriptionBillingPlanTypeIOS
+
+
+ Enum used by{' '}
+
+ RequestSubscriptionIosProps.billingPlanType
+
+ , PurchaseIOS.billingPlanTypeIOS, pricing terms, and
+ renewal commitment fields.
+
+
+
+
+
Value
+
Summary
+
+
+
+
+
+ unknown
+
+
+ StoreKit returned an unsupported or unavailable billing plan
+ type.
+
+
+
+
+ monthly
+
+
Monthly billing with a 12-month commitment.
+
+
+
+ up-front
+
+
Up-front billing for the full subscription period.
+ Pricing term returned on ProductIOS.pricingTermsIOS,{' '}
+ ProductSubscriptionIOS.pricingTermsIOS, and{' '}
+ SubscriptionInfoIOS.pricingTerms. Use it to display the
+ available commitment choices before starting a purchase.
+
+ Full commitment price and period for this plan. See{' '}
+
+ SubscriptionCommitmentInfoIOS
+
+ .
+
+
+
+
+ subscriptionOffers
+
+
+ StoreKit offers attached to this billing plan, normalized as{' '}
+
+ SubscriptionOffer
+
+ .
+
+
+
+
+
+
+
+
+ SubscriptionCommitmentInfoIOS
+
+
+ Commitment summary attached to a pricing term before purchase. It
+ describes the full commitment, not just one billing installment.
+
+
+
+
+
Name
+
Summary
+
+
+
+
+
+ displayPrice
+
+
Localized commitment total.
+
+
+
+ period
+
+
Full commitment duration.
+
+
+
+ price
+
+
Numeric commitment total.
+
+
+
+
+
+
+
+ TransactionCommitmentInfoIOS
+
+
+ Commitment state attached to{' '}
+ PurchaseIOS.commitmentInfoIOS after purchase and on later
+ transaction reads such as{' '}
+
+ latestTransactionIOS
+
+ .
+
+
+
+
+
Name
+
Summary
+
+
+
+
+
+ billingPeriodNumber
+
+
Current billing period number within the commitment.
+
+
+
+ totalBillingPeriods
+
+
Total number of billing periods in the commitment.
+
+
+
+ commitmentExpiresDate
+
+
Commitment expiration timestamp in epoch milliseconds.
+
+
+
+ commitmentPrice
+
+
Numeric price StoreKit reports for the commitment.
+
+
+
+
+
+
+
+ RenewalCommitmentInfoIOS
+
+
+ Commitment renewal state attached to{' '}
+
+ RenewalInfoIOS.commitmentInfo
+
+ . Use this when StoreKit reports the next commitment renewal plan or
+ renewal price.
+
+
+
+
+
Name
+
Summary
+
+
+
+
+
+ commitmentAutoRenewProductId
+
+
Product ID StoreKit will auto-renew to.
+
+
+
+ commitmentAutoRenewStatus
+
+
Whether StoreKit reports the commitment will auto-renew.
+
+
+
+ commitmentRenewalBillingPlanType
+
+
Billing plan type for the next commitment renewal.
+
+
+
+ commitmentRenewalDate
+
+
Next commitment renewal timestamp in epoch milliseconds.
+ StoreKit 26.4 billing plan selected for a subscription
+ transaction: monthly, up-front
+ , or unknown.
+
+
+
+
+ commitmentInfoIOS
+
+
+ Commitment state for the transaction, including current
+ billing period, total periods, expiration, and price.
+ See{' '}
+
+ TransactionCommitmentInfoIOS
+
+ .
+
+ StoreKit 26.4 billing plan for auto-renewable
+ subscriptions: monthly for monthly billing
+ with commitment or up-front for full-period
+ payment. Omit on older OS versions and do not pass{' '}
+ unknown. See{' '}
+
+ SubscriptionBillingPlanTypeIOS
+
+ .
+
+
introductoryOfferEligibility
diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx
index 9e01da232..f2835a001 100644
--- a/packages/docs/src/pages/docs/types/subscription-product.tsx
+++ b/packages/docs/src/pages/docs/types/subscription-product.tsx
@@ -227,6 +227,22 @@ function SubscriptionProduct() {
isEligibleForIntroOfferIOS
+
+
+ pricingTermsIOS
+
+
+ StoreKit 26.4 billing plan choices for this
+ subscription. Each term includes billing price, billing
+ period, monthly or up-front{' '}
+ plan type, commitment info, and plan-specific
+ subscription offers. See{' '}
+
+ SubscriptionPricingTermsIOS
+
+ .
+
+ Publishes OpenIAP Spec 2.0.4 and SDK releases for
+ StoreKit 26.4 subscription billing plans, including monthly billing
+ with a 12-month commitment. Apps can pass{' '}
+ RequestSubscriptionIosProps.billingPlanType during
+ subscription purchase requests and inspect pricing terms,
+ transaction commitment progress, and renewal billing-plan metadata
+ from iOS product and purchase payloads. Track the feature request in{' '}
+
+ issue #198
+
+ . See the{' '}
+
+ iOS commitment billing plans guide
+ {' '}
+ for usage examples and field references.
+
+
+
+
+ Billing plan selection — iOS subscription
+ purchases now accept billingPlanType with{' '}
+ monthly and up-front values, mapping to
+ StoreKit's{' '}
+ Product.PurchaseOption.billingPlanType on iOS, macOS,
+ tvOS, watchOS, and visionOS 26.4+.
+
+
+ Pricing terms exposed — iOS subscription products
+ expose pricingTermsIOS and{' '}
+ SubscriptionInfoIOS.pricingTerms, including billing
+ price, billing period, commitment period, commitment price, and
+ subscription offers associated with each pricing term.
+
+
+ Transaction commitment metadata —{' '}
+ PurchaseIOS.billingPlanTypeIOS and{' '}
+ PurchaseIOS.commitmentInfoIOS report the selected
+ billing plan, current billing-period number, total billing
+ periods, commitment price, and commitment expiration date when
+ StoreKit returns that data.
+
+
+ Renewal plan visibility —{' '}
+ RenewalInfoIOS.renewalBillingPlanType and{' '}
+ RenewalInfoIOS.commitmentInfo surface upcoming
+ renewal billing-plan and commitment metadata for active
+ subscriptions.
+
+
+ Framework parity — React Native, Expo, Flutter,
+ KMP, Godot, and MAUI receive regenerated types; React Native,
+ Flutter, KMP, and Godot also forward the new iOS subscription
+ request option through their purchase bridges.
+
+
+ Android behavior unchanged — openiap-google is
+ regenerated for schema parity, but this release does not change
+ Google Play or Horizon billing behavior.
+
+ ),
+ },
+
// June 28, 2026 — iOS syncIOS cancellation error hotfix
{
id: 'ios-syncios-cancellation-error-hotfix-2026-06-28',
From 14b0654e956db1b200c81c8bd04e828df0b7c414 Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:27:04 +0900
Subject: [PATCH 6/8] fix(godot): harden billing plan payloads
---
.../godot-iap/addons/godot-iap/godot_iap.gd | 4 +-
libraries/godot-iap/addons/godot-iap/types.gd | 76 +++++++++----------
packages/gql/codegen/plugins/gdscript.ts | 53 ++++++++-----
packages/gql/src/generated/types.gd | 76 +++++++++----------
4 files changed, 113 insertions(+), 96 deletions(-)
diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd
index 38aef4994..619b55c6d 100644
--- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd
+++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd
@@ -431,9 +431,9 @@ func _request_purchase_raw(args: Dictionary) -> Dictionary:
return { "success": false, "error": "Invalid request: SKU is required" }
var ios_payload = { "type": purchase_type }
if purchase_type == "subs":
- ios_payload["requestSubscription"] = { "ios": apple_props }
+ ios_payload["requestSubscription"] = { "apple": apple_props }
else:
- ios_payload["requestPurchase"] = { "ios": apple_props }
+ ios_payload["requestPurchase"] = { "apple": apple_props }
result_raw = _native_plugin.call("requestPurchaseWithPayload", JSON.stringify(ios_payload))
else:
return { "success": false, "error": "Unsupported platform" }
diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd
index b471470ed..6cd124e05 100644
--- a/libraries/godot-iap/addons/godot-iap/types.gd
+++ b/libraries/godot-iap/addons/godot-iap/types.gd
@@ -1412,8 +1412,8 @@ class ProductAndroid:
obj.name_android = data["nameAndroid"]
if data.has("productStatusAndroid") and data["productStatusAndroid"] != null:
var enum_str = data["productStatusAndroid"]
- if enum_str is String and PRODUCT_STATUS_ANDROID_FROM_STRING.has(enum_str):
- obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN)
else:
obj.product_status_android = enum_str
if data.has("discountOffers") and data["discountOffers"] != null:
@@ -1822,8 +1822,8 @@ class ProductSubscriptionAndroid:
obj.name_android = data["nameAndroid"]
if data.has("productStatusAndroid") and data["productStatusAndroid"] != null:
var enum_str = data["productStatusAndroid"]
- if enum_str is String and PRODUCT_STATUS_ANDROID_FROM_STRING.has(enum_str):
- obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN)
else:
obj.product_status_android = enum_str
if data.has("discountOffers") and data["discountOffers"] != null:
@@ -2240,8 +2240,8 @@ class PurchaseAndroid:
obj.purchase_token = data["purchaseToken"]
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("platform") and data["platform"] != null:
@@ -2254,8 +2254,8 @@ class PurchaseAndroid:
obj.quantity = data["quantity"]
if data.has("purchaseState") and data["purchaseState"] != null:
var enum_str = data["purchaseState"]
- if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN)
else:
obj.purchase_state = enum_str
if data.has("isAutoRenewing") and data["isAutoRenewing"] != null:
@@ -2351,8 +2351,8 @@ class PurchaseError:
var obj = PurchaseError.new()
if data.has("code") and data["code"] != null:
var enum_str = data["code"]
- if enum_str is String and ERROR_CODE_FROM_STRING.has(enum_str):
- obj.code = ERROR_CODE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.code = ERROR_CODE_FROM_STRING.get(enum_str, ErrorCode.UNKNOWN)
else:
obj.code = enum_str
if data.has("message") and data["message"] != null:
@@ -2448,8 +2448,8 @@ class PurchaseIOS:
obj.purchase_token = data["purchaseToken"]
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("platform") and data["platform"] != null:
@@ -2462,8 +2462,8 @@ class PurchaseIOS:
obj.quantity = data["quantity"]
if data.has("purchaseState") and data["purchaseState"] != null:
var enum_str = data["purchaseState"]
- if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN)
else:
obj.purchase_state = enum_str
if data.has("isAutoRenewing") and data["isAutoRenewing"] != null:
@@ -2524,8 +2524,8 @@ class PurchaseIOS:
obj.renewal_info_ios = data["renewalInfoIOS"]
if data.has("billingPlanTypeIOS") and data["billingPlanTypeIOS"] != null:
var enum_str = data["billingPlanTypeIOS"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type_ios = enum_str
if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null:
@@ -2683,8 +2683,8 @@ class RenewalCommitmentInfoIOS:
obj.commitment_auto_renew_status = data["commitmentAutoRenewStatus"]
if data.has("commitmentRenewalBillingPlanType") and data["commitmentRenewalBillingPlanType"] != null:
var enum_str = data["commitmentRenewalBillingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.commitment_renewal_billing_plan_type = enum_str
if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null:
@@ -2757,8 +2757,8 @@ class RenewalInfoIOS:
obj.renewal_offer_type = data["renewalOfferType"]
if data.has("renewalBillingPlanType") and data["renewalBillingPlanType"] != null:
var enum_str = data["renewalBillingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.renewal_billing_plan_type = enum_str
if data.has("commitmentInfo") and data["commitmentInfo"] != null:
@@ -2834,16 +2834,16 @@ class RequestVerifyPurchaseWithIapkitResult:
var obj = RequestVerifyPurchaseWithIapkitResult.new()
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("isValid") and data["isValid"] != null:
obj.is_valid = data["isValid"]
if data.has("state") and data["state"] != null:
var enum_str = data["state"]
- if enum_str is String and IAPKIT_PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING.get(enum_str, IapkitPurchaseState.UNKNOWN)
else:
obj.state = enum_str
return obj
@@ -3027,8 +3027,8 @@ class SubscriptionOffer:
obj.period_count = data["periodCount"]
if data.has("paymentMode") and data["paymentMode"] != null:
var enum_str = data["paymentMode"]
- if enum_str is String and PAYMENT_MODE_FROM_STRING.has(enum_str):
- obj.payment_mode = PAYMENT_MODE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.payment_mode = PAYMENT_MODE_FROM_STRING.get(enum_str, PaymentMode.UNKNOWN)
else:
obj.payment_mode = enum_str
if data.has("keyIdentifierIOS") and data["keyIdentifierIOS"] != null:
@@ -3179,8 +3179,8 @@ class SubscriptionPeriod:
var obj = SubscriptionPeriod.new()
if data.has("unit") and data["unit"] != null:
var enum_str = data["unit"]
- if enum_str is String and SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.has(enum_str):
- obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.get(enum_str, SubscriptionPeriodUnit.UNKNOWN)
else:
obj.unit = enum_str
if data.has("value") and data["value"] != null:
@@ -3240,8 +3240,8 @@ class SubscriptionPricingTermsIOS:
obj.billing_period = data["billingPeriod"]
if data.has("billingPlanType") and data["billingPlanType"] != null:
var enum_str = data["billingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type = enum_str
if data.has("billingPrice") and data["billingPrice"] != null:
@@ -3676,8 +3676,8 @@ class WebhookEvent:
obj.product_id = data["productId"]
if data.has("subscriptionState") and data["subscriptionState"] != null:
var enum_str = data["subscriptionState"]
- if enum_str is String and SUBSCRIPTION_STATE_FROM_STRING.has(enum_str):
- obj.subscription_state = SUBSCRIPTION_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.subscription_state = SUBSCRIPTION_STATE_FROM_STRING.get(enum_str, SubscriptionState.UNKNOWN)
else:
obj.subscription_state = enum_str
if data.has("expiresAt") and data["expiresAt"] != null:
@@ -4046,8 +4046,8 @@ class PurchaseInput:
obj.purchase_token = data["purchaseToken"]
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("platform") and data["platform"] != null:
@@ -4060,8 +4060,8 @@ class PurchaseInput:
obj.quantity = data["quantity"]
if data.has("purchaseState") and data["purchaseState"] != null:
var enum_str = data["purchaseState"]
- if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN)
else:
obj.purchase_state = enum_str
if data.has("isAutoRenewing") and data["isAutoRenewing"] != null:
@@ -4494,8 +4494,8 @@ class RequestSubscriptionIosProps:
obj.promotional_offer_jws = data["promotionalOfferJWS"]
if data.has("billingPlanType") and data["billingPlanType"] != null:
var enum_str = data["billingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type = enum_str
if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
diff --git a/packages/gql/codegen/plugins/gdscript.ts b/packages/gql/codegen/plugins/gdscript.ts
index 75e424155..10ad8913c 100644
--- a/packages/gql/codegen/plugins/gdscript.ts
+++ b/packages/gql/codegen/plugins/gdscript.ts
@@ -302,6 +302,33 @@ export class GDScriptPlugin extends CodegenPlugin {
this.emit('');
}
+ private getEnumUnknownFallback(typeName: string): string | null {
+ const irEnum = this.schema.enums.find(e => e.name === typeName);
+ const unknown = irEnum?.values.find(
+ (value) => value.name.toLowerCase() === 'unknown' || value.rawValue.toLowerCase() === 'unknown'
+ );
+ return unknown ? `${typeName}.${this.enumValueCase(unknown.name)}` : null;
+ }
+
+ private emitEnumFromDictAssignment(indent: string, target: string, typeName: string, sourceExpression: string): void {
+ const enumReverseLookup = toConstantCase(typeName) + '_FROM_STRING';
+ const fallback = this.getEnumUnknownFallback(typeName);
+
+ this.emit(`${indent}var enum_str = ${sourceExpression}`);
+ if (fallback) {
+ this.emit(`${indent}if enum_str is String:`);
+ this.emit(`${indent}\t${target} = ${enumReverseLookup}.get(enum_str, ${fallback})`);
+ this.emit(`${indent}else:`);
+ this.emit(`${indent}\t${target} = enum_str`);
+ return;
+ }
+
+ this.emit(`${indent}if enum_str is String and ${enumReverseLookup}.has(enum_str):`);
+ this.emit(`${indent}\t${target} = ${enumReverseLookup}[enum_str]`);
+ this.emit(`${indent}else:`);
+ this.emit(`${indent}\t${target} = enum_str`);
+ }
+
// ============================================================================
// Interfaces (not used in GDScript, but required by base class)
// ============================================================================
@@ -392,12 +419,7 @@ export class GDScriptPlugin extends CodegenPlugin {
this.emit(`\t\t\telse:`);
this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`);
} else if (type.kind === 'enum') {
- const enumReverseLookup = toConstantCase(type.name!) + '_FROM_STRING';
- this.emit(`\t\t\tvar enum_str = data["${graphqlName}"]`);
- this.emit(`\t\t\tif enum_str is String and ${enumReverseLookup}.has(enum_str):`);
- this.emit(`\t\t\t\tobj.${fieldName} = ${enumReverseLookup}[enum_str]`);
- this.emit(`\t\t\telse:`);
- this.emit(`\t\t\t\tobj.${fieldName} = enum_str`);
+ this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`);
} else {
this.emit(`\t\t\tobj.${fieldName} = data["${graphqlName}"]`);
}
@@ -543,12 +565,7 @@ export class GDScriptPlugin extends CodegenPlugin {
this.emit(`\t\t\telse:`);
this.emit(`\t\t\t\tobj.${fieldName} = data["${graphqlName}"]`);
} else if (type.kind === 'enum') {
- const enumReverseLookup = toConstantCase(type.name!) + '_FROM_STRING';
- this.emit(`\t\t\tvar enum_str = data["${graphqlName}"]`);
- this.emit(`\t\t\tif enum_str is String and ${enumReverseLookup}.has(enum_str):`);
- this.emit(`\t\t\t\tobj.${fieldName} = ${enumReverseLookup}[enum_str]`);
- this.emit(`\t\t\telse:`);
- this.emit(`\t\t\t\tobj.${fieldName} = enum_str`);
+ this.emitEnumFromDictAssignment('\t\t\t', `obj.${fieldName}`, type.name!, `data["${graphqlName}"]`);
} else {
this.emit(`\t\t\tobj.${fieldName} = data["${graphqlName}"]`);
}
@@ -631,12 +648,12 @@ export class GDScriptPlugin extends CodegenPlugin {
const argSnakeName = this.escapeKeyword(toSnakeCase(arg.name));
this.emit(`\t\t\t\tif data.has("${arg.name}") and data["${arg.name}"] != null:`);
if (arg.type.kind === 'enum') {
- const enumReverseLookup = toConstantCase(arg.type.name!) + '_FROM_STRING';
- this.emit(`\t\t\t\t\tvar enum_str = data["${arg.name}"]`);
- this.emit(`\t\t\t\t\tif enum_str is String and ${enumReverseLookup}.has(enum_str):`);
- this.emit(`\t\t\t\t\t\tobj.${argSnakeName} = ${enumReverseLookup}[enum_str]`);
- this.emit(`\t\t\t\t\telse:`);
- this.emit(`\t\t\t\t\t\tobj.${argSnakeName} = enum_str`);
+ this.emitEnumFromDictAssignment(
+ '\t\t\t\t\t',
+ `obj.${argSnakeName}`,
+ arg.type.name!,
+ `data["${arg.name}"]`
+ );
} else {
this.emit(`\t\t\t\t\tobj.${argSnakeName} = data["${arg.name}"]`);
}
diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd
index b471470ed..6cd124e05 100644
--- a/packages/gql/src/generated/types.gd
+++ b/packages/gql/src/generated/types.gd
@@ -1412,8 +1412,8 @@ class ProductAndroid:
obj.name_android = data["nameAndroid"]
if data.has("productStatusAndroid") and data["productStatusAndroid"] != null:
var enum_str = data["productStatusAndroid"]
- if enum_str is String and PRODUCT_STATUS_ANDROID_FROM_STRING.has(enum_str):
- obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN)
else:
obj.product_status_android = enum_str
if data.has("discountOffers") and data["discountOffers"] != null:
@@ -1822,8 +1822,8 @@ class ProductSubscriptionAndroid:
obj.name_android = data["nameAndroid"]
if data.has("productStatusAndroid") and data["productStatusAndroid"] != null:
var enum_str = data["productStatusAndroid"]
- if enum_str is String and PRODUCT_STATUS_ANDROID_FROM_STRING.has(enum_str):
- obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.product_status_android = PRODUCT_STATUS_ANDROID_FROM_STRING.get(enum_str, ProductStatusAndroid.UNKNOWN)
else:
obj.product_status_android = enum_str
if data.has("discountOffers") and data["discountOffers"] != null:
@@ -2240,8 +2240,8 @@ class PurchaseAndroid:
obj.purchase_token = data["purchaseToken"]
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("platform") and data["platform"] != null:
@@ -2254,8 +2254,8 @@ class PurchaseAndroid:
obj.quantity = data["quantity"]
if data.has("purchaseState") and data["purchaseState"] != null:
var enum_str = data["purchaseState"]
- if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN)
else:
obj.purchase_state = enum_str
if data.has("isAutoRenewing") and data["isAutoRenewing"] != null:
@@ -2351,8 +2351,8 @@ class PurchaseError:
var obj = PurchaseError.new()
if data.has("code") and data["code"] != null:
var enum_str = data["code"]
- if enum_str is String and ERROR_CODE_FROM_STRING.has(enum_str):
- obj.code = ERROR_CODE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.code = ERROR_CODE_FROM_STRING.get(enum_str, ErrorCode.UNKNOWN)
else:
obj.code = enum_str
if data.has("message") and data["message"] != null:
@@ -2448,8 +2448,8 @@ class PurchaseIOS:
obj.purchase_token = data["purchaseToken"]
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("platform") and data["platform"] != null:
@@ -2462,8 +2462,8 @@ class PurchaseIOS:
obj.quantity = data["quantity"]
if data.has("purchaseState") and data["purchaseState"] != null:
var enum_str = data["purchaseState"]
- if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN)
else:
obj.purchase_state = enum_str
if data.has("isAutoRenewing") and data["isAutoRenewing"] != null:
@@ -2524,8 +2524,8 @@ class PurchaseIOS:
obj.renewal_info_ios = data["renewalInfoIOS"]
if data.has("billingPlanTypeIOS") and data["billingPlanTypeIOS"] != null:
var enum_str = data["billingPlanTypeIOS"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.billing_plan_type_ios = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type_ios = enum_str
if data.has("commitmentInfoIOS") and data["commitmentInfoIOS"] != null:
@@ -2683,8 +2683,8 @@ class RenewalCommitmentInfoIOS:
obj.commitment_auto_renew_status = data["commitmentAutoRenewStatus"]
if data.has("commitmentRenewalBillingPlanType") and data["commitmentRenewalBillingPlanType"] != null:
var enum_str = data["commitmentRenewalBillingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.commitment_renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.commitment_renewal_billing_plan_type = enum_str
if data.has("commitmentRenewalDate") and data["commitmentRenewalDate"] != null:
@@ -2757,8 +2757,8 @@ class RenewalInfoIOS:
obj.renewal_offer_type = data["renewalOfferType"]
if data.has("renewalBillingPlanType") and data["renewalBillingPlanType"] != null:
var enum_str = data["renewalBillingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.renewal_billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.renewal_billing_plan_type = enum_str
if data.has("commitmentInfo") and data["commitmentInfo"] != null:
@@ -2834,16 +2834,16 @@ class RequestVerifyPurchaseWithIapkitResult:
var obj = RequestVerifyPurchaseWithIapkitResult.new()
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("isValid") and data["isValid"] != null:
obj.is_valid = data["isValid"]
if data.has("state") and data["state"] != null:
var enum_str = data["state"]
- if enum_str is String and IAPKIT_PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.state = IAPKIT_PURCHASE_STATE_FROM_STRING.get(enum_str, IapkitPurchaseState.UNKNOWN)
else:
obj.state = enum_str
return obj
@@ -3027,8 +3027,8 @@ class SubscriptionOffer:
obj.period_count = data["periodCount"]
if data.has("paymentMode") and data["paymentMode"] != null:
var enum_str = data["paymentMode"]
- if enum_str is String and PAYMENT_MODE_FROM_STRING.has(enum_str):
- obj.payment_mode = PAYMENT_MODE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.payment_mode = PAYMENT_MODE_FROM_STRING.get(enum_str, PaymentMode.UNKNOWN)
else:
obj.payment_mode = enum_str
if data.has("keyIdentifierIOS") and data["keyIdentifierIOS"] != null:
@@ -3179,8 +3179,8 @@ class SubscriptionPeriod:
var obj = SubscriptionPeriod.new()
if data.has("unit") and data["unit"] != null:
var enum_str = data["unit"]
- if enum_str is String and SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.has(enum_str):
- obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.unit = SUBSCRIPTION_PERIOD_UNIT_FROM_STRING.get(enum_str, SubscriptionPeriodUnit.UNKNOWN)
else:
obj.unit = enum_str
if data.has("value") and data["value"] != null:
@@ -3240,8 +3240,8 @@ class SubscriptionPricingTermsIOS:
obj.billing_period = data["billingPeriod"]
if data.has("billingPlanType") and data["billingPlanType"] != null:
var enum_str = data["billingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type = enum_str
if data.has("billingPrice") and data["billingPrice"] != null:
@@ -3676,8 +3676,8 @@ class WebhookEvent:
obj.product_id = data["productId"]
if data.has("subscriptionState") and data["subscriptionState"] != null:
var enum_str = data["subscriptionState"]
- if enum_str is String and SUBSCRIPTION_STATE_FROM_STRING.has(enum_str):
- obj.subscription_state = SUBSCRIPTION_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.subscription_state = SUBSCRIPTION_STATE_FROM_STRING.get(enum_str, SubscriptionState.UNKNOWN)
else:
obj.subscription_state = enum_str
if data.has("expiresAt") and data["expiresAt"] != null:
@@ -4046,8 +4046,8 @@ class PurchaseInput:
obj.purchase_token = data["purchaseToken"]
if data.has("store") and data["store"] != null:
var enum_str = data["store"]
- if enum_str is String and IAP_STORE_FROM_STRING.has(enum_str):
- obj.store = IAP_STORE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.store = IAP_STORE_FROM_STRING.get(enum_str, IapStore.UNKNOWN)
else:
obj.store = enum_str
if data.has("platform") and data["platform"] != null:
@@ -4060,8 +4060,8 @@ class PurchaseInput:
obj.quantity = data["quantity"]
if data.has("purchaseState") and data["purchaseState"] != null:
var enum_str = data["purchaseState"]
- if enum_str is String and PURCHASE_STATE_FROM_STRING.has(enum_str):
- obj.purchase_state = PURCHASE_STATE_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.purchase_state = PURCHASE_STATE_FROM_STRING.get(enum_str, PurchaseState.UNKNOWN)
else:
obj.purchase_state = enum_str
if data.has("isAutoRenewing") and data["isAutoRenewing"] != null:
@@ -4494,8 +4494,8 @@ class RequestSubscriptionIosProps:
obj.promotional_offer_jws = data["promotionalOfferJWS"]
if data.has("billingPlanType") and data["billingPlanType"] != null:
var enum_str = data["billingPlanType"]
- if enum_str is String and SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.has(enum_str):
- obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING[enum_str]
+ if enum_str is String:
+ obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type = enum_str
if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
From 79a63c4c09c114356601de897704d31eb11078ec Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:42:57 +0900
Subject: [PATCH 7/8] fix(sdk): align ios subscription request options
---
.../expo-iap/src/__tests__/index.test.ts | 4 +--
libraries/expo-iap/src/types.ts | 10 +++----
.../flutter_inapp_purchase/lib/builders.dart | 14 +++++-----
.../flutter_inapp_purchase/lib/types.dart | 16 +++++------
.../flutter_inapp_purchase/lib/utils.dart | 7 +++--
.../flutter_inapp_purchase_channel_test.dart | 4 +--
libraries/godot-iap/addons/godot-iap/types.gd | 12 ++++-----
.../github/hyochan/kmpiap/dsl/PurchaseDsl.kt | 11 +++-----
.../io/github/hyochan/kmpiap/openiap/Types.kt | 16 +++++------
.../hyochan/kmpiap/InAppPurchaseTest.kt | 6 +++++
.../github/hyochan/kmpiap/InAppPurchaseIOS.kt | 12 +++++----
.../ApiDefinition.cs | 4 +--
libraries/maui-iap/src/OpenIap.Maui/Types.cs | 12 ++++-----
.../react-native-iap/ios/HybridRnIap.swift | 4 +--
.../src/__tests__/index.test.ts | 4 +--
libraries/react-native-iap/src/index.ts | 7 ++---
.../react-native-iap/src/specs/RnIap.nitro.ts | 8 +++---
libraries/react-native-iap/src/types.ts | 10 +++----
.../Sources/Helpers/StoreKitTypesBridge.swift | 8 +++---
packages/apple/Sources/Models/Types.swift | 14 +++++-----
.../apple/Sources/OpenIapModule+ObjC.swift | 27 ++++++++++++++-----
packages/apple/Tests/OpenIapTests.swift | 22 +++++++--------
packages/docs/src/lib/searchData.ts | 2 +-
.../docs/features/subscription/index.tsx | 10 +++----
.../ios/subscription-billing-plan-ios.tsx | 4 +--
.../docs/types/request-purchase-props.tsx | 8 +++---
.../docs/src/pages/docs/updates/releases.tsx | 13 +++++++--
.../src/main/java/dev/hyo/openiap/Types.kt | 16 +++++------
packages/gql/src/generated/Types.cs | 12 ++++-----
packages/gql/src/generated/Types.kt | 16 +++++------
packages/gql/src/generated/Types.swift | 14 +++++-----
packages/gql/src/generated/types.dart | 16 +++++------
packages/gql/src/generated/types.gd | 12 ++++-----
packages/gql/src/generated/types.ts | 10 +++----
packages/gql/src/type-ios.graphql | 10 +++----
scripts/audit-non-godot-parity.mjs | 2 +-
36 files changed, 200 insertions(+), 177 deletions(-)
diff --git a/libraries/expo-iap/src/__tests__/index.test.ts b/libraries/expo-iap/src/__tests__/index.test.ts
index 807389842..d380134c8 100644
--- a/libraries/expo-iap/src/__tests__/index.test.ts
+++ b/libraries/expo-iap/src/__tests__/index.test.ts
@@ -873,7 +873,7 @@ describe('Public API (index.ts)', () => {
apple: {
sku: 'com.example.subscription.monthly',
billingPlanType: 'monthly',
- introductoryOfferEligibility: true,
+ compactJWS: 'intro-eligibility-jws',
promotionalOfferJWS: {
offerId: 'promo-offer',
jws: 'compact-jws',
@@ -892,7 +892,7 @@ describe('Public API (index.ts)', () => {
ios: {
sku: 'com.example.subscription.monthly',
billingPlanType: 'monthly',
- introductoryOfferEligibility: true,
+ compactJWS: 'intro-eligibility-jws',
promotionalOfferJWS: {
offerId: 'promo-offer',
jws: 'compact-jws',
diff --git a/libraries/expo-iap/src/types.ts b/libraries/expo-iap/src/types.ts
index e62ef2b05..4df08dad3 100644
--- a/libraries/expo-iap/src/types.ts
+++ b/libraries/expo-iap/src/types.ts
@@ -1680,12 +1680,12 @@ export interface RequestSubscriptionIosProps {
*/
billingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ * Generate the JWS on your server and pass it to StoreKit's
+ * introductoryOfferEligibility(compactJWS:) purchase option.
*/
- introductoryOfferEligibility?: (boolean | null);
+ compactJWS?: (string | null);
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
diff --git a/libraries/flutter_inapp_purchase/lib/builders.dart b/libraries/flutter_inapp_purchase/lib/builders.dart
index 910cfadcc..af30414a2 100644
--- a/libraries/flutter_inapp_purchase/lib/builders.dart
+++ b/libraries/flutter_inapp_purchase/lib/builders.dart
@@ -20,9 +20,9 @@ class RequestPurchaseIosBuilder {
/// Billing plan for annual subscriptions with monthly commitment (iOS 26.4+)
SubscriptionBillingPlanTypeIOS? billingPlanType;
- /// Override introductory offer eligibility when this builder is used with
- /// subscriptions (iOS 15+)
- bool? introductoryOfferEligibility;
+ /// Compact JWS for overriding introductory offer eligibility when this
+ /// builder is used with subscriptions (iOS 15+)
+ String? compactJWS;
RequestPurchaseIosBuilder();
@@ -57,8 +57,8 @@ class RequestSubscriptionIosBuilder {
/// Billing plan for annual subscriptions with monthly commitment (iOS 26.4+)
SubscriptionBillingPlanTypeIOS? billingPlanType;
- /// Override introductory offer eligibility (iOS 15+)
- bool? introductoryOfferEligibility;
+ /// Compact JWS for overriding introductory offer eligibility (iOS 15+)
+ String? compactJWS;
RequestSubscriptionIosBuilder();
@@ -74,7 +74,7 @@ class RequestSubscriptionIosBuilder {
billingPlanType: billingPlanType,
winBackOffer: winBackOffer,
promotionalOfferJWS: promotionalOfferJWS,
- introductoryOfferEligibility: introductoryOfferEligibility,
+ compactJWS: compactJWS,
);
}
}
@@ -196,7 +196,7 @@ class RequestPurchaseBuilder {
billingPlanType: ios.billingPlanType,
winBackOffer: ios.winBackOffer,
promotionalOfferJWS: ios.promotionalOfferJWS,
- introductoryOfferEligibility: ios.introductoryOfferEligibility,
+ compactJWS: ios.compactJWS,
);
final androidSub = androidProps == null
diff --git a/libraries/flutter_inapp_purchase/lib/types.dart b/libraries/flutter_inapp_purchase/lib/types.dart
index bbe4f26b5..f6209618a 100644
--- a/libraries/flutter_inapp_purchase/lib/types.dart
+++ b/libraries/flutter_inapp_purchase/lib/types.dart
@@ -4854,7 +4854,7 @@ class RequestSubscriptionIosProps {
this.andDangerouslyFinishTransactionAutomatically,
this.appAccountToken,
this.billingPlanType,
- this.introductoryOfferEligibility,
+ this.compactJWS,
this.promotionalOfferJWS,
this.quantity,
required this.sku,
@@ -4872,11 +4872,11 @@ class RequestSubscriptionIosProps {
/// Billing plan to use when purchasing an annual subscription that offers
/// monthly billing with a 12-month commitment (iOS 26.4+).
final SubscriptionBillingPlanTypeIOS? billingPlanType;
- /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
- /// Set to true to indicate the user is eligible for introductory offer,
- /// or false to indicate they are not. When nil, the system determines eligibility.
- /// Back-deployed to iOS 15.
- final bool? introductoryOfferEligibility;
+ /// Compact JWS string for overriding introductory offer eligibility
+ /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ /// Generate the JWS on your server and pass it to StoreKit's
+ /// introductoryOfferEligibility(compactJWS:) purchase option.
+ final String? compactJWS;
/// JWS promotional offer (iOS 15+, WWDC 2025).
/// New signature format using compact JWS string for promotional offers.
/// Back-deployed to iOS 15.
@@ -4898,7 +4898,7 @@ class RequestSubscriptionIosProps {
andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?,
appAccountToken: json['appAccountToken'] as String?,
billingPlanType: json['billingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String) : null,
- introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?,
+ compactJWS: json['compactJWS'] as String?,
promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null,
quantity: json['quantity'] as int?,
sku: json['sku'] as String,
@@ -4913,7 +4913,7 @@ class RequestSubscriptionIosProps {
'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically,
'appAccountToken': appAccountToken,
'billingPlanType': billingPlanType?.toJson(),
- 'introductoryOfferEligibility': introductoryOfferEligibility,
+ 'compactJWS': compactJWS,
'promotionalOfferJWS': promotionalOfferJWS?.toJson(),
'quantity': quantity,
'sku': sku,
diff --git a/libraries/flutter_inapp_purchase/lib/utils.dart b/libraries/flutter_inapp_purchase/lib/utils.dart
index fcd063a84..2ce404345 100644
--- a/libraries/flutter_inapp_purchase/lib/utils.dart
+++ b/libraries/flutter_inapp_purchase/lib/utils.dart
@@ -87,10 +87,9 @@ Map? buildIosPurchasePayload(
payload['billingPlanType'] = billingPlanType;
}
- final dynamic introductoryOfferEligibility =
- propsJson['introductoryOfferEligibility'];
- if (introductoryOfferEligibility is bool) {
- payload['introductoryOfferEligibility'] = introductoryOfferEligibility;
+ final String? compactJWS = propsJson['compactJWS'] as String?;
+ if (compactJWS != null && compactJWS.isNotEmpty) {
+ payload['compactJWS'] = compactJWS;
}
final dynamic promotionalOfferJWS = propsJson['promotionalOfferJWS'];
diff --git a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart
index e7cf2a837..4e8d1881b 100644
--- a/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart
+++ b/libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart
@@ -523,7 +523,7 @@ void main() {
apple: types.RequestSubscriptionIosProps(
sku: 'ios.sub',
billingPlanType: types.SubscriptionBillingPlanTypeIOS.Monthly,
- introductoryOfferEligibility: true,
+ compactJWS: 'intro-eligibility-jws',
promotionalOfferJWS: types.PromotionalOfferJWSInputIOS(
offerId: 'promo-offer',
jws: 'header.payload.signature',
@@ -548,7 +548,7 @@ void main() {
expect(payload['sku'], 'ios.sub');
expect(payload['type'], 'subs');
expect(payload['billingPlanType'], 'monthly');
- expect(payload['introductoryOfferEligibility'], isTrue);
+ expect(payload['compactJWS'], 'intro-eligibility-jws');
final promotionalOfferJWS = Map.from(
payload['promotionalOfferJWS'] as Map,
);
diff --git a/libraries/godot-iap/addons/godot-iap/types.gd b/libraries/godot-iap/addons/godot-iap/types.gd
index 6cd124e05..8174f3048 100644
--- a/libraries/godot-iap/addons/godot-iap/types.gd
+++ b/libraries/godot-iap/addons/godot-iap/types.gd
@@ -4462,8 +4462,8 @@ class RequestSubscriptionIosProps:
var promotional_offer_jws: PromotionalOfferJWSInputIOS
## Billing plan to use when purchasing an annual subscription that offers
var billing_plan_type: SubscriptionBillingPlanTypeIOS
- ## Override introductory offer eligibility (iOS 15+, WWDC 2025).
- var introductory_offer_eligibility: Variant = null
+ ## Compact JWS string for overriding introductory offer eligibility
+ var compact_jws: Variant = null
## Advanced commerce data token (iOS 15+).
var advanced_commerce_data: Variant = null
@@ -4498,8 +4498,8 @@ class RequestSubscriptionIosProps:
obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type = enum_str
- if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
- obj.introductory_offer_eligibility = data["introductoryOfferEligibility"]
+ if data.has("compactJWS") and data["compactJWS"] != null:
+ obj.compact_jws = data["compactJWS"]
if data.has("advancedCommerceData") and data["advancedCommerceData"] != null:
obj.advanced_commerce_data = data["advancedCommerceData"]
return obj
@@ -4534,8 +4534,8 @@ class RequestSubscriptionIosProps:
dict["billingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type]
else:
dict["billingPlanType"] = billing_plan_type
- if introductory_offer_eligibility != null:
- dict["introductoryOfferEligibility"] = introductory_offer_eligibility
+ if compact_jws != null:
+ dict["compactJWS"] = compact_jws
if advanced_commerce_data != null:
dict["advancedCommerceData"] = advanced_commerce_data
return dict
diff --git a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt
index 42773e86f..7a23aaac2 100644
--- a/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt
+++ b/libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/dsl/PurchaseDsl.kt
@@ -146,13 +146,10 @@ class IosOptionsBuilder {
*/
var billingPlanType: SubscriptionBillingPlanTypeIOS? = null
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When null, the system determines eligibility.
- * Back-deployed to iOS 15. Requires Xcode 16.4+ to compile.
- * Added in openiap-gql v1.3.13 / openiap-apple v1.3.11
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When null, the system determines eligibility.
*/
- var introductoryOfferEligibility: Boolean? = null
+ var compactJWS: String? = null
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
@@ -186,7 +183,7 @@ class IosOptionsBuilder {
withOffer = withOffer,
advancedCommerceData = advancedCommerceData,
billingPlanType = billingPlanType,
- introductoryOfferEligibility = introductoryOfferEligibility,
+ compactJWS = compactJWS,
promotionalOfferJWS = promotionalOfferJWS,
winBackOffer = winBackOffer
)
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 e4073ca0b..af7c0d48c 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
@@ -4947,12 +4947,12 @@ public data class RequestSubscriptionIosProps(
*/
val billingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ * Generate the JWS on your server and pass it to StoreKit's
+ * introductoryOfferEligibility(compactJWS:) purchase option.
*/
- val introductoryOfferEligibility: Boolean? = null,
+ val compactJWS: String? = null,
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
@@ -4980,7 +4980,7 @@ public data class RequestSubscriptionIosProps(
val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean
val appAccountToken = json["appAccountToken"] as? String
val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }
- val introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean
+ val compactJWS = json["compactJWS"] as? String
val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }
val quantity = (json["quantity"] as? Number)?.toInt()
val sku = json["sku"] as? String
@@ -4992,7 +4992,7 @@ public data class RequestSubscriptionIosProps(
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
appAccountToken = appAccountToken,
billingPlanType = billingPlanType,
- introductoryOfferEligibility = introductoryOfferEligibility,
+ compactJWS = compactJWS,
promotionalOfferJWS = promotionalOfferJWS,
quantity = quantity,
sku = sku,
@@ -5007,7 +5007,7 @@ public data class RequestSubscriptionIosProps(
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
"billingPlanType" to billingPlanType?.toJson(),
- "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "compactJWS" to compactJWS,
"promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
diff --git a/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt b/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt
index 50d37fcee..b85a68b62 100644
--- a/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt
+++ b/libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/InAppPurchaseTest.kt
@@ -175,6 +175,12 @@ class InAppPurchaseTest {
assertEquals("com.example.subscription.monthly", props.sku)
assertEquals("campaign_q4_2025", props.advancedCommerceData)
assertEquals(SubscriptionBillingPlanTypeIOS.Monthly, props.billingPlanType)
+
+ val decoded = RequestSubscriptionIosProps.fromJson(props.toJson())!!
+
+ assertEquals("com.example.subscription.monthly", decoded.sku)
+ assertEquals("campaign_q4_2025", decoded.advancedCommerceData)
+ assertEquals(SubscriptionBillingPlanTypeIOS.Monthly, decoded.billingPlanType)
}
// =========================================================================
diff --git a/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt b/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
index 545ade47a..0fa7c92d5 100644
--- a/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
+++ b/libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt
@@ -1168,11 +1168,13 @@ internal class InAppPurchaseIOS : KmpInAppPurchase {
private fun convertAnyListToSubscriptionPricingTermsIOS(data: Any?): List? {
val list = data as? List<*> ?: return null
- return list.mapNotNull { item ->
- mapFromAny(item)?.let { map ->
- runCatching { SubscriptionPricingTermsIOS.fromJson(map) }.getOrNull()
- }
- }.ifEmpty { null }
+ val decoded = mutableListOf()
+ for (item in list) {
+ val map = mapFromAny(item) ?: return null
+ val term = runCatching { SubscriptionPricingTermsIOS.fromJson(map) }.getOrElse { return null }
+ decoded += term
+ }
+ return decoded.ifEmpty { null }
}
@Suppress("UNCHECKED_CAST")
diff --git a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs
index 54b8fda7d..c6a338ac2 100644
--- a/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs
+++ b/libraries/maui-iap/src/OpenIap.Maui.Bindings.iOS/ApiDefinition.cs
@@ -56,12 +56,12 @@ interface OpenIapModule
[Async]
void RequestSubscription(string sku, [NullAllowed] NSDictionary offer, Action completion);
- [Export("requestSubscriptionWithSku:offer:introductoryOfferEligibility:promotionalOfferJWS:winBackOfferId:billingPlanType:completion:")]
+ [Export("requestSubscriptionWithSku:offer:compactJWS:promotionalOfferJWS:winBackOfferId:billingPlanType:completion:")]
[Async]
void RequestSubscriptionExtended(
string sku,
[NullAllowed] NSDictionary offer,
- [NullAllowed] NSNumber introductoryOfferEligibility,
+ [NullAllowed] string compactJWS,
[NullAllowed] NSDictionary promotionalOfferJWS,
[NullAllowed] string winBackOfferId,
[NullAllowed] string billingPlanType,
diff --git a/libraries/maui-iap/src/OpenIap.Maui/Types.cs b/libraries/maui-iap/src/OpenIap.Maui/Types.cs
index 339cb9ecf..cac2ea838 100644
--- a/libraries/maui-iap/src/OpenIap.Maui/Types.cs
+++ b/libraries/maui-iap/src/OpenIap.Maui/Types.cs
@@ -4060,12 +4060,12 @@ public sealed record RequestSubscriptionIosProps
/// monthly billing with a 12-month commitment (iOS 26.4+).
[JsonPropertyName("billingPlanType")]
public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; }
- /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
- /// Set to true to indicate the user is eligible for introductory offer,
- /// or false to indicate they are not. When nil, the system determines eligibility.
- /// Back-deployed to iOS 15.
- [JsonPropertyName("introductoryOfferEligibility")]
- public bool? IntroductoryOfferEligibility { get; init; }
+ /// Compact JWS string for overriding introductory offer eligibility
+ /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ /// Generate the JWS on your server and pass it to StoreKit's
+ /// introductoryOfferEligibility(compactJWS:) purchase option.
+ [JsonPropertyName("compactJWS")]
+ public string? CompactJws { get; init; }
/// Advanced commerce data token (iOS 15+).
/// Used with StoreKit 2's Product.PurchaseOption.custom API for passing
/// campaign tokens, affiliate IDs, or other attribution data.
diff --git a/libraries/react-native-iap/ios/HybridRnIap.swift b/libraries/react-native-iap/ios/HybridRnIap.swift
index 27fd448a8..44a4f70d9 100644
--- a/libraries/react-native-iap/ios/HybridRnIap.swift
+++ b/libraries/react-native-iap/ios/HybridRnIap.swift
@@ -212,8 +212,8 @@ class HybridRnIap: HybridRnIapSpec {
iosPayload["billingPlanType"] = billingPlanType
}
// WWDC 2025 / iOS 18+ subscription offer fields
- if case .second(let introductoryOfferEligibility) = iosRequest.introductoryOfferEligibility {
- iosPayload["introductoryOfferEligibility"] = introductoryOfferEligibility
+ if case .second(let compactJWS) = iosRequest.compactJWS {
+ iosPayload["compactJWS"] = compactJWS
}
if case .second(let promotionalOfferJWS) = iosRequest.promotionalOfferJWS {
iosPayload["promotionalOfferJWS"] = [
diff --git a/libraries/react-native-iap/src/__tests__/index.test.ts b/libraries/react-native-iap/src/__tests__/index.test.ts
index 1382a8bb6..b407a27c6 100644
--- a/libraries/react-native-iap/src/__tests__/index.test.ts
+++ b/libraries/react-native-iap/src/__tests__/index.test.ts
@@ -861,7 +861,7 @@ describe('Public API (src/index.ts)', () => {
apple: {
sku: 'premium_sub',
billingPlanType: 'monthly',
- introductoryOfferEligibility: false,
+ compactJWS: 'intro-eligibility-jws',
promotionalOfferJWS: {
offerId: 'promo-offer',
jws: 'compact-jws',
@@ -875,7 +875,7 @@ describe('Public API (src/index.ts)', () => {
});
const passed = mockIap.requestPurchase.mock.calls.pop()?.[0];
expect(passed.ios.billingPlanType).toBe('monthly');
- expect(passed.ios.introductoryOfferEligibility).toBe(false);
+ expect(passed.ios.compactJWS).toBe('intro-eligibility-jws');
expect(passed.ios.promotionalOfferJWS).toEqual({
offerId: 'promo-offer',
jws: 'compact-jws',
diff --git a/libraries/react-native-iap/src/index.ts b/libraries/react-native-iap/src/index.ts
index b38bf0f0d..c6ad3c5b3 100644
--- a/libraries/react-native-iap/src/index.ts
+++ b/libraries/react-native-iap/src/index.ts
@@ -1724,11 +1724,8 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
if (subscriptionRequest.billingPlanType) {
iosPayload.billingPlanType = subscriptionRequest.billingPlanType;
}
- if (
- subscriptionRequest.introductoryOfferEligibility !== undefined
- ) {
- iosPayload.introductoryOfferEligibility =
- subscriptionRequest.introductoryOfferEligibility;
+ if (subscriptionRequest.compactJWS !== undefined) {
+ iosPayload.compactJWS = subscriptionRequest.compactJWS;
}
if (subscriptionRequest.promotionalOfferJWS) {
iosPayload.promotionalOfferJWS =
diff --git a/libraries/react-native-iap/src/specs/RnIap.nitro.ts b/libraries/react-native-iap/src/specs/RnIap.nitro.ts
index 0458e080b..0403aa5d7 100644
--- a/libraries/react-native-iap/src/specs/RnIap.nitro.ts
+++ b/libraries/react-native-iap/src/specs/RnIap.nitro.ts
@@ -182,13 +182,11 @@ export interface NitroRequestPurchaseIos {
*/
billingPlanType?: RequestSubscriptionIosProps['billingPlanType'];
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
* @platform iOS
*/
- introductoryOfferEligibility?: boolean | null;
+ compactJWS?: RequestSubscriptionIosProps['compactJWS'];
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
diff --git a/libraries/react-native-iap/src/types.ts b/libraries/react-native-iap/src/types.ts
index e62ef2b05..4df08dad3 100644
--- a/libraries/react-native-iap/src/types.ts
+++ b/libraries/react-native-iap/src/types.ts
@@ -1680,12 +1680,12 @@ export interface RequestSubscriptionIosProps {
*/
billingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ * Generate the JWS on your server and pass it to StoreKit's
+ * introductoryOfferEligibility(compactJWS:) purchase option.
*/
- introductoryOfferEligibility?: (boolean | null);
+ compactJWS?: (string | null);
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
index a110db8ba..a078ccf4a 100644
--- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
+++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
@@ -505,13 +505,13 @@ enum StoreKitTypesBridge {
}
// Introductory Offer Eligibility Override (iOS 15+, WWDC 2025)
- // Allows overriding the system's eligibility check for introductory offers
+ // Allows overriding the system's eligibility check with a server-signed JWS
// Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile
- if let eligibility = subscriptionProps.introductoryOfferEligibility {
+ if let compactJWS = subscriptionProps.compactJWS {
#if swift(>=6.1)
// Swift 6.1+ implementation
- options.insert(.introductoryOfferEligibility(eligibility))
- OpenIapLog.debug("✅ Added introductory offer eligibility override: \(eligibility)")
+ options.insert(.introductoryOfferEligibility(compactJWS: compactJWS))
+ OpenIapLog.debug("✅ Added introductory offer eligibility override")
#else
// Swift < 6.1: API not available, throw error to fail fast
OpenIapLog.error("❌ Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+")
diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift
index 95327c44e..b45414851 100644
--- a/packages/apple/Sources/Models/Types.swift
+++ b/packages/apple/Sources/Models/Types.swift
@@ -1986,11 +1986,11 @@ public struct RequestSubscriptionIosProps: Codable {
/// Billing plan to use when purchasing an annual subscription that offers
/// monthly billing with a 12-month commitment (iOS 26.4+).
public var billingPlanType: SubscriptionBillingPlanTypeIOS?
- /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
- /// Set to true to indicate the user is eligible for introductory offer,
- /// or false to indicate they are not. When nil, the system determines eligibility.
- /// Back-deployed to iOS 15.
- public var introductoryOfferEligibility: Bool?
+ /// Compact JWS string for overriding introductory offer eligibility
+ /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ /// Generate the JWS on your server and pass it to StoreKit's
+ /// introductoryOfferEligibility(compactJWS:) purchase option.
+ public var compactJWS: String?
/// JWS promotional offer (iOS 15+, WWDC 2025).
/// New signature format using compact JWS string for promotional offers.
/// Back-deployed to iOS 15.
@@ -2011,7 +2011,7 @@ public struct RequestSubscriptionIosProps: Codable {
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
billingPlanType: SubscriptionBillingPlanTypeIOS? = nil,
- introductoryOfferEligibility: Bool? = nil,
+ compactJWS: String? = nil,
promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
sku: String,
@@ -2022,7 +2022,7 @@ public struct RequestSubscriptionIosProps: Codable {
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
self.billingPlanType = billingPlanType
- self.introductoryOfferEligibility = introductoryOfferEligibility
+ self.compactJWS = compactJWS
self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
self.sku = sku
diff --git a/packages/apple/Sources/OpenIapModule+ObjC.swift b/packages/apple/Sources/OpenIapModule+ObjC.swift
index 3b60deca3..c7c5e4586 100644
--- a/packages/apple/Sources/OpenIapModule+ObjC.swift
+++ b/packages/apple/Sources/OpenIapModule+ObjC.swift
@@ -199,7 +199,7 @@ import StoreKit
requestSubscriptionWithSku(
sku,
offer: offer,
- introductoryOfferEligibility: nil,
+ compactJWS: nil,
promotionalOfferJWS: nil,
winBackOfferId: nil,
billingPlanType: nil,
@@ -211,7 +211,7 @@ import StoreKit
/// - Parameters:
/// - sku: The product SKU
/// - offer: Legacy promotional offer (DiscountOfferInputIOS)
- /// - introductoryOfferEligibility: Override introductory offer eligibility (iOS 15+, WWDC 2025)
+ /// - compactJWS: JWS for introductory offer eligibility override (iOS 15+, WWDC 2025)
/// - promotionalOfferJWS: JWS promotional offer dict with "offerId" and "jws" keys (iOS 15+, WWDC 2025)
/// - winBackOfferId: Win-back offer ID (iOS 18+)
/// - billingPlanType: Billing plan type ("monthly" or "up-front", iOS 26.4+)
@@ -219,7 +219,7 @@ import StoreKit
@objc func requestSubscriptionWithSku(
_ sku: String,
offer: [String: Any]?,
- introductoryOfferEligibility: NSNumber?,
+ compactJWS: String?,
promotionalOfferJWS: [String: Any]?,
winBackOfferId: String?,
billingPlanType: String?,
@@ -261,15 +261,30 @@ import StoreKit
nil
}
- let billingPlan = billingPlanType.map {
- SubscriptionBillingPlanTypeIOS(rawValue: $0) ?? .unknown
+ let billingPlan: SubscriptionBillingPlanTypeIOS?
+ if let billingPlanType {
+ guard let parsedBillingPlan = SubscriptionBillingPlanTypeIOS(rawValue: billingPlanType),
+ parsedBillingPlan != .unknown else {
+ completion(
+ nil,
+ PurchaseError.make(
+ code: .developerError,
+ productId: sku,
+ message: "billingPlanType must be \"monthly\" or \"up-front\"."
+ )
+ )
+ return
+ }
+ billingPlan = parsedBillingPlan
+ } else {
+ billingPlan = nil
}
let iosProps = RequestSubscriptionIosProps(
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
billingPlanType: billingPlan,
- introductoryOfferEligibility: introductoryOfferEligibility?.boolValue,
+ compactJWS: compactJWS,
promotionalOfferJWS: jwsOffer,
sku: sku,
winBackOffer: winBack,
diff --git a/packages/apple/Tests/OpenIapTests.swift b/packages/apple/Tests/OpenIapTests.swift
index 705a85882..5ecb78084 100644
--- a/packages/apple/Tests/OpenIapTests.swift
+++ b/packages/apple/Tests/OpenIapTests.swift
@@ -546,7 +546,7 @@ final class OpenIapTests: XCTestCase {
advancedCommerceData: nil,
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
- introductoryOfferEligibility: nil,
+ compactJWS: nil,
promotionalOfferJWS: nil,
quantity: nil,
sku: "dev.hyo.subscription.monthly",
@@ -570,7 +570,7 @@ final class OpenIapTests: XCTestCase {
advancedCommerceData: nil,
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
- introductoryOfferEligibility: nil,
+ compactJWS: nil,
promotionalOfferJWS: jwsOffer,
quantity: nil,
sku: "dev.hyo.subscription.yearly",
@@ -590,12 +590,12 @@ final class OpenIapTests: XCTestCase {
XCTAssertEqual(decoded.promotionalOfferJWS?.jws, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...")
}
- func testRequestSubscriptionIosPropsWithIntroductoryOfferEligibility() throws {
+ func testRequestSubscriptionIosPropsWithIntroductoryOfferEligibilityCompactJWS() throws {
let props = RequestSubscriptionIosProps(
advancedCommerceData: nil,
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
- introductoryOfferEligibility: true,
+ compactJWS: "intro-eligibility-jws",
promotionalOfferJWS: nil,
quantity: nil,
sku: "dev.hyo.subscription.monthly",
@@ -604,12 +604,12 @@ final class OpenIapTests: XCTestCase {
)
XCTAssertEqual(props.sku, "dev.hyo.subscription.monthly")
- XCTAssertEqual(props.introductoryOfferEligibility, true)
+ XCTAssertEqual(props.compactJWS, "intro-eligibility-jws")
// Test encoding/decoding
let data = try JSONEncoder().encode(props)
let decoded = try JSONDecoder().decode(RequestSubscriptionIosProps.self, from: data)
- XCTAssertEqual(decoded.introductoryOfferEligibility, true)
+ XCTAssertEqual(decoded.compactJWS, "intro-eligibility-jws")
}
func testRequestSubscriptionIosPropsWithAllSubscriptionOnlyFields() throws {
@@ -628,7 +628,7 @@ final class OpenIapTests: XCTestCase {
andDangerouslyFinishTransactionAutomatically: false,
appAccountToken: "user-uuid-123",
billingPlanType: .monthly,
- introductoryOfferEligibility: true,
+ compactJWS: "intro-eligibility-jws",
promotionalOfferJWS: jwsOffer,
quantity: 1,
sku: "dev.hyo.subscription.premium",
@@ -643,7 +643,7 @@ final class OpenIapTests: XCTestCase {
XCTAssertEqual(props.advancedCommerceData, "campaign_data")
XCTAssertEqual(props.andDangerouslyFinishTransactionAutomatically, false)
XCTAssertEqual(props.billingPlanType, .monthly)
- XCTAssertEqual(props.introductoryOfferEligibility, true)
+ XCTAssertEqual(props.compactJWS, "intro-eligibility-jws")
XCTAssertEqual(props.winBackOffer?.offerId, "winback_offer")
XCTAssertEqual(props.promotionalOfferJWS?.offerId, "promo_offer")
XCTAssertEqual(props.withOffer?.keyIdentifier, "key123")
@@ -654,7 +654,7 @@ final class OpenIapTests: XCTestCase {
XCTAssertEqual(decoded.sku, props.sku)
XCTAssertEqual(decoded.billingPlanType, props.billingPlanType)
- XCTAssertEqual(decoded.introductoryOfferEligibility, props.introductoryOfferEligibility)
+ XCTAssertEqual(decoded.compactJWS, props.compactJWS)
XCTAssertEqual(decoded.winBackOffer?.offerId, props.winBackOffer?.offerId)
XCTAssertEqual(decoded.promotionalOfferJWS?.offerId, props.promotionalOfferJWS?.offerId)
}
@@ -666,7 +666,7 @@ final class OpenIapTests: XCTestCase {
andDangerouslyFinishTransactionAutomatically: nil,
appAccountToken: nil,
billingPlanType: .upFront,
- introductoryOfferEligibility: true,
+ compactJWS: "intro-eligibility-jws",
promotionalOfferJWS: PromotionalOfferJWSInputIOS(jws: "jws", offerId: "offer"),
quantity: nil,
sku: "sub_sku",
@@ -675,7 +675,7 @@ final class OpenIapTests: XCTestCase {
)
// These fields exist on RequestSubscriptionIosProps
- XCTAssertEqual(subscriptionProps.introductoryOfferEligibility, true)
+ XCTAssertEqual(subscriptionProps.compactJWS, "intro-eligibility-jws")
XCTAssertEqual(subscriptionProps.billingPlanType, .upFront)
XCTAssertNotNil(subscriptionProps.promotionalOfferJWS)
XCTAssertNotNil(subscriptionProps.winBackOffer)
diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts
index b06518af2..226a8221b 100644
--- a/packages/docs/src/lib/searchData.ts
+++ b/packages/docs/src/lib/searchData.ts
@@ -848,7 +848,7 @@ export const apiData: ApiItem[] = [
title: 'RequestSubscriptionIosProps',
category: 'Types (iOS)',
description:
- 'iOS subscription request parameters (same as RequestPurchaseIosProps)',
+ 'iOS subscription request parameters: billingPlanType, compactJWS, promotionalOfferJWS, winBackOffer',
path: '/docs/types/request-purchase-props',
},
{
diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx
index 47adc9c39..ac403dfae 100644
--- a/packages/docs/src/pages/docs/features/subscription/index.tsx
+++ b/packages/docs/src/pages/docs/features/subscription/index.tsx
@@ -57,11 +57,11 @@ function Subscription() {
- Availability: billing plan selection requires
- StoreKit 26.4 runtime support. If the app runs on an older Apple OS
- version, omit billingPlanType and let StoreKit purchase
- the default plan. Never send unknown as a purchase
- option.
+ Availability: billing plan selection requires iOS,
+ iPadOS, macOS, tvOS, or visionOS 26.4+ and an app compiled with the
+ 26.5 SDK or later. If the app runs on an older Apple OS version,
+ omit billingPlanType and let StoreKit purchase the
+ default plan. Never send unknown as a purchase option.
diff --git a/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx b/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx
index 494494a60..841d887cd 100644
--- a/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx
+++ b/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx
@@ -30,8 +30,8 @@ function SubscriptionBillingPlanIOS() {
Availability: billing plan selection requires iOS,
- macOS, tvOS, watchOS, or visionOS 26.4+ and an SDK compiled with
- StoreKit 26.4 symbols. On older runtimes, omit{' '}
+ iPadOS, macOS, tvOS, or visionOS 26.4+ and an app compiled with the
+ 26.5 SDK or later. On older runtimes, omit{' '}
billingPlanType and use the store's default billing plan.
- Override introductory offer eligibility (iOS 15+, WWDC
- 2025). Pass true/false to
- force, omit to let the system decide.
+ Compact JWS string for overriding introductory offer
+ eligibility (iOS 15+, WWDC 2025). Generate it on your
+ server and omit it to let the system decide.
diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx
index 527040c98..d14b7c0d1 100644
--- a/packages/docs/src/pages/docs/updates/releases.tsx
+++ b/packages/docs/src/pages/docs/updates/releases.tsx
@@ -82,8 +82,17 @@ function Releases() {
purchases now accept billingPlanType with{' '}
monthly and up-front values, mapping to
StoreKit's{' '}
- Product.PurchaseOption.billingPlanType on iOS, macOS,
- tvOS, watchOS, and visionOS 26.4+.
+ Product.PurchaseOption.billingPlanType on iOS,
+ iPadOS, macOS, tvOS, and visionOS 26.4+ when compiled with the
+ 26.5 SDK or later.
+
+
+ Introductory offer eligibility correction —{' '}
+ RequestSubscriptionIosProps.compactJWS replaces the
+ previous boolean-shaped eligibility override and maps to
+ StoreKit's{' '}
+ introductoryOfferEligibility(compactJWS:) purchase
+ option.
Pricing terms exposed — iOS subscription products
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 b4d5d28fd..cdb93df17 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
@@ -4823,12 +4823,12 @@ public data class RequestSubscriptionIosProps(
*/
val billingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ * Generate the JWS on your server and pass it to StoreKit's
+ * introductoryOfferEligibility(compactJWS:) purchase option.
*/
- val introductoryOfferEligibility: Boolean? = null,
+ val compactJWS: String? = null,
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
@@ -4856,7 +4856,7 @@ public data class RequestSubscriptionIosProps(
val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean
val appAccountToken = json["appAccountToken"] as? String
val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }
- val introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean
+ val compactJWS = json["compactJWS"] as? String
val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }
val quantity = (json["quantity"] as? Number)?.toInt()
val sku = json["sku"] as? String
@@ -4868,7 +4868,7 @@ public data class RequestSubscriptionIosProps(
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
appAccountToken = appAccountToken,
billingPlanType = billingPlanType,
- introductoryOfferEligibility = introductoryOfferEligibility,
+ compactJWS = compactJWS,
promotionalOfferJWS = promotionalOfferJWS,
quantity = quantity,
sku = sku,
@@ -4883,7 +4883,7 @@ public data class RequestSubscriptionIosProps(
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
"billingPlanType" to billingPlanType?.toJson(),
- "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "compactJWS" to compactJWS,
"promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
diff --git a/packages/gql/src/generated/Types.cs b/packages/gql/src/generated/Types.cs
index 339cb9ecf..cac2ea838 100644
--- a/packages/gql/src/generated/Types.cs
+++ b/packages/gql/src/generated/Types.cs
@@ -4060,12 +4060,12 @@ public sealed record RequestSubscriptionIosProps
/// monthly billing with a 12-month commitment (iOS 26.4+).
[JsonPropertyName("billingPlanType")]
public SubscriptionBillingPlanTypeIOS? BillingPlanType { get; init; }
- /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
- /// Set to true to indicate the user is eligible for introductory offer,
- /// or false to indicate they are not. When nil, the system determines eligibility.
- /// Back-deployed to iOS 15.
- [JsonPropertyName("introductoryOfferEligibility")]
- public bool? IntroductoryOfferEligibility { get; init; }
+ /// Compact JWS string for overriding introductory offer eligibility
+ /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ /// Generate the JWS on your server and pass it to StoreKit's
+ /// introductoryOfferEligibility(compactJWS:) purchase option.
+ [JsonPropertyName("compactJWS")]
+ public string? CompactJws { get; init; }
/// Advanced commerce data token (iOS 15+).
/// Used with StoreKit 2's Product.PurchaseOption.custom API for passing
/// campaign tokens, affiliate IDs, or other attribution data.
diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt
index 10f083550..1fc6849f3 100644
--- a/packages/gql/src/generated/Types.kt
+++ b/packages/gql/src/generated/Types.kt
@@ -4945,12 +4945,12 @@ public data class RequestSubscriptionIosProps(
*/
val billingPlanType: SubscriptionBillingPlanTypeIOS? = null,
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ * Generate the JWS on your server and pass it to StoreKit's
+ * introductoryOfferEligibility(compactJWS:) purchase option.
*/
- val introductoryOfferEligibility: Boolean? = null,
+ val compactJWS: String? = null,
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
@@ -4978,7 +4978,7 @@ public data class RequestSubscriptionIosProps(
val andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean
val appAccountToken = json["appAccountToken"] as? String
val billingPlanType = (json["billingPlanType"] as? String)?.let { SubscriptionBillingPlanTypeIOS.fromJson(it) }
- val introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean
+ val compactJWS = json["compactJWS"] as? String
val promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }
val quantity = (json["quantity"] as? Number)?.toInt()
val sku = json["sku"] as? String
@@ -4990,7 +4990,7 @@ public data class RequestSubscriptionIosProps(
andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically,
appAccountToken = appAccountToken,
billingPlanType = billingPlanType,
- introductoryOfferEligibility = introductoryOfferEligibility,
+ compactJWS = compactJWS,
promotionalOfferJWS = promotionalOfferJWS,
quantity = quantity,
sku = sku,
@@ -5005,7 +5005,7 @@ public data class RequestSubscriptionIosProps(
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
"appAccountToken" to appAccountToken,
"billingPlanType" to billingPlanType?.toJson(),
- "introductoryOfferEligibility" to introductoryOfferEligibility,
+ "compactJWS" to compactJWS,
"promotionalOfferJWS" to promotionalOfferJWS?.toJson(),
"quantity" to quantity,
"sku" to sku,
diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift
index 95327c44e..b45414851 100644
--- a/packages/gql/src/generated/Types.swift
+++ b/packages/gql/src/generated/Types.swift
@@ -1986,11 +1986,11 @@ public struct RequestSubscriptionIosProps: Codable {
/// Billing plan to use when purchasing an annual subscription that offers
/// monthly billing with a 12-month commitment (iOS 26.4+).
public var billingPlanType: SubscriptionBillingPlanTypeIOS?
- /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
- /// Set to true to indicate the user is eligible for introductory offer,
- /// or false to indicate they are not. When nil, the system determines eligibility.
- /// Back-deployed to iOS 15.
- public var introductoryOfferEligibility: Bool?
+ /// Compact JWS string for overriding introductory offer eligibility
+ /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ /// Generate the JWS on your server and pass it to StoreKit's
+ /// introductoryOfferEligibility(compactJWS:) purchase option.
+ public var compactJWS: String?
/// JWS promotional offer (iOS 15+, WWDC 2025).
/// New signature format using compact JWS string for promotional offers.
/// Back-deployed to iOS 15.
@@ -2011,7 +2011,7 @@ public struct RequestSubscriptionIosProps: Codable {
andDangerouslyFinishTransactionAutomatically: Bool? = nil,
appAccountToken: String? = nil,
billingPlanType: SubscriptionBillingPlanTypeIOS? = nil,
- introductoryOfferEligibility: Bool? = nil,
+ compactJWS: String? = nil,
promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil,
quantity: Int? = nil,
sku: String,
@@ -2022,7 +2022,7 @@ public struct RequestSubscriptionIosProps: Codable {
self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically
self.appAccountToken = appAccountToken
self.billingPlanType = billingPlanType
- self.introductoryOfferEligibility = introductoryOfferEligibility
+ self.compactJWS = compactJWS
self.promotionalOfferJWS = promotionalOfferJWS
self.quantity = quantity
self.sku = sku
diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart
index bbe4f26b5..f6209618a 100644
--- a/packages/gql/src/generated/types.dart
+++ b/packages/gql/src/generated/types.dart
@@ -4854,7 +4854,7 @@ class RequestSubscriptionIosProps {
this.andDangerouslyFinishTransactionAutomatically,
this.appAccountToken,
this.billingPlanType,
- this.introductoryOfferEligibility,
+ this.compactJWS,
this.promotionalOfferJWS,
this.quantity,
required this.sku,
@@ -4872,11 +4872,11 @@ class RequestSubscriptionIosProps {
/// Billing plan to use when purchasing an annual subscription that offers
/// monthly billing with a 12-month commitment (iOS 26.4+).
final SubscriptionBillingPlanTypeIOS? billingPlanType;
- /// Override introductory offer eligibility (iOS 15+, WWDC 2025).
- /// Set to true to indicate the user is eligible for introductory offer,
- /// or false to indicate they are not. When nil, the system determines eligibility.
- /// Back-deployed to iOS 15.
- final bool? introductoryOfferEligibility;
+ /// Compact JWS string for overriding introductory offer eligibility
+ /// (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ /// Generate the JWS on your server and pass it to StoreKit's
+ /// introductoryOfferEligibility(compactJWS:) purchase option.
+ final String? compactJWS;
/// JWS promotional offer (iOS 15+, WWDC 2025).
/// New signature format using compact JWS string for promotional offers.
/// Back-deployed to iOS 15.
@@ -4898,7 +4898,7 @@ class RequestSubscriptionIosProps {
andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?,
appAccountToken: json['appAccountToken'] as String?,
billingPlanType: json['billingPlanType'] != null ? SubscriptionBillingPlanTypeIOS.fromJson(json['billingPlanType'] as String) : null,
- introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?,
+ compactJWS: json['compactJWS'] as String?,
promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null,
quantity: json['quantity'] as int?,
sku: json['sku'] as String,
@@ -4913,7 +4913,7 @@ class RequestSubscriptionIosProps {
'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically,
'appAccountToken': appAccountToken,
'billingPlanType': billingPlanType?.toJson(),
- 'introductoryOfferEligibility': introductoryOfferEligibility,
+ 'compactJWS': compactJWS,
'promotionalOfferJWS': promotionalOfferJWS?.toJson(),
'quantity': quantity,
'sku': sku,
diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd
index 6cd124e05..8174f3048 100644
--- a/packages/gql/src/generated/types.gd
+++ b/packages/gql/src/generated/types.gd
@@ -4462,8 +4462,8 @@ class RequestSubscriptionIosProps:
var promotional_offer_jws: PromotionalOfferJWSInputIOS
## Billing plan to use when purchasing an annual subscription that offers
var billing_plan_type: SubscriptionBillingPlanTypeIOS
- ## Override introductory offer eligibility (iOS 15+, WWDC 2025).
- var introductory_offer_eligibility: Variant = null
+ ## Compact JWS string for overriding introductory offer eligibility
+ var compact_jws: Variant = null
## Advanced commerce data token (iOS 15+).
var advanced_commerce_data: Variant = null
@@ -4498,8 +4498,8 @@ class RequestSubscriptionIosProps:
obj.billing_plan_type = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_FROM_STRING.get(enum_str, SubscriptionBillingPlanTypeIOS.UNKNOWN)
else:
obj.billing_plan_type = enum_str
- if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null:
- obj.introductory_offer_eligibility = data["introductoryOfferEligibility"]
+ if data.has("compactJWS") and data["compactJWS"] != null:
+ obj.compact_jws = data["compactJWS"]
if data.has("advancedCommerceData") and data["advancedCommerceData"] != null:
obj.advanced_commerce_data = data["advancedCommerceData"]
return obj
@@ -4534,8 +4534,8 @@ class RequestSubscriptionIosProps:
dict["billingPlanType"] = SUBSCRIPTION_BILLING_PLAN_TYPE_IOS_VALUES[billing_plan_type]
else:
dict["billingPlanType"] = billing_plan_type
- if introductory_offer_eligibility != null:
- dict["introductoryOfferEligibility"] = introductory_offer_eligibility
+ if compact_jws != null:
+ dict["compactJWS"] = compact_jws
if advanced_commerce_data != null:
dict["advancedCommerceData"] = advanced_commerce_data
return dict
diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts
index e62ef2b05..4df08dad3 100644
--- a/packages/gql/src/generated/types.ts
+++ b/packages/gql/src/generated/types.ts
@@ -1680,12 +1680,12 @@ export interface RequestSubscriptionIosProps {
*/
billingPlanType?: (SubscriptionBillingPlanTypeIOS | null);
/**
- * Override introductory offer eligibility (iOS 15+, WWDC 2025).
- * Set to true to indicate the user is eligible for introductory offer,
- * or false to indicate they are not. When nil, the system determines eligibility.
- * Back-deployed to iOS 15.
+ * Compact JWS string for overriding introductory offer eligibility
+ * (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ * Generate the JWS on your server and pass it to StoreKit's
+ * introductoryOfferEligibility(compactJWS:) purchase option.
*/
- introductoryOfferEligibility?: (boolean | null);
+ compactJWS?: (string | null);
/**
* JWS promotional offer (iOS 15+, WWDC 2025).
* New signature format using compact JWS string for promotional offers.
diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql
index 750e933ef..9b9093899 100644
--- a/packages/gql/src/type-ios.graphql
+++ b/packages/gql/src/type-ios.graphql
@@ -344,12 +344,12 @@ input RequestSubscriptionIosProps {
"""
billingPlanType: SubscriptionBillingPlanTypeIOS
"""
- Override introductory offer eligibility (iOS 15+, WWDC 2025).
- Set to true to indicate the user is eligible for introductory offer,
- or false to indicate they are not. When nil, the system determines eligibility.
- Back-deployed to iOS 15.
+ Compact JWS string for overriding introductory offer eligibility
+ (iOS 15+, WWDC 2025). When nil, the system determines eligibility.
+ Generate the JWS on your server and pass it to StoreKit's
+ introductoryOfferEligibility(compactJWS:) purchase option.
"""
- introductoryOfferEligibility: Boolean
+ compactJWS: String
"""
Advanced commerce data token (iOS 15+).
Used with StoreKit 2's Product.PurchaseOption.custom API for passing
diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs
index f282c5e61..5728bd837 100644
--- a/scripts/audit-non-godot-parity.mjs
+++ b/scripts/audit-non-godot-parity.mjs
@@ -560,7 +560,7 @@ function checkFlutter() {
'Alternative Billing',
], `${base} widget tests`);
expectIncludes('libraries/flutter_inapp_purchase/lib/utils.dart', [
- 'introductoryOfferEligibility',
+ 'compactJWS',
'promotionalOfferJWS',
'winBackOffer',
], 'Flutter iOS purchase payload');
From 89ddc9d353912b0bc54f7c00ebeecfaf4b49e0f2 Mon Sep 17 00:00:00 2001
From: Hyo
Date: Thu, 2 Jul 2026 01:50:17 +0900
Subject: [PATCH 8/8] fix(apple): handle optional commitment preference
---
.../apple/Sources/Helpers/StoreKitTypesBridge.swift | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
index a078ccf4a..c83bee127 100644
--- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
+++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift
@@ -565,7 +565,7 @@ enum StoreKitTypesBridge {
if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {
guard let commitment = info.commitmentInfo else { return nil }
return RenewalCommitmentInfoIOS(
- commitmentAutoRenewProductId: commitment.autoRenewPreference,
+ commitmentAutoRenewProductId: commitmentAutoRenewProductId(from: commitment.autoRenewPreference),
commitmentAutoRenewStatus: commitment.willAutoRenew,
commitmentRenewalBillingPlanType: billingPlanTypeIOS(from: commitment.renewalBillingPlanType),
commitmentRenewalDate: commitment.renewalDate.milliseconds,
@@ -576,6 +576,14 @@ enum StoreKitTypesBridge {
return nil
}
+ private static func commitmentAutoRenewProductId(from preference: String) -> String {
+ preference
+ }
+
+ private static func commitmentAutoRenewProductId(from preference: String?) -> String {
+ preference ?? ""
+ }
+
static func renewalBillingPlanTypeIOS(from info: StoreKit.Product.SubscriptionInfo.RenewalInfo) -> SubscriptionBillingPlanTypeIOS? {
#if swift(>=6.3)
if #available(iOS 26.4, macOS 26.4, tvOS 26.4, watchOS 26.4, visionOS 26.4, *) {