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 fieldUse 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. +
    + RenewalInfoIOS.commitmentInfo + Inspecting the next commitment renewal state.
    + + + {{ + typescript: ( + {`import { fetchProducts, requestPurchase } from 'expo-iap'; +// Same API in react-native-iap. + +const [subscription] = await fetchProducts({ + skus: ['premium_annual'], + type: 'subs', +}); + +const monthlyPlan = subscription.pricingTermsIOS?.find( + (term) => term.billingPlanType === 'monthly', +); + +if (monthlyPlan) { + console.log('Billed:', monthlyPlan.billingDisplayPrice); + console.log('Commitment:', monthlyPlan.commitmentInfo.displayPrice); +} + +await requestPurchase({ + type: 'subs', + request: { + apple: { + sku: 'premium_annual', + billingPlanType: 'monthly', + }, + }, +});`} + ), + swift: ( + {`import OpenIap + +try await OpenIapModule.shared.requestPurchase( + RequestPurchaseProps( + request: .subscription( + RequestSubscriptionPropsByPlatforms( + apple: RequestSubscriptionIosProps( + sku: "premium_annual", + billingPlanType: .monthly + ) + ) + ), + type: .subs + ) +)`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP +import io.github.hyochan.kmpiap.requestPurchase +import io.github.hyochan.kmpiap.openiap.ProductType +import io.github.hyochan.kmpiap.openiap.SubscriptionBillingPlanTypeIOS + +val kmpIAP = KmpIAP() + +kmpIAP.requestPurchase { + type = ProductType.Subs + ios { + sku = "premium_annual" + billingPlanType = SubscriptionBillingPlanTypeIOS.Monthly + } +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +await FlutterInappPurchase.instance.requestPurchaseWithBuilder( + build: (builder) { + builder + ..type = ProductQueryType.Subs + ..ios.sku = 'premium_annual' + ..ios.billingPlanType = SubscriptionBillingPlanTypeIOS.Monthly; + }, +);`} + ), + csharp: ( + {`using OpenIap; +using OpenIap.Maui; + +await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync( + new RequestPurchaseProps + { + RequestSubscription = new RequestSubscriptionPropsByPlatforms + { + Apple = new RequestSubscriptionIosProps + { + Sku = "premium_annual", + BillingPlanType = SubscriptionBillingPlanTypeIOS.Monthly, + }, + }, + Type = ProductQueryType.Subs, + });`} + ), + gdscript: ( + {`var props = RequestPurchaseProps.new() +props.request = RequestSubscriptionPropsByPlatforms.new() +props.request.apple = RequestSubscriptionIosProps.new() +props.request.apple.sku = "premium_annual" +props.request.apple.billing_plan_type = SubscriptionBillingPlanTypeIOS.MONTHLY +props.type = ProductQueryType.SUBS + +await iap.request_purchase(props)`} + ), + }} + + +

    + See:{' '} + + Subscription Billing Plan iOS types + + {' · '} + + RequestSubscriptionIosProps + + {' · '} + PurchaseIOS +

    +
    +
    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. +

    + + + + + + + + + + + + + + + + + + + + + +
    ValueSummary
    + 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.
    + + {`type SubscriptionBillingPlanTypeIOS = 'unknown' | 'monthly' | 'up-front';`} +
    + +
    + + SubscriptionPricingTermsIOS + +

    + Pricing term returned on ProductIOS.pricingTermsIOS,{' '} + ProductSubscriptionIOS.pricingTermsIOS, and{' '} + SubscriptionInfoIOS.pricingTerms. Use it to display the + available commitment choices before starting a purchase. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + billingDisplayPrice + Localized billing price for this plan.
    + billingPeriod + + Billing cadence as a{' '} + + SubscriptionPeriodIOS + + . +
    + billingPlanType + + monthly, up-front, or{' '} + unknown. +
    + billingPrice + Numeric billing price for this plan.
    + commitmentInfo + + 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. +

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

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

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + 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.
    + commitmentRenewalPrice + Numeric renewal price for the next commitment.
    +
    + +

    + See also:{' '} + + iOS commitment billing plans guide + + {' · '} + requestPurchase + {' · '} + PurchaseIOS +

    +
    + ); +} + +export default SubscriptionBillingPlanIOS; diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx index b7862917d..f607c654d 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -238,6 +238,19 @@ function Product() { — unified across iOS/Android. + + + pricingTermsIOS + + + StoreKit 26.4 subscription billing plans, including + monthly commitment and up-front pricing terms. See{' '} + + SubscriptionPricingTermsIOS + + . + + jsonRepresentationIOS diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index f9a2bfc34..4a6de1309 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -370,6 +370,30 @@ function Purchase() { type, paymentMode + + + billingPlanTypeIOS + + + 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 + + . + + currencyCodeIOS diff --git a/packages/docs/src/pages/docs/types/request-purchase-props.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx index a83746004..334ce7b76 100644 --- a/packages/docs/src/pages/docs/types/request-purchase-props.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -571,6 +571,22 @@ await iap.request_purchase(subs_props)`} JWS-signed promotional offer (iOS 15+, WWDC 2025). + + + billingPlanType + + + 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 + + . + + typeIOS diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 7f1ba23ba..527040c98 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -26,6 +26,255 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // July 2, 2026 — iOS subscription commitment billing plans + { + id: 'ios-subscription-commitment-billing-plans-2026-07-02', + date: new Date('2026-07-02'), + element: ( +
    + + July 2, 2026 — iOS subscription commitment billing plans + + +

    + 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.

    diff --git a/packages/docs/src/pages/docs/types/request-purchase-props.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx index 334ce7b76..4c16b1a81 100644 --- a/packages/docs/src/pages/docs/types/request-purchase-props.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -589,12 +589,12 @@ await iap.request_purchase(subs_props)`} - introductoryOfferEligibility + compactJWS - 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, *) {