diff --git a/.vscode/launch.json b/.vscode/launch.json index e66ec3d3..91dd4a72 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,18 +21,22 @@ "console": "integratedTerminal" }, { - "type": "node-terminal", - "request": "launch", "name": "📝 GQL: Generate Types", - "command": "bun run generate", - "cwd": "${workspaceFolder}/packages/gql" + "type": "node", + "request": "launch", + "runtimeExecutable": "bash", + "runtimeArgs": ["-lc", "bun run generate"], + "cwd": "${workspaceFolder}/packages/gql", + "console": "integratedTerminal" }, { - "type": "node-terminal", - "request": "launch", "name": "📚 Docs: Dev Server", - "command": "bun run dev", - "cwd": "${workspaceFolder}/packages/docs" + "type": "node", + "request": "launch", + "runtimeExecutable": "bash", + "runtimeArgs": ["-lc", "bun run dev"], + "cwd": "${workspaceFolder}/packages/docs", + "console": "integratedTerminal" }, { "type": "node-terminal", diff --git a/packages/apple/Sources/Models/Types.swift b/packages/apple/Sources/Models/Types.swift index 9ab84e4b..182e96a5 100644 --- a/packages/apple/Sources/Models/Types.swift +++ b/packages/apple/Sources/Models/Types.swift @@ -610,6 +610,10 @@ public struct DiscountOffer: Codable { public var preorderDetailsAndroid: PreorderDetailsAndroid? /// Numeric price value public var price: Double + /// [Android] Purchase option ID for this offer. + /// Used to identify which purchase option the user selected. + /// Available in Google Play Billing Library 7.0+ + public var purchaseOptionIdAndroid: String? /// [Android] Rental details if this is a rental offer. public var rentalDetailsAndroid: RentalDetailsAndroid? /// Type of discount offer @@ -701,6 +705,21 @@ public enum FetchProductsResult { case subscriptions([ProductSubscription]?) } +/// Installment plan details for subscription offers (Android) +/// Contains information about the installment plan commitment. +/// Available in Google Play Billing Library 7.0+ +public struct InstallmentPlanDetailsAndroid: Codable { + /// Committed payments count after a user signs up for this subscription plan. + /// For example, for a monthly subscription with commitmentPaymentsCount of 12, + /// users will be charged monthly for 12 months after signup. + public var commitmentPaymentsCount: Int + /// Subsequent committed payments count after the subscription plan renews. + /// For example, for a monthly subscription with subsequentCommitmentPaymentsCount of 12, + /// users will be committed to another 12 monthly payments when the plan renews. + /// Returns 0 if the installment plan has no subsequent commitment (reverts to normal plan). + public var subsequentCommitmentPaymentsCount: Int +} + /// Limited quantity information for one-time purchase offers (Android) /// Available in Google Play Billing Library 7.0+ public struct LimitedQuantityInfoAndroid: Codable { @@ -710,6 +729,20 @@ public struct LimitedQuantityInfoAndroid: Codable { public var remainingQuantity: Int } +/// Pending purchase update for subscription upgrades/downgrades (Android) +/// When a user initiates a subscription change (upgrade/downgrade), the new purchase +/// may be pending until the current billing period ends. This type contains the +/// details of the pending change. +/// Available in Google Play Billing Library 5.0+ +public struct PendingPurchaseUpdateAndroid: Codable { + /// Product IDs for the pending purchase update. + /// These are the new products the user is switching to. + public var products: [String] + /// Purchase token for the pending transaction. + /// Use this token to track or manage the pending purchase update. + public var purchaseToken: String +} + /// Pre-order details for one-time purchase products (Android) /// Available in Google Play Billing Library 8.1.0+ public struct PreorderDetailsAndroid: Codable { @@ -793,6 +826,10 @@ public struct ProductAndroidOneTimePurchaseOfferDetail: Codable { public var preorderDetailsAndroid: PreorderDetailsAndroid? public var priceAmountMicros: String public var priceCurrencyCode: String + /// Purchase option ID for this offer (Android) + /// Used to identify which purchase option the user selected. + /// Available in Google Play Billing Library 7.0+ + public var purchaseOptionId: String? /// Rental details for rental offers public var rentalDetailsAndroid: RentalDetailsAndroid? /// Valid time window for the offer @@ -862,6 +899,10 @@ public struct ProductSubscriptionAndroid: Codable, ProductCommon { /// @see https://openiap.dev/docs/types#subscription-offer public struct ProductSubscriptionAndroidOfferDetails: Codable { public var basePlanId: String + /// Installment plan details for this subscription offer. + /// Only set for installment subscription plans; null for non-installment plans. + /// Available in Google Play Billing Library 7.0+ + public var installmentPlanDetails: InstallmentPlanDetailsAndroid? public var offerId: String? public var offerTags: [String] public var offerToken: String @@ -918,6 +959,11 @@ public struct PurchaseAndroid: Codable, PurchaseCommon { public var obfuscatedAccountIdAndroid: String? public var obfuscatedProfileIdAndroid: String? public var packageNameAndroid: String? + /// Pending purchase update for uncommitted subscription upgrade/downgrade (Android) + /// Contains the new products and purchase token for the pending transaction. + /// Returns null if no pending update exists. + /// Available in Google Play Billing Library 5.0+ + public var pendingPurchaseUpdateAndroid: PendingPurchaseUpdateAndroid? public var platform: IapPlatform public var productId: String public var purchaseState: PurchaseState @@ -1067,6 +1113,10 @@ public struct SubscriptionOffer: Codable { /// - iOS: Discount identifier from App Store Connect /// - Android: offerId from ProductSubscriptionAndroidOfferDetails public var id: String + /// [Android] Installment plan details for this subscription offer. + /// Only set for installment subscription plans; null for non-installment plans. + /// Available in Google Play Billing Library 7.0+ + public var installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? /// [iOS] Key identifier for signature validation. /// Used with server-side signature generation for promotional offers. public var keyIdentifierIOS: String? diff --git a/packages/docs/src/pages/docs/types/android.tsx b/packages/docs/src/pages/docs/types/android.tsx index 7160a1cc..7c23ccb6 100644 --- a/packages/docs/src/pages/docs/types/android.tsx +++ b/packages/docs/src/pages/docs/types/android.tsx @@ -188,6 +188,15 @@ function TypesAndroid() { Quantity-limited offer availability + + + purchaseOptionId + + + string | null + + Purchase option ID to identify which option was selected (7.0+) + diff --git a/packages/docs/src/pages/docs/types/offer.tsx b/packages/docs/src/pages/docs/types/offer.tsx index 0871b6fb..7f378eb0 100644 --- a/packages/docs/src/pages/docs/types/offer.tsx +++ b/packages/docs/src/pages/docs/types/offer.tsx @@ -235,6 +235,15 @@ function TypesOffer() { Rental offer details + + + purchaseOptionIdAndroid + + + String + + Purchase option ID for identifying which purchase option was selected (7.0+) + @@ -263,6 +272,7 @@ function TypesOffer() { limitedQuantityInfoAndroid?: LimitedQuantityInfoAndroid; preorderDetailsAndroid?: PreorderDetailsAndroid; rentalDetailsAndroid?: RentalDetailsAndroid; + purchaseOptionIdAndroid?: string; } enum DiscountOfferType { @@ -292,6 +302,7 @@ enum DiscountOfferType { let limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? let preorderDetailsAndroid: PreorderDetailsAndroid? let rentalDetailsAndroid: RentalDetailsAndroid? + let purchaseOptionIdAndroid: String? } enum DiscountOfferType: String, Codable { @@ -320,7 +331,8 @@ enum DiscountOfferType: String, Codable { val validTimeWindowAndroid: ValidTimeWindowAndroid? = null, val limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? = null, val preorderDetailsAndroid: PreorderDetailsAndroid? = null, - val rentalDetailsAndroid: RentalDetailsAndroid? = null + val rentalDetailsAndroid: RentalDetailsAndroid? = null, + val purchaseOptionIdAndroid: String? = null ) enum class DiscountOfferType { @@ -350,6 +362,7 @@ enum class DiscountOfferType { final LimitedQuantityInfoAndroid? limitedQuantityInfoAndroid; final PreorderDetailsAndroid? preorderDetailsAndroid; final RentalDetailsAndroid? rentalDetailsAndroid; + final String? purchaseOptionIdAndroid; DiscountOffer({ this.id, @@ -367,6 +380,7 @@ enum class DiscountOfferType { this.limitedQuantityInfoAndroid, this.preorderDetailsAndroid, this.rentalDetailsAndroid, + this.purchaseOptionIdAndroid, }); } @@ -398,6 +412,7 @@ var valid_time_window_android: ValidTimeWindowAndroid var limited_quantity_info_android: LimitedQuantityInfoAndroid var preorder_details_android: PreorderDetailsAndroid var rental_details_android: RentalDetailsAndroid +var purchase_option_id_android: String enum DiscountOfferType { INTRODUCTORY, @@ -627,6 +642,15 @@ enum DiscountOfferType { Pricing phases (trial, intro, regular) + + + installmentPlanDetailsAndroid + + + InstallmentPlanDetailsAndroid + + Installment plan details for subscription commitments (7.0+) + @@ -660,6 +684,12 @@ enum DiscountOfferType { offerTokenAndroid?: string; offerTagsAndroid?: string[]; pricingPhasesAndroid?: PricingPhasesAndroid; + installmentPlanDetailsAndroid?: InstallmentPlanDetailsAndroid; +} + +interface InstallmentPlanDetailsAndroid { + commitmentPaymentsCount: number; + subsequentCommitmentPaymentsCount: number; } interface SubscriptionPeriod { @@ -707,6 +737,12 @@ enum PaymentMode { let offerTokenAndroid: String? let offerTagsAndroid: [String]? let pricingPhasesAndroid: PricingPhasesAndroid? + let installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? +} + +struct InstallmentPlanDetailsAndroid: Codable { + let commitmentPaymentsCount: Int + let subsequentCommitmentPaymentsCount: Int } struct SubscriptionPeriod: Codable { @@ -753,7 +789,13 @@ enum PaymentMode: String, Codable { val basePlanIdAndroid: String? = null, val offerTokenAndroid: String? = null, val offerTagsAndroid: List? = null, - val pricingPhasesAndroid: PricingPhasesAndroid? = null + val pricingPhasesAndroid: PricingPhasesAndroid? = null, + val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null +) + +data class InstallmentPlanDetailsAndroid( + val commitmentPaymentsCount: Int, + val subsequentCommitmentPaymentsCount: Int ) data class SubscriptionPeriod( @@ -794,6 +836,7 @@ enum class PaymentMode { final String? offerTokenAndroid; final List? offerTagsAndroid; final PricingPhasesAndroid? pricingPhasesAndroid; + final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid; SubscriptionOffer({ required this.id, @@ -814,6 +857,17 @@ enum class PaymentMode { this.offerTokenAndroid, this.offerTagsAndroid, this.pricingPhasesAndroid, + this.installmentPlanDetailsAndroid, + }); +} + +class InstallmentPlanDetailsAndroid { + final int commitmentPaymentsCount; + final int subsequentCommitmentPaymentsCount; + + InstallmentPlanDetailsAndroid({ + required this.commitmentPaymentsCount, + required this.subsequentCommitmentPaymentsCount, }); } @@ -854,6 +908,11 @@ var base_plan_id_android: String var offer_token_android: String var offer_tags_android: Array[String] var pricing_phases_android: PricingPhasesAndroid +var installment_plan_details_android: InstallmentPlanDetailsAndroid + +class InstallmentPlanDetailsAndroid: + var commitment_payments_count: int + var subsequent_commitment_payments_count: int class SubscriptionPeriod: var unit: SubscriptionPeriodUnit diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index 1c077fb7..f4f05c91 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -547,8 +547,81 @@ function TypesPurchase() { ) + + + pendingPurchaseUpdateAndroid + + + Pending subscription upgrade/downgrade details. When a user + initiates a plan change, this contains the new product IDs + and purchase token for the pending transaction. Returns null + if no pending update exists. See{' '} + + PendingPurchaseUpdateAndroid + {' '} + below. ( + + Billing Library 5.0+ + + ) + + + +
+ + PendingPurchaseUpdateAndroid{' '} + + (from{' '} + + Purchase.PendingPurchaseUpdate + + ) + + +

+ Contains details about a pending subscription upgrade or downgrade. + When a user changes their subscription plan, the new plan may be + pending until the current billing period ends. +

+ + + + + + + + + + + + + + + + + +
NameSummary
+ products + + List of product IDs for the pending purchase update. + These are the new products the user is switching to. +
+ purchaseToken + + Unique token identifying the pending transaction. + Use this to track or manage the pending update. +
+
), }} diff --git a/packages/docs/src/pages/docs/updates/notes.tsx b/packages/docs/src/pages/docs/updates/notes.tsx index 5d9a82f7..3165ade2 100644 --- a/packages/docs/src/pages/docs/updates/notes.tsx +++ b/packages/docs/src/pages/docs/updates/notes.tsx @@ -26,6 +26,92 @@ function Notes() { useScrollToHash(); const allNotes: Note[] = [ + // GQL 1.3.17 / Google 1.3.28 - Feb 11, 2026 + { + id: 'gql-1-3-17-google-1-3-28', + date: new Date('2026-02-11'), + element: ( +
+ + 📅 openiap-gql v1.3.17 / openiap-google v1.3.28 - Android BillingClient Enhancement + + +

+ Added new fields from Google Play Billing Library 5.0+ and 7.0+ for offer details, installment plans, and pending subscription updates. +

+ + {/* Section 1: purchaseOptionId */} +
+
+ 1. purchaseOptionId for One-Time Purchase Offers +
+

+ Identifies which purchase option the user selected for one-time products with multiple offers. +

+
+ + ProductAndroidOneTimePurchaseOfferDetail.purchaseOptionId + + + DiscountOffer.purchaseOptionIdAndroid + +
+
+ + {/* Section 2: InstallmentPlanDetailsAndroid */} +
+
+ 2. InstallmentPlanDetailsAndroid for Subscriptions +
+

+ Subscription installment plans - users pay over a commitment period (e.g., 12 monthly payments). +

+ {`type InstallmentPlanDetailsAndroid { + commitmentPaymentsCount: Int! # Initial commitment payments + subsequentCommitmentPaymentsCount: Int! # Renewal commitment (0 = reverts to normal) +}`} +
+ + ProductSubscriptionAndroidOfferDetails.installmentPlanDetails + + + SubscriptionOffer.installmentPlanDetailsAndroid + +
+
+ + {/* Section 3: PendingPurchaseUpdateAndroid */} +
+
+ 3. PendingPurchaseUpdateAndroid for Upgrades/Downgrades +
+

+ Track pending subscription plan changes that take effect at the end of the current billing period. +

+ {`type PendingPurchaseUpdateAndroid { + products: [String!]! # New product IDs user is switching to + purchaseToken: String! # Token for the pending transaction +}`} +
+ + PurchaseAndroid.pendingPurchaseUpdateAndroid + +
+
+ + {/* References */} +
+ References + +
+
+ ), + }, // GQL 1.3.16 / Apple 1.3.14 - Jan 26, 2026 { id: 'gql-1-3-16-apple-1-3-14', @@ -36,75 +122,60 @@ function Notes() { 📅 openiap-gql v1.3.16 / openiap-apple v1.3.14 - ExternalPurchaseCustomLink Support (iOS 18.1+) -

