diff --git a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift index 90af35e6..14e799c1 100644 --- a/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift +++ b/packages/apple/Sources/Helpers/StoreKitTypesBridge.swift @@ -359,7 +359,7 @@ enum StoreKitTypesBridge { } } - static func purchaseOptions(from props: RequestPurchaseIosProps, product: StoreKit.Product? = nil) throws -> Set { + static func purchaseOptions(from props: some IosPropsProtocol, product: StoreKit.Product? = nil) throws -> Set { var options: Set = [] if let quantity = props.quantity, quantity > 1 { options.insert(.quantity(quantity)) @@ -377,88 +377,93 @@ enum StoreKitTypesBridge { } options.insert(option) } - // Win-back offers (iOS 18+) - // Used to re-engage churned subscribers - if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { - if let winBackInput = props.winBackOffer { - guard let product = product else { - OpenIapLog.error("❌ Win-back offer requires product context") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Win-back offer requires product context. Fetch the product before calling requestPurchase." - ) - } - // Find the win-back offer from the product's promotional offers - if let subscription = product.subscription { - let winBackOffer = subscription.promotionalOffers.first { offer in - offer.id == winBackInput.offerId && offer.type == .winBack + + // Subscription-only options (only available on RequestSubscriptionIosProps) + if let subscriptionProps = props as? RequestSubscriptionIosProps { + // Win-back offers (iOS 18+) + // Used to re-engage churned subscribers + if let winBackInput = subscriptionProps.winBackOffer { + if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + guard let product = product else { + OpenIapLog.error("❌ Win-back offer requires product context") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Win-back offer requires product context. Fetch the product before calling requestPurchase." + ) } - if let offer = winBackOffer { - options.insert(.winBackOffer(offer)) - OpenIapLog.debug("✅ Added win-back offer: \(winBackInput.offerId)") + // Find the win-back offer from the product's promotional offers + if let subscription = product.subscription { + let winBackOffer = subscription.promotionalOffers.first { offer in + offer.id == winBackInput.offerId && offer.type == .winBack + } + if let offer = winBackOffer { + options.insert(.winBackOffer(offer)) + OpenIapLog.debug("✅ Added win-back offer: \(winBackInput.offerId)") + } else { + OpenIapLog.error("❌ Win-back offer not found: \(winBackInput.offerId)") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Win-back offer not found: \(winBackInput.offerId). Ensure the user is eligible and the offer ID is correct." + ) + } } else { - OpenIapLog.error("❌ Win-back offer not found: \(winBackInput.offerId)") + OpenIapLog.error("❌ Win-back offer requires a subscription product") throw PurchaseError.make( code: .developerError, productId: props.sku, - message: "Win-back offer not found: \(winBackInput.offerId). Ensure the user is eligible and the offer ID is correct." + message: "Win-back offers can only be applied to subscription products" ) } } else { - OpenIapLog.error("❌ Win-back offer requires a subscription product") + // Fail fast when win-back offers are used on unsupported OS versions + OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+") throw PurchaseError.make( code: .developerError, productId: props.sku, - message: "Win-back offers can only be applied to subscription products" + message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." ) } } - } else if props.winBackOffer != nil { - // Fail fast when win-back offers are used on unsupported OS versions - OpenIapLog.error("❌ Win-back offers require iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Win-back offers are only supported on iOS 18+ / macOS 15+ / tvOS 18+ / watchOS 11+ / visionOS 2+." - ) - } - // JWS Promotional Offer (iOS 15+, WWDC 2025) - // New signature format using compact JWS string for promotional offers - // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile - if let jwsOffer = props.promotionalOfferJWS { - #if swift(>=6.1) - // Swift 6.1+ implementation - options.insert(.promotionalOffer(jwsOffer.jws)) - OpenIapLog.debug("✅ Added JWS promotional offer: \(jwsOffer.offerId)") - #else - // Swift < 6.1: API not available, throw error to fail fast - OpenIapLog.error("❌ JWS promotional offers require Xcode 16.4+ / Swift 6.1+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "JWS promotional offers require Xcode 16.4+ / Swift 6.1+. Use withOffer with signature-based promotional offers instead." - ) - #endif - } - - // Introductory Offer Eligibility Override (iOS 15+, WWDC 2025) - // Allows overriding the system's eligibility check for introductory offers - // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile - if let eligibility = props.introductoryOfferEligibility { - #if swift(>=6.1) - // Swift 6.1+ implementation - options.insert(.introductoryOfferEligibility(eligibility)) - OpenIapLog.debug("✅ Added introductory offer eligibility override: \(eligibility)") - #else - // Swift < 6.1: API not available, throw error to fail fast - OpenIapLog.error("❌ Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+") - throw PurchaseError.make( - code: .developerError, - productId: props.sku, - message: "Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+. The system will determine eligibility automatically." - ) - #endif + + // JWS Promotional Offer (iOS 15+, WWDC 2025) + // New signature format using compact JWS string for promotional offers + // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile + if let jwsOffer = subscriptionProps.promotionalOfferJWS { + #if swift(>=6.1) + // Swift 6.1+ implementation + options.insert(.promotionalOffer(compactJWS: jwsOffer.jws)) + OpenIapLog.debug("✅ Added JWS promotional offer: \(jwsOffer.offerId)") + #else + // Swift < 6.1: API not available, throw error to fail fast + OpenIapLog.error("❌ JWS promotional offers require Xcode 16.4+ / Swift 6.1+") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "JWS promotional offers require Xcode 16.4+ / Swift 6.1+. Use withOffer with signature-based promotional offers instead." + ) + #endif + } + + // Introductory Offer Eligibility Override (iOS 15+, WWDC 2025) + // Allows overriding the system's eligibility check for introductory offers + // Back-deployed to iOS 15, but requires Xcode 16.4+ / Swift 6.1+ to compile + if let eligibility = subscriptionProps.introductoryOfferEligibility { + #if swift(>=6.1) + // Swift 6.1+ implementation + options.insert(.introductoryOfferEligibility(eligibility)) + OpenIapLog.debug("✅ Added introductory offer eligibility override: \(eligibility)") + #else + // Swift < 6.1: API not available, throw error to fail fast + OpenIapLog.error("❌ Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+") + throw PurchaseError.make( + code: .developerError, + productId: props.sku, + message: "Introductory offer eligibility override requires Xcode 16.4+ / Swift 6.1+. The system will determine eligibility automatically." + ) + #endif + } } // Advanced Commerce Data (iOS 15+) diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index f176fbf0..19407cb1 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -1417,45 +1417,27 @@ public struct RequestPurchaseIosProps: Codable { public var andDangerouslyFinishTransactionAutomatically: Bool? /// App account token for user tracking public var appAccountToken: String? - /// Override introductory offer eligibility (iOS 15+, WWDC 2025). - /// Set to true to indicate the user is eligible for introductory offer, - /// or false to indicate they are not. When nil, the system determines eligibility. - /// Back-deployed to iOS 15. - public var introductoryOfferEligibility: Bool? - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. - public var promotionalOfferJWS: PromotionalOfferJWSInputIOS? /// Purchase quantity public var quantity: Int? /// Product SKU public var sku: String - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// Note: Win-back offers only apply to subscription products. - public var winBackOffer: WinBackOfferInputIOS? - /// Discount offer to apply + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. public var withOffer: DiscountOfferInputIOS? public init( advancedCommerceData: String? = nil, andDangerouslyFinishTransactionAutomatically: Bool? = nil, appAccountToken: String? = nil, - introductoryOfferEligibility: Bool? = nil, - promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil, quantity: Int? = nil, sku: String, - winBackOffer: WinBackOfferInputIOS? = nil, withOffer: DiscountOfferInputIOS? = nil ) { self.advancedCommerceData = advancedCommerceData self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically self.appAccountToken = appAccountToken - self.introductoryOfferEligibility = introductoryOfferEligibility - self.promotionalOfferJWS = promotionalOfferJWS self.quantity = quantity self.sku = sku - self.winBackOffer = winBackOffer self.withOffer = withOffer } } @@ -1630,6 +1612,8 @@ public struct RequestSubscriptionIosProps: Codable { /// The offer is available when the customer is eligible and can be discovered /// via StoreKit Message (automatic) or subscription offer APIs. public var winBackOffer: WinBackOfferInputIOS? + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. public var withOffer: DiscountOfferInputIOS? public init( diff --git a/packages/apple/Sources/OpenIapModule.swift b/packages/apple/Sources/OpenIapModule.swift index 2741bcd2..b0ef3f98 100644 --- a/packages/apple/Sources/OpenIapModule.swift +++ b/packages/apple/Sources/OpenIapModule.swift @@ -1263,7 +1263,9 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { return first } - private func resolveIosPurchaseProps(from params: RequestPurchaseProps) throws -> RequestPurchaseIosProps { + /// Resolves iOS purchase props from request params. + /// Returns either RequestPurchaseIosProps or RequestSubscriptionIosProps based on request type. + private func resolveIosPurchaseProps(from params: RequestPurchaseProps) throws -> any IosPropsProtocol { switch params.request { case let .purchase(platforms): if let ios = platforms.ios { @@ -1271,17 +1273,7 @@ public final class OpenIapModule: NSObject, OpenIapModuleProtocol { } case let .subscription(platforms): if let ios = platforms.ios { - return RequestPurchaseIosProps( - advancedCommerceData: ios.advancedCommerceData, - andDangerouslyFinishTransactionAutomatically: ios.andDangerouslyFinishTransactionAutomatically, - appAccountToken: ios.appAccountToken, - introductoryOfferEligibility: ios.introductoryOfferEligibility, - promotionalOfferJWS: ios.promotionalOfferJWS, - quantity: ios.quantity, - sku: ios.sku, - winBackOffer: ios.winBackOffer, - withOffer: ios.withOffer - ) + return ios } } throw makePurchaseError(code: .purchaseError, message: "Missing iOS purchase parameters") diff --git a/packages/apple/Sources/OpenIapProtocol.swift b/packages/apple/Sources/OpenIapProtocol.swift index 676a493d..8add5473 100644 --- a/packages/apple/Sources/OpenIapProtocol.swift +++ b/packages/apple/Sources/OpenIapProtocol.swift @@ -1,6 +1,22 @@ import Foundation import StoreKit +// MARK: - iOS Props Protocol + +/// Protocol for iOS purchase/subscription props to enable polymorphic handling. +/// Both RequestPurchaseIosProps and RequestSubscriptionIosProps conform to this protocol. +public protocol IosPropsProtocol { + var sku: String { get } + var quantity: Int? { get } + var appAccountToken: String? { get } + var withOffer: DiscountOfferInputIOS? { get } + var advancedCommerceData: String? { get } + var andDangerouslyFinishTransactionAutomatically: Bool? { get } +} + +extension RequestPurchaseIosProps: IosPropsProtocol {} +extension RequestSubscriptionIosProps: IosPropsProtocol {} + // MARK: - Event Listeners @available(iOS 15.0, macOS 14.0, *) 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 b319f92f..b1e79710 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 @@ -3527,19 +3527,6 @@ public data class RequestPurchaseIosProps( * App account token for user tracking */ val appAccountToken: String? = null, - /** - * Override introductory offer eligibility (iOS 15+, WWDC 2025). - * Set to true to indicate the user is eligible for introductory offer, - * or false to indicate they are not. When nil, the system determines eligibility. - * Back-deployed to iOS 15. - */ - val introductoryOfferEligibility: Boolean? = null, - /** - * JWS promotional offer (iOS 15+, WWDC 2025). - * New signature format using compact JWS string for promotional offers. - * Back-deployed to iOS 15. - */ - val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null, /** * Purchase quantity */ @@ -3549,13 +3536,8 @@ public data class RequestPurchaseIosProps( */ val sku: String, /** - * Win-back offer to apply (iOS 18+) - * Used to re-engage churned subscribers with a discount or free trial. - * Note: Win-back offers only apply to subscription products. - */ - val winBackOffer: WinBackOfferInputIOS? = null, - /** - * Discount offer to apply + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. */ val withOffer: DiscountOfferInputIOS? = null ) { @@ -3565,11 +3547,8 @@ public data class RequestPurchaseIosProps( advancedCommerceData = json["advancedCommerceData"] as? String, andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean, appAccountToken = json["appAccountToken"] as? String, - introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean, - promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }, quantity = (json["quantity"] as? Number)?.toInt(), sku = json["sku"] as? String ?: "", - winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) }, withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) }, ) } @@ -3579,11 +3558,8 @@ public data class RequestPurchaseIosProps( "advancedCommerceData" to advancedCommerceData, "andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically, "appAccountToken" to appAccountToken, - "introductoryOfferEligibility" to introductoryOfferEligibility, - "promotionalOfferJWS" to promotionalOfferJWS?.toJson(), "quantity" to quantity, "sku" to sku, - "winBackOffer" to winBackOffer?.toJson(), "withOffer" to withOffer?.toJson(), ) } @@ -3789,6 +3765,10 @@ public data class RequestSubscriptionIosProps( * via StoreKit Message (automatic) or subscription offer APIs. */ val winBackOffer: WinBackOfferInputIOS? = null, + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ val withOffer: DiscountOfferInputIOS? = null ) { companion object { diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt index 3a0d4987..978e8942 100644 --- a/packages/gql/src/generated/Types.kt +++ b/packages/gql/src/generated/Types.kt @@ -3610,19 +3610,6 @@ public data class RequestPurchaseIosProps( * App account token for user tracking */ val appAccountToken: String? = null, - /** - * Override introductory offer eligibility (iOS 15+, WWDC 2025). - * Set to true to indicate the user is eligible for introductory offer, - * or false to indicate they are not. When nil, the system determines eligibility. - * Back-deployed to iOS 15. - */ - val introductoryOfferEligibility: Boolean? = null, - /** - * JWS promotional offer (iOS 15+, WWDC 2025). - * New signature format using compact JWS string for promotional offers. - * Back-deployed to iOS 15. - */ - val promotionalOfferJWS: PromotionalOfferJWSInputIOS? = null, /** * Purchase quantity */ @@ -3632,13 +3619,8 @@ public data class RequestPurchaseIosProps( */ val sku: String, /** - * Win-back offer to apply (iOS 18+) - * Used to re-engage churned subscribers with a discount or free trial. - * Note: Win-back offers only apply to subscription products. - */ - val winBackOffer: WinBackOfferInputIOS? = null, - /** - * Discount offer to apply + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. */ val withOffer: DiscountOfferInputIOS? = null ) { @@ -3648,11 +3630,8 @@ public data class RequestPurchaseIosProps( advancedCommerceData = json["advancedCommerceData"] as? String, andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as? Boolean, appAccountToken = json["appAccountToken"] as? String, - introductoryOfferEligibility = json["introductoryOfferEligibility"] as? Boolean, - promotionalOfferJWS = (json["promotionalOfferJWS"] as? Map)?.let { PromotionalOfferJWSInputIOS.fromJson(it) }, quantity = (json["quantity"] as? Number)?.toInt(), sku = json["sku"] as? String ?: "", - winBackOffer = (json["winBackOffer"] as? Map)?.let { WinBackOfferInputIOS.fromJson(it) }, withOffer = (json["withOffer"] as? Map)?.let { DiscountOfferInputIOS.fromJson(it) }, ) } @@ -3662,11 +3641,8 @@ public data class RequestPurchaseIosProps( "advancedCommerceData" to advancedCommerceData, "andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically, "appAccountToken" to appAccountToken, - "introductoryOfferEligibility" to introductoryOfferEligibility, - "promotionalOfferJWS" to promotionalOfferJWS?.toJson(), "quantity" to quantity, "sku" to sku, - "winBackOffer" to winBackOffer?.toJson(), "withOffer" to withOffer?.toJson(), ) } @@ -3872,6 +3848,10 @@ public data class RequestSubscriptionIosProps( * via StoreKit Message (automatic) or subscription offer APIs. */ val winBackOffer: WinBackOfferInputIOS? = null, + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ val withOffer: DiscountOfferInputIOS? = null ) { companion object { diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift index f176fbf0..19407cb1 100644 --- a/packages/gql/src/generated/Types.swift +++ b/packages/gql/src/generated/Types.swift @@ -1417,45 +1417,27 @@ public struct RequestPurchaseIosProps: Codable { public var andDangerouslyFinishTransactionAutomatically: Bool? /// App account token for user tracking public var appAccountToken: String? - /// Override introductory offer eligibility (iOS 15+, WWDC 2025). - /// Set to true to indicate the user is eligible for introductory offer, - /// or false to indicate they are not. When nil, the system determines eligibility. - /// Back-deployed to iOS 15. - public var introductoryOfferEligibility: Bool? - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. - public var promotionalOfferJWS: PromotionalOfferJWSInputIOS? /// Purchase quantity public var quantity: Int? /// Product SKU public var sku: String - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// Note: Win-back offers only apply to subscription products. - public var winBackOffer: WinBackOfferInputIOS? - /// Discount offer to apply + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. public var withOffer: DiscountOfferInputIOS? public init( advancedCommerceData: String? = nil, andDangerouslyFinishTransactionAutomatically: Bool? = nil, appAccountToken: String? = nil, - introductoryOfferEligibility: Bool? = nil, - promotionalOfferJWS: PromotionalOfferJWSInputIOS? = nil, quantity: Int? = nil, sku: String, - winBackOffer: WinBackOfferInputIOS? = nil, withOffer: DiscountOfferInputIOS? = nil ) { self.advancedCommerceData = advancedCommerceData self.andDangerouslyFinishTransactionAutomatically = andDangerouslyFinishTransactionAutomatically self.appAccountToken = appAccountToken - self.introductoryOfferEligibility = introductoryOfferEligibility - self.promotionalOfferJWS = promotionalOfferJWS self.quantity = quantity self.sku = sku - self.winBackOffer = winBackOffer self.withOffer = withOffer } } @@ -1630,6 +1612,8 @@ public struct RequestSubscriptionIosProps: Codable { /// The offer is available when the customer is eligible and can be discovered /// via StoreKit Message (automatic) or subscription offer APIs. public var winBackOffer: WinBackOfferInputIOS? + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. public var withOffer: DiscountOfferInputIOS? public init( diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart index 78c49cec..b873b3ef 100644 --- a/packages/gql/src/generated/types.dart +++ b/packages/gql/src/generated/types.dart @@ -3646,11 +3646,8 @@ class RequestPurchaseIosProps { this.advancedCommerceData, this.andDangerouslyFinishTransactionAutomatically, this.appAccountToken, - this.introductoryOfferEligibility, - this.promotionalOfferJWS, this.quantity, required this.sku, - this.winBackOffer, this.withOffer, }); @@ -3663,24 +3660,12 @@ class RequestPurchaseIosProps { final bool? andDangerouslyFinishTransactionAutomatically; /// App account token for user tracking final String? appAccountToken; - /// Override introductory offer eligibility (iOS 15+, WWDC 2025). - /// Set to true to indicate the user is eligible for introductory offer, - /// or false to indicate they are not. When nil, the system determines eligibility. - /// Back-deployed to iOS 15. - final bool? introductoryOfferEligibility; - /// JWS promotional offer (iOS 15+, WWDC 2025). - /// New signature format using compact JWS string for promotional offers. - /// Back-deployed to iOS 15. - final PromotionalOfferJWSInputIOS? promotionalOfferJWS; /// Purchase quantity final int? quantity; /// Product SKU final String sku; - /// Win-back offer to apply (iOS 18+) - /// Used to re-engage churned subscribers with a discount or free trial. - /// Note: Win-back offers only apply to subscription products. - final WinBackOfferInputIOS? winBackOffer; - /// Discount offer to apply + /// Promotional offer to apply (subscriptions only, ignored for one-time purchases). + /// iOS only supports promotional offers for auto-renewable subscriptions. final DiscountOfferInputIOS? withOffer; factory RequestPurchaseIosProps.fromJson(Map json) { @@ -3688,11 +3673,8 @@ class RequestPurchaseIosProps { advancedCommerceData: json['advancedCommerceData'] as String?, andDangerouslyFinishTransactionAutomatically: json['andDangerouslyFinishTransactionAutomatically'] as bool?, appAccountToken: json['appAccountToken'] as String?, - introductoryOfferEligibility: json['introductoryOfferEligibility'] as bool?, - promotionalOfferJWS: json['promotionalOfferJWS'] != null ? PromotionalOfferJWSInputIOS.fromJson(json['promotionalOfferJWS'] as Map) : null, quantity: json['quantity'] as int?, sku: json['sku'] as String, - winBackOffer: json['winBackOffer'] != null ? WinBackOfferInputIOS.fromJson(json['winBackOffer'] as Map) : null, withOffer: json['withOffer'] != null ? DiscountOfferInputIOS.fromJson(json['withOffer'] as Map) : null, ); } @@ -3702,11 +3684,8 @@ class RequestPurchaseIosProps { 'advancedCommerceData': advancedCommerceData, 'andDangerouslyFinishTransactionAutomatically': andDangerouslyFinishTransactionAutomatically, 'appAccountToken': appAccountToken, - 'introductoryOfferEligibility': introductoryOfferEligibility, - 'promotionalOfferJWS': promotionalOfferJWS?.toJson(), 'quantity': quantity, 'sku': sku, - 'winBackOffer': winBackOffer?.toJson(), 'withOffer': withOffer?.toJson(), }; } @@ -3915,6 +3894,8 @@ class RequestSubscriptionIosProps { /// The offer is available when the customer is eligible and can be discovered /// via StoreKit Message (automatic) or subscription offer APIs. final WinBackOfferInputIOS? winBackOffer; + /// Promotional offer to apply for subscription purchases. + /// Requires server-signed offer with nonce, timestamp, keyId, and signature. final DiscountOfferInputIOS? withOffer; factory RequestSubscriptionIosProps.fromJson(Map json) { diff --git a/packages/gql/src/generated/types.gd b/packages/gql/src/generated/types.gd index eb3932ed..2ea458bc 100644 --- a/packages/gql/src/generated/types.gd +++ b/packages/gql/src/generated/types.gd @@ -3054,14 +3054,8 @@ class RequestPurchaseIosProps: var app_account_token: String ## Purchase quantity var quantity: int - ## Discount offer to apply + ## Promotional offer to apply (subscriptions only, ignored for one-time purchases). var with_offer: DiscountOfferInputIOS - ## Win-back offer to apply (iOS 18+) - var win_back_offer: WinBackOfferInputIOS - ## JWS promotional offer (iOS 15+, WWDC 2025). - var promotional_offer_jws: PromotionalOfferJWSInputIOS - ## Override introductory offer eligibility (iOS 15+, WWDC 2025). - var introductory_offer_eligibility: bool ## Advanced commerce data token (iOS 15+). var advanced_commerce_data: String @@ -3080,18 +3074,6 @@ class RequestPurchaseIosProps: obj.with_offer = DiscountOfferInputIOS.from_dict(data["withOffer"]) else: obj.with_offer = data["withOffer"] - if data.has("winBackOffer") and data["winBackOffer"] != null: - if data["winBackOffer"] is Dictionary: - obj.win_back_offer = WinBackOfferInputIOS.from_dict(data["winBackOffer"]) - else: - obj.win_back_offer = data["winBackOffer"] - if data.has("promotionalOfferJWS") and data["promotionalOfferJWS"] != null: - if data["promotionalOfferJWS"] is Dictionary: - obj.promotional_offer_jws = PromotionalOfferJWSInputIOS.from_dict(data["promotionalOfferJWS"]) - else: - obj.promotional_offer_jws = data["promotionalOfferJWS"] - if data.has("introductoryOfferEligibility") and data["introductoryOfferEligibility"] != null: - obj.introductory_offer_eligibility = data["introductoryOfferEligibility"] if data.has("advancedCommerceData") and data["advancedCommerceData"] != null: obj.advanced_commerce_data = data["advancedCommerceData"] return obj @@ -3111,18 +3093,6 @@ class RequestPurchaseIosProps: dict["withOffer"] = with_offer.to_dict() else: dict["withOffer"] = with_offer - if win_back_offer != null: - if win_back_offer.has_method("to_dict"): - dict["winBackOffer"] = win_back_offer.to_dict() - else: - dict["winBackOffer"] = win_back_offer - if promotional_offer_jws != null: - if promotional_offer_jws.has_method("to_dict"): - dict["promotionalOfferJWS"] = promotional_offer_jws.to_dict() - else: - dict["promotionalOfferJWS"] = promotional_offer_jws - if introductory_offer_eligibility != null: - dict["introductoryOfferEligibility"] = introductory_offer_eligibility if advanced_commerce_data != null: dict["advancedCommerceData"] = advanced_commerce_data return dict @@ -3328,6 +3298,7 @@ class RequestSubscriptionIosProps: var and_dangerously_finish_transaction_automatically: bool var app_account_token: String var quantity: int + ## Promotional offer to apply for subscription purchases. var with_offer: DiscountOfferInputIOS ## Win-back offer to apply (iOS 18+) var win_back_offer: WinBackOfferInputIOS diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts index 28e59152..c646d5bc 100644 --- a/packages/gql/src/generated/types.ts +++ b/packages/gql/src/generated/types.ts @@ -1216,30 +1216,14 @@ export interface RequestPurchaseIosProps { andDangerouslyFinishTransactionAutomatically?: (boolean | null); /** App account token for user tracking */ appAccountToken?: (string | null); - /** - * Override introductory offer eligibility (iOS 15+, WWDC 2025). - * Set to true to indicate the user is eligible for introductory offer, - * or false to indicate they are not. When nil, the system determines eligibility. - * Back-deployed to iOS 15. - */ - introductoryOfferEligibility?: (boolean | null); - /** - * JWS promotional offer (iOS 15+, WWDC 2025). - * New signature format using compact JWS string for promotional offers. - * Back-deployed to iOS 15. - */ - promotionalOfferJWS?: (PromotionalOfferJwsInputIOS | null); /** Purchase quantity */ quantity?: (number | null); /** Product SKU */ sku: string; /** - * Win-back offer to apply (iOS 18+) - * Used to re-engage churned subscribers with a discount or free trial. - * Note: Win-back offers only apply to subscription products. + * Promotional offer to apply (subscriptions only, ignored for one-time purchases). + * iOS only supports promotional offers for auto-renewable subscriptions. */ - winBackOffer?: (WinBackOfferInputIOS | null); - /** Discount offer to apply */ withOffer?: (DiscountOfferInputIOS | null); } @@ -1343,6 +1327,10 @@ export interface RequestSubscriptionIosProps { * via StoreKit Message (automatic) or subscription offer APIs. */ winBackOffer?: (WinBackOfferInputIOS | null); + /** + * Promotional offer to apply for subscription purchases. + * Requires server-signed offer with nonce, timestamp, keyId, and signature. + */ withOffer?: (DiscountOfferInputIOS | null); } diff --git a/packages/gql/src/type-ios.graphql b/packages/gql/src/type-ios.graphql index e5e3dd1c..75a77934 100644 --- a/packages/gql/src/type-ios.graphql +++ b/packages/gql/src/type-ios.graphql @@ -218,7 +218,7 @@ type PurchaseOfferIOS { paymentMode: String! } -# iOS purchase/subscribe inputs +# iOS purchase inputs (for in-app products: consumables and non-consumables) input RequestPurchaseIosProps { """ Product SKU @@ -237,29 +237,11 @@ input RequestPurchaseIosProps { """ quantity: Int """ - Discount offer to apply + Promotional offer to apply (subscriptions only, ignored for one-time purchases). + iOS only supports promotional offers for auto-renewable subscriptions. """ withOffer: DiscountOfferInputIOS """ - Win-back offer to apply (iOS 18+) - Used to re-engage churned subscribers with a discount or free trial. - Note: Win-back offers only apply to subscription products. - """ - winBackOffer: WinBackOfferInputIOS - """ - JWS promotional offer (iOS 15+, WWDC 2025). - New signature format using compact JWS string for promotional offers. - Back-deployed to iOS 15. - """ - promotionalOfferJWS: PromotionalOfferJWSInputIOS - """ - Override introductory offer eligibility (iOS 15+, WWDC 2025). - Set to true to indicate the user is eligible for introductory offer, - or false to indicate they are not. When nil, the system determines eligibility. - Back-deployed to iOS 15. - """ - introductoryOfferEligibility: Boolean - """ Advanced commerce data token (iOS 15+). Used with StoreKit 2's Product.PurchaseOption.custom API for passing campaign tokens, affiliate IDs, or other attribution data. @@ -274,6 +256,10 @@ input RequestSubscriptionIosProps { andDangerouslyFinishTransactionAutomatically: Boolean appAccountToken: String quantity: Int + """ + Promotional offer to apply for subscription purchases. + Requires server-signed offer with nonce, timestamp, keyId, and signature. + """ withOffer: DiscountOfferInputIOS """ Win-back offer to apply (iOS 18+)