New: ExternalPurchaseCustomLink API Support

-

- Added full support for Apple's ExternalPurchaseCustomLink API (iOS 18.1+) for apps using - custom external purchase links with token-based reporting. -

- -

New APIs:

- - - - - - - - - - - - -
MethodDescription
isEligibleForExternalPurchaseCustomLinkIOS()Check if app can use ExternalPurchaseCustomLink API
getExternalPurchaseCustomLinkTokenIOS(tokenType)Get token for reporting to Apple's External Purchase Server API
showExternalPurchaseCustomLinkNoticeIOS(noticeType)Show CustomLink-specific disclosure notice sheet
- -

New Types:

- - -
- -

Improved: presentExternalPurchaseNoticeSheetIOS()

-

- Now returns externalPurchaseToken field when user continues. This token is required for - reporting transactions to Apple's External Purchase Server API. -

-
-{`// Before
+          

+ Added full support for Apple's ExternalPurchaseCustomLink API (iOS 18.1+) for apps using custom external purchase links with token-based reporting. +

+ +
+
1. New APIs
+
    +
  • isEligibleForExternalPurchaseCustomLinkIOS() - Check if app can use ExternalPurchaseCustomLink API
  • +
  • getExternalPurchaseCustomLinkTokenIOS(tokenType) - Get token for reporting to Apple's External Purchase Server API
  • +
  • showExternalPurchaseCustomLinkNoticeIOS(noticeType) - Show CustomLink-specific disclosure notice sheet
  • +
+
+ +
+
2. New Types
+
    +
  • ExternalPurchaseCustomLinkTokenTypeIOS - Token types: acquisition, services
  • +
  • ExternalPurchaseCustomLinkNoticeTypeIOS - Notice types: browser
  • +
  • ExternalPurchaseCustomLinkTokenResultIOS - Token result with token and error
  • +
  • ExternalPurchaseCustomLinkNoticeResultIOS - Notice result with continued and error
  • +
+
+ +
+
3. Improved presentExternalPurchaseNoticeSheetIOS()
+

+ Now returns externalPurchaseToken field when user continues. This token is required for reporting transactions to Apple's External Purchase Server API. +

+ {`// Before result.result // "continue" or "dismissed" result.error // optional error // After (v1.3.14+) result.result // "continue" or "dismissed" result.externalPurchaseToken // Token string (when result is "continue") -result.error // optional error`} -
- -

API Distinction:

- - - - - - - - - - - - -
APIiOS VersionUse Case
ExternalPurchase17.4+Basic external purchase notice
ExternalPurchaseCustomLink18.1+Custom links with token-based reporting
- -

References:

- +result.error // optional error`} + + +
+
4. API Comparison
+

+ ExternalPurchase (17.4+): Basic external purchase notice | ExternalPurchaseCustomLink (18.1+): Custom links with token-based reporting +

+
+ +
+ References + +
), }, @@ -118,36 +189,26 @@ result.error // optional error`} 📅 openiap-gql v1.3.15 / openiap-google v1.3.27 / openiap-apple v1.3.13 - Bug Fix -

Android - Fix SubscriptionProductReplacementParams ReplacementMode Mapping:

-

- Fixed incorrect replacementModeConstant mapping in applySubscriptionProductReplacementParams. - The function was using values from the legacy SubscriptionUpdateParams.ReplacementMode API instead of - the new SubscriptionProductReplacementParams.ReplacementMode API (Billing Library 8.1.0+). -

-

- Issue: #71 -

- - - - - - - - - - - - - - -
ModeBefore (Wrong)After (Correct)
CHARGE_FULL_PRICE54
DEFERRED65
KEEP_EXISTING76
- -

References:

- +

+ Fixed incorrect replacementModeConstant mapping in applySubscriptionProductReplacementParams. The function was using values from the legacy SubscriptionUpdateParams.ReplacementMode API instead of the new SubscriptionProductReplacementParams.ReplacementMode API (Billing Library 8.1.0+). Issue: #71 +

+ +
+
Mode Value Changes
+
    +
  • CHARGE_FULL_PRICE: 5 → 4
  • +
  • DEFERRED: 6 → 5
  • +
  • KEEP_EXISTING: 7 → 6
  • +
+
+ +
+ References + +
), }, @@ -161,73 +222,54 @@ result.error // optional error`} 📅 openiap-gql v1.3.14 / openiap-google v1.3.25 / openiap-apple v1.3.13 - Breaking Changes & Bug Fixes -

iOS - Subscription-Only Props Cleanup (Breaking Change):

-

- Removed subscription-specific fields from RequestPurchaseIosProps. These fields now only exist in RequestSubscriptionIosProps. -

- -

- Migration: If using these fields for non-subscription purchases, move to requestSubscription() API. -

- -
- -

Known Issue - introductoryOfferEligibility API (Issue #68):

-

- The current introductoryOfferEligibility field uses Boolean type, but Apple's actual - introductoryOfferEligibility(compactJWS:) API - requires a JWS string parameter, not a boolean. -

-

- This will be corrected in a future release. The API signature will change from Boolean to String (JWS). -

- -
- -

Android - Fix displayPrice for Subscriptions with Free Trials:

-

- Fixed an issue where displayPrice returned "Free" or "$0.00" for subscription products - with free trials, instead of the actual base/recurring price. -

-
-{`// Before (bug)
-product.displayPrice  // "Free" or "$0.00"
-product.price         // 0.0
-
-// After (fixed)
-product.displayPrice  // "$9.99" (base recurring price)
-product.price         // 9.99
-
-// Note: Free trial info is still available in subscriptionOffers
-product.subscriptionOffers[0].displayPrice   // "$0.00"
-product.subscriptionOffers[0].paymentMode    // "free-trial"`}
-          
- -
- -

Apple v1.3.13 - Objective-C Bridge Updates:

-

- Updated OpenIapModule+ObjC.swift to properly expose new Swift async functions to Objective-C. - This is critical for kmp-iap and other platforms using Kotlin/Native cinterop. -

- -

- Note: When updating iOS functions in OpenIapModule.swift, always update OpenIapModule+ObjC.swift as well. - See Objective-C Bridge Documentation. -

- -

References:

- +

+ Breaking changes for iOS subscription props, bug fixes for Android displayPrice, and Objective-C bridge updates. +

+ +
+
1. iOS - Subscription-Only Props Cleanup (Breaking Change)
+

+ Removed subscription-specific fields from RequestPurchaseIosProps. These fields now only exist in RequestSubscriptionIosProps. +

+
    +
  • introductoryOfferEligibility - Removed
  • +
  • promotionalOfferJWS - Removed
  • +
  • winBackOffer - Removed
  • +
+

Migration: Use requestSubscription() API.

+
+ +
+
2. Known Issue - introductoryOfferEligibility API (#68)
+

+ Current field uses Boolean type, but Apple's introductoryOfferEligibility(compactJWS:) API requires a JWS string. Will be corrected in future release. +

+
+ +
+
3. Android - Fix displayPrice for Subscriptions with Free Trials
+

+ Fixed displayPrice returning "Free" or "$0.00" instead of actual base/recurring price. +

+ {`// Before (bug): displayPrice = "Free", price = 0.0 +// After (fixed): displayPrice = "$9.99", price = 9.99 +// Free trial info available in: subscriptionOffers[0].displayPrice`} +
+ +
+
4. Apple v1.3.13 - Objective-C Bridge Updates
+

+ Updated OpenIapModule+ObjC.swift to expose new Swift async functions to Objective-C. Critical for kmp-iap. See Objective-C Bridge Documentation. +

+
+ +
+ References + +
), }, @@ -241,78 +283,66 @@ product.subscriptionOffers[0].paymentMode // "free-trial"`} 📅 openiap-gql v1.3.13 / openiap-google v1.3.24 / openiap-apple v1.3.11 - Platform API Gap Analysis -

iOS - Win-Back Offers (iOS 18+):

-

- Added support for win-back offers to re-engage churned subscribers. -

- -
-{`// Apply win-back offer to subscription purchase
-requestSubscription({
-  sku: 'premium_monthly',
-  winBackOffer: { offerId: 'winback_50_off' }
-});`}
-          
- -

iOS - JWS Promotional Offers (iOS 15+, WWDC 2025):

-

- New signature format using compact JWS string for promotional offers. Back-deployed to iOS 15. -

- -

- Note: Requires Xcode 16.4+ to compile. Falls back to legacy signature-based offers until then. -

- -

iOS - Introductory Offer Eligibility Override (iOS 15+, WWDC 2025):

- -

- Note: Requires Xcode 16.4+ to compile. System determines eligibility automatically until then. -

- -
- -

Android - Product Status Codes (Billing 8.0+):

-

- Product-level status codes indicating why products couldn't be fetched. -

- -
-{`// Check product fetch status
-val product = fetchProducts(skus).firstOrNull()
-when (product?.productStatusAndroid) {
-    ProductStatusAndroid.Ok -> { /* Success */ }
-    ProductStatusAndroid.NotFound -> { /* SKU doesn't exist */ }
-    ProductStatusAndroid.NoOffersAvailable -> { /* User not eligible */ }
-    ProductStatusAndroid.Unknown -> { /* Unknown status */ }
-    null -> { /* No product or status */ }
-}`}
-          
- -

Android - Auto Service Reconnection:

-

- enableAutoServiceReconnection() is now always enabled internally since OpenIAP uses Billing Library 8.3.0+. - No configuration needed - the library automatically re-establishes connection if disconnected. -

- -

References:

- +

+ New iOS win-back offers, JWS promotional offers, and Android product status codes. +

+ +
+
1. iOS - Win-Back Offers (iOS 18+)
+

+ Added support for win-back offers to re-engage churned subscribers. +

+
    +
  • winBackOffer - New field in purchase props
  • +
  • WinBackOfferInputIOS - Input type with offerId field
  • +
  • SubscriptionOfferTypeIOS.WinBack - New enum value
  • +
+
+ +
+
2. iOS - JWS Promotional Offers (iOS 15+, WWDC 2025)
+

+ New signature format using compact JWS string for promotional offers. Back-deployed to iOS 15. Requires Xcode 16.4+. +

+
    +
  • promotionalOfferJWS - New field in purchase props
  • +
  • PromotionalOfferJWSInputIOS - Input type with offerId and jws fields
  • +
+
+ +
+
3. iOS - Introductory Offer Eligibility Override (iOS 15+, WWDC 2025)
+

+ introductoryOfferEligibility - Override system eligibility check. Set true/false/nil for system default. Requires Xcode 16.4+. +

+
+ +
+
4. Android - Product Status Codes (Billing 8.0+)
+

+ Product-level status codes indicating why products couldn't be fetched. +

+
    +
  • ProductStatusAndroid - Enum: Ok, NotFound, NoOffersAvailable, Unknown
  • +
  • productStatusAndroid - New field on ProductAndroid
  • +
+
+ +
+
5. Android - Auto Service Reconnection
+

+ enableAutoServiceReconnection() is now always enabled internally since OpenIAP uses Billing Library 8.3.0+. +

+
+ +
+ References + +
), }, @@ -326,87 +356,62 @@ when (product?.productStatusAndroid) { 📅 openiap-gql v1.3.12 / openiap-google v1.3.22 / openiap-apple v1.3.10 - Standardized Offer Types -

New Cross-Platform Offer Types:

-

- Introduced standardized DiscountOffer and SubscriptionOffer types - for unified handling of discounts and subscription offers across iOS and Android. -

- -

DiscountOffer (One-time products):

- - -

SubscriptionOffer:

- - -

New Fields on Product Types:

-
-{`// ProductAndroid & ProductIOS now include:
-discountOffers: [DiscountOffer!]      // One-time product discounts
-subscriptionOffers: [SubscriptionOffer!]  // Subscription offers`}
-          
- -

PaymentMode Logic Fix (Android):

- - -

Deprecated Types:

- - -

Migration Example:

-
-{`// Before (deprecated)
-val discount = product.oneTimePurchaseOfferDetailsAndroid?.firstOrNull()
-val offerToken = discount?.offerToken
-
-// After (recommended)
-val discount = product.discountOffers?.firstOrNull()
-val offerToken = discount?.offerTokenAndroid`}
-          
- -

References:

- +

+ Introduced standardized DiscountOffer and SubscriptionOffer types for unified handling across iOS and Android. +

+ +
+
1. DiscountOffer (One-time products)
+
    +
  • Cross-platform type for one-time purchase discounts
  • +
  • Android fields: offerTokenAndroid, fullPriceMicrosAndroid, percentageDiscountAndroid
  • +
  • Replaces deprecated ProductAndroidOneTimePurchaseOfferDetail
  • +
+
+ +
+
2. SubscriptionOffer
+
    +
  • Cross-platform type for subscription offers (introductory, promotional)
  • +
  • Includes paymentMode: FreeTrial, PayAsYouGo, PayUpFront
  • +
  • Replaces deprecated ProductSubscriptionAndroidOfferDetails, DiscountOfferIOS, DiscountIOS
  • +
+
+ +
+
3. New Fields on Product Types
+
    +
  • discountOffers: [DiscountOffer!] - One-time product discounts
  • +
  • subscriptionOffers: [SubscriptionOffer!] - Subscription offers
  • +
+
+ +
+
4. PaymentMode Logic Fix (Android)
+
    +
  • Zero price → FreeTrial (regardless of recurrenceMode)
  • +
  • NON_RECURRING (3) with paid → PayUpFront
  • +
  • FINITE_RECURRING (2) / INFINITE_RECURRING (1) with paid → PayAsYouGo
  • +
+
+ +
+
5. Deprecated Types
+
    +
  • ProductAndroidOneTimePurchaseOfferDetailDiscountOffer
  • +
  • ProductSubscriptionAndroidOfferDetailsSubscriptionOffer
  • +
  • oneTimePurchaseOfferDetailsAndroiddiscountOffers
  • +
  • subscriptionOfferDetailsAndroidsubscriptionOffers
  • +
+
+ +
+ References + +
), }, @@ -420,160 +425,49 @@ val offerToken = discount?.offerTokenAndroid`} 📅 openiap-gql v1.3.11 / openiap-google v1.3.21 / openiap-apple v1.3.9 - PurchaseState Cleanup - {/* PurchaseState Changes */} -

PurchaseState Simplified:

-

- Removed unused Failed, Restored, and{' '} - Deferred states from PurchaseState enum. -

-
-{`// Before
-enum PurchaseState {
-  Pending, Purchased, Failed, Restored, Deferred, Unknown
-}
-
-// After
-enum PurchaseState {
-  Pending, Purchased, Unknown
-}`}
-          
-

Why removed?

- -

- Note: Apple StoreKit 1's{' '} - - SKPaymentTransactionState - {' '} - (purchasing, purchased, failed, restored, deferred) is fully deprecated. StoreKit 2 uses{' '} - - Product.PurchaseResult - {' '} - instead, which only provides a Transaction on success. -

-

References:

- - -
- -

- API Consolidation: Deprecated{' '} - AlternativeBillingModeAndroid in favor of unified{' '} - BillingProgramAndroid enum. -

- - {/* GQL 1.3.11 Changes */} -

GQL v1.3.11 Other Changes:

- - - {/* Google 1.3.21 Changes */} -

Google v1.3.21 Changes:

- - -

Migration Guide:

- - - - - - - - - - - - - - - - - -
Before (Deprecated)After (Recommended)
alternativeBillingModeAndroid: USER_CHOICEenableBillingProgramAndroid: USER_CHOICE_BILLING
alternativeBillingModeAndroid: ALTERNATIVE_ONLYenableBillingProgramAndroid: EXTERNAL_OFFER
-
-{`// Before (deprecated)
-val config = InitConnectionConfig(
-    alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice
-)
-
-// After (recommended)
-val config = InitConnectionConfig(
-    enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
-)`}
-          
+

+ Simplified PurchaseState enum and deprecated AlternativeBillingModeAndroid in favor of BillingProgramAndroid. +

+ +
+
1. PurchaseState Simplified
+

+ Removed unused Failed, Restored, Deferred states. Now: Pending, Purchased, Unknown +

+
    +
  • Failed - Platforms return errors instead
  • +
  • Restored - Returns as Purchased state
  • +
  • Deferred - StoreKit 2 has no transaction state; Android uses Pending
  • +
+
+ +
+
2. API Consolidation - BillingProgramAndroid
+

+ Deprecated AlternativeBillingModeAndroid in favor of unified BillingProgramAndroid enum. +

+
    +
  • BillingProgramAndroid.USER_CHOICE_BILLING - New enum value (7.0+)
  • +
  • AlternativeBillingModeAndroid - Deprecated
  • +
  • InitConnectionConfig.alternativeBillingModeAndroid - Deprecated
  • +
+
+ +
+
3. Migration
+
    +
  • alternativeBillingModeAndroid: USER_CHOICEenableBillingProgramAndroid: USER_CHOICE_BILLING
  • +
  • alternativeBillingModeAndroid: ALTERNATIVE_ONLYenableBillingProgramAndroid: EXTERNAL_OFFER
  • +
+
+ +
+ References + +
), }, @@ -584,185 +478,47 @@ val config = InitConnectionConfig( element: (
- 📅 openiap-gql v1.3.10 / openiap-google v1.3.19 / openiap-apple v1.3.8 -{' '} - - Google Play Billing 8.3.0 External Payments - + 📅 openiap-gql v1.3.10 / openiap-google v1.3.19 / openiap-apple v1.3.8 - Google Play Billing 8.3.0 External Payments - {/* GQL 1.3.10 */} -

- GQL v1.3.10 - InitConnectionConfig Enhancement: -

-

- Added enableBillingProgramAndroid field to{' '} - InitConnectionConfig for easier billing program setup during connection initialization. -

-
    -
  • - - enableBillingProgramAndroid: BillingProgramAndroid - {' '} - - Enable a specific billing program during initConnection() -
  • -
-
-{`// Enable External Payments during connection
-val config = InitConnectionConfig(
-    enableBillingProgramAndroid = BillingProgramAndroid.ExternalPayments
-)
-iapStore.initConnection(config)`}
-          
-

- This provides a cleaner alternative to calling enableBillingProgram(){' '} - separately before initConnection(). -

- -
- - {/* Apple 1.3.8 */} -

- Apple v1.3.8 - Auto Connection Management: -

-

- All API methods now automatically call initConnection() internally - if the connection hasn't been established yet. This eliminates the need to - manually call initConnection() before using any API. -

-
    -
  • - ensureConnection() - New internal helper that automatically initializes the connection when needed -
  • -
  • - All public API methods (fetchProducts, requestPurchase,{' '} - finishTransaction, etc.) now use ensureConnection() -
  • -
  • - Backward Compatible - Existing code that calls{' '} - initConnection() explicitly will continue to work -
  • -
-

Before (v1.3.7):

-
-{`// Must call initConnection first
-try await OpenIapModule.shared.initConnection()
-let products = try await OpenIapModule.shared.fetchProducts(request)`}
-          
-

After (v1.3.8):

-
-{`// Just call the API directly - connection is handled automatically
-let products = try await OpenIapModule.shared.fetchProducts(request)`}
-          
-

- Note: While explicit initConnection() is no longer required, - you may still want to call it during app startup to pre-initialize the - StoreKit connection for faster subsequent API calls. -

- -
- - {/* Google 1.3.19 - External Payments */} -

- Google v1.3.19 - External Payments Program (Japan Only): -

-

- Google Play Billing Library 8.3.0 introduces the External Payments - program, which presents a side-by-side choice between Google Play - Billing and the developer's external payment option directly in the - purchase flow. -

-

New APIs:

-
    -
  • - BillingProgramAndroid.EXTERNAL_PAYMENTS{' '} - - New billing program type for external payments -
  • -
  • - DeveloperBillingOptionParamsAndroid{' '} - - Configure external payment option in purchase flow -
  • -
  • - DeveloperBillingLaunchModeAndroid{' '} - - How to launch the external payment link -
  • -
  • - DeveloperProvidedBillingDetailsAndroid{' '} - - Contains externalTransactionToken when user selects developer billing -
  • -
  • - developerProvidedBillingListenerAndroid{' '} - - New listener for when user selects developer billing -
  • -
  • - developerBillingOptionAndroid{' '} - - New field in RequestPurchaseAndroidProps and RequestSubscriptionAndroidProps -
  • -
-

New Event:

-
    -
  • - IapEvent.DeveloperProvidedBillingAndroid{' '} - - Fired when user selects developer billing in External Payments flow -
  • -
-

Key Differences from User Choice Billing:

- - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureUser Choice BillingExternal Payments
Billing Library7.0+8.3.0+
AvailabilityEligible regionsJapan only
UISeparate dialogSide-by-side in purchase dialog
-

References:

- +

+ InitConnectionConfig enhancement, auto connection management for iOS, and External Payments program support. +

+ +
+
1. GQL v1.3.10 - InitConnectionConfig Enhancement
+

+ Added enableBillingProgramAndroid: BillingProgramAndroid field for easier billing program setup during initConnection(). +

+
+ +
+
2. Apple v1.3.8 - Auto Connection Management
+

+ All API methods now automatically call initConnection() internally. No need to manually call it before using any API. Backward compatible. +

+
+ +
+
3. Google v1.3.19 - External Payments Program (Japan Only)
+

+ Billing Library 8.3.0 introduces side-by-side choice between Google Play Billing and developer's external payment. +

+
    +
  • BillingProgramAndroid.EXTERNAL_PAYMENTS - New billing program type
  • +
  • DeveloperBillingOptionParamsAndroid - Configure external payment option
  • +
  • DeveloperProvidedBillingDetailsAndroid - Contains externalTransactionToken
  • +
  • IapEvent.DeveloperProvidedBillingAndroid - New event
  • +
+
+ +
+ References + +
), }, @@ -774,117 +530,40 @@ let products = try await OpenIapModule.shared.fetchProducts(request)`} element: (
- 📅 openiap-google v1.3.16 -{' '} - - Google Play Billing 8.2.1 - + 📅 openiap-google v1.3.16 - Google Play Billing 8.2.1 -

- Billing Library Upgrade: 8.1.0 → 8.2.1 -

-

- Upgraded to Google Play Billing Library 8.2.1 which includes the new - Billing Programs API and bug fixes. -

-

- Why 8.2.1 instead of 8.2.0? -

-

- Version 8.2.0 had a bug in isBillingProgramAvailableAsync{' '} - and createBillingProgramReportingDetailsAsync. This was - fixed in 8.2.1 (released 2025-12-15). -

-

- New APIs for External Content Links and External Offers: -

-
    -
  • - - enableBillingProgram() - {' '} - - Setup BillingClient for billing programs before{' '} - initConnection() -
  • -
  • - - isBillingProgramAvailableAsync() - {' '} - - Determine user eligibility for the billing program -
  • -
  • - - createBillingProgramReportingDetailsAsync() - {' '} - - Create external transaction token for reporting -
  • -
  • - - launchExternalLink() - {' '} - - Initiate external link to digital content offer or app download -
  • -
-

- Deprecated External Offers APIs: -

-
    -
  • - - enableExternalOffer() - {' '} - → Use enableBillingProgram(BillingProgramAndroid.ExternalOffer) -
  • -
  • - - isExternalOfferAvailableAsync() - {' '} - → Use isBillingProgramAvailable(BillingProgramAndroid.ExternalOffer) -
  • -
  • - - createExternalOfferReportingDetailsAsync() - {' '} - → Use createBillingProgramReportingDetails() -
  • -
  • - - showExternalOfferInformationDialog() - {' '} - → Use launchExternalLink() -
  • -
-

- References: -

- + +

+ Upgraded from 8.1.0 to 8.2.1 with new Billing Programs API. Skipped 8.2.0 due to bugs in isBillingProgramAvailableAsync and createBillingProgramReportingDetailsAsync. +

+ +
+
1. New APIs
+
    +
  • enableBillingProgram() - Setup BillingClient for billing programs
  • +
  • isBillingProgramAvailableAsync() - Determine user eligibility
  • +
  • createBillingProgramReportingDetailsAsync() - Create external transaction token
  • +
  • launchExternalLink() - Initiate external link
  • +
+
+ +
+
2. Deprecated APIs
+
    +
  • enableExternalOffer()enableBillingProgram(BillingProgramAndroid.ExternalOffer)
  • +
  • isExternalOfferAvailableAsync()isBillingProgramAvailable()
  • +
  • createExternalOfferReportingDetailsAsync()createBillingProgramReportingDetails()
  • +
  • showExternalOfferInformationDialog()launchExternalLink()
  • +
+
+ +
+ References + +
), }, @@ -896,43 +575,18 @@ let products = try await OpenIapModule.shared.fetchProducts(request)`} element: (
- 📅 openiap-gql v1.3.8 + 📅 openiap-gql v1.3.8 - Kotlin Null-Safe Casting -

- Kotlin Type Generation: Null-Safe Casting -

-

- Fixed potential TypeCastException in generated Kotlin - types by using safe casts (as?) instead of unsafe casts - (as). + +

+ Fixed potential TypeCastException in generated Kotlin types by using safe casts (as?) instead of unsafe casts (as).

-
    -
  • - Lists now use mapNotNull with safe element casting -
  • -
  • - Non-nullable fields provide sensible defaults (empty string, - false, 0, emptyList) -
  • -
  • - Prevents crashes when JSON keys are missing or contain unexpected - null values -
  • + +
      +
    • Lists now use mapNotNull with safe element casting
    • +
    • Non-nullable fields provide sensible defaults (empty string, false, 0, emptyList)
    • +
    • Prevents crashes when JSON keys are missing or contain unexpected null values
    -

    - Before (unsafe): -

    - - {`offerTags = (json["offerTags"] as List<*>).map { it as String } -offerToken = json["offerToken"] as String`} - -

    - After (null-safe): -

    - - {`offerTags = (json["offerTags"] as? List<*>)?.mapNotNull { it as? String } ?: emptyList() -offerToken = json["offerToken"] as? String ?: ""`} -
), }, @@ -944,138 +598,34 @@ offerToken = json["offerToken"] as? String ?: ""`} element: (
- 📅 openiap-gql v1.3.7 / openiap-apple v1.3.7 / openiap-google v1.3.15 + 📅 openiap-gql v1.3.7 / openiap-apple v1.3.7 / openiap-google v1.3.15 - Advanced Commerce Data -

- New Feature: Advanced Commerce Data -

-

- Added support for{' '} - - StoreKit 2's Product.PurchaseOption.custom API - {' '} - to pass attribution data during purchases. -

-
    -
  • - - advancedCommerceData - {' '} - - New optional field in RequestPurchaseIosProps and{' '} - RequestSubscriptionIosProps -
  • -
  • - Enables passing campaign tokens, affiliate IDs, and other - attribution data to StoreKit during purchase -
  • -
  • - Data is formatted as JSON:{' '} - {`{"signatureInfo": {"token": ""}}`} -
  • -
-

- Usage: -

- - {`requestPurchase({ - request: { - apple: { - sku: 'com.example.premium', - advancedCommerceData: 'campaign_summer_2025', - } - }, - type: 'in-app' -});`} - -

- Use Cases: -

-
    -
  • Campaign attribution tracking
  • -
  • Affiliate marketing integration
  • -
  • Promotional code tracking
  • -
-

- Reference:{' '} - - react-native-iap PR #3106 - -

-

- - Deprecated:{' '} - - requestPurchaseOnPromotedProductIOS() - - -

-

- The{' '} - - requestPurchaseOnPromotedProductIOS() - {' '} - API is now deprecated. In StoreKit 2, promoted products can be - purchased directly via the standard requestPurchase(){' '} - flow. -

-
    -
  • - Use promotedProductListenerIOS to receive the product - ID when a user taps a promoted product in the App Store -
  • -
  • - Call requestPurchase() with the received SKU directly -
  • -
- - {`// Recommended approach -promotedProductListenerIOS(async (productId) => { - await requestPurchase({ - request: { apple: { sku: productId } }, - type: 'in-app' - }); -});`} - -

- - Android: Support for `google` field (openiap-google v1.3.15) - -

-

- The Android library now supports the google field in - request parameters, with fallback to the deprecated{' '} - android{' '} - field for backward compatibility. -

- - {`// Recommended (new) -requestPurchase(RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - google = RequestPurchaseAndroidProps(skus = listOf("sku_id")) - ) - ), - type = ProductQueryType.InApp -)) - -// Still supported (deprecated) -requestPurchase(RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - android = RequestPurchaseAndroidProps(skus = listOf("sku_id")) - ) - ), - type = ProductQueryType.InApp -))`} - + +

+ Added support for StoreKit 2's Product.PurchaseOption.custom API to pass attribution data during purchases. +

+ +
+
1. advancedCommerceData Field
+
    +
  • New optional field in RequestPurchaseIosProps and RequestSubscriptionIosProps
  • +
  • Use cases: Campaign attribution, affiliate marketing, promotional code tracking
  • +
+
+ +
+
2. Deprecated requestPurchaseOnPromotedProductIOS()
+

+ In StoreKit 2, use promotedProductListenerIOS + requestPurchase() directly. +

+
+ +
+
3. Android: google Field Support
+

+ Now supports google field with fallback to deprecated android field. +

+
), }, @@ -1089,45 +639,16 @@ requestPurchase(RequestPurchaseProps( 📅 openiap-gql v1.3.5 / openiap-apple v1.3.5 - GitHub Release Tag Management Update -

- GitHub Release Tag Naming Convention: -

-

- No API changes in this release. This update focuses on GitHub - release tag management for better Swift Package Manager (SPM) - compatibility. + +

+ No API changes. Updated GitHub release tag management for Swift Package Manager (SPM) compatibility.

